Compare commits

...

129 Commits

Author SHA1 Message Date
akwizgran
75dfa80541 Bump version numbers for 1.2.4 release. 2019-11-06 09:58:00 +00:00
akwizgran
41b59fbcfe Merge branch '1610-pending-contacts-offline-snackbar' into 'master'
Don't show offline snackbar when there's no pending contacts

Closes #1610

See merge request briar/briar!1193
2019-11-06 09:50:39 +00:00
akwizgran
98a4f5def1 Merge branch '1654-notification-channel-unavailable' into 'master'
Fail gracefully when ACTION_CHANNEL_NOTIFICATION_SETTINGS is not available

Closes #1654

See merge request briar/briar!1192
2019-11-06 09:49:21 +00:00
akwizgran
aeefa35f38 Merge branch '1454-theme-system-crash' into 'master'
Prevent crash when user has set theme to system default on unsupported API level

Closes #1454

See merge request briar/briar!1191
2019-11-06 09:46:48 +00:00
akwizgran
4e7f33edfd Merge branch '1483-group-invite-not-allowed' into 'master'
Make sure group actions can only be made by the correct role

See merge request briar/briar!1190
2019-11-06 09:45:28 +00:00
akwizgran
f1e957ffed Merge branch '1655-no-bluetooth-activity' into 'master'
Check if REQUEST_BLUETOOTH_DISCOVERABLE is available before launching

Closes #1655

See merge request briar/briar!1189
2019-11-06 09:42:00 +00:00
akwizgran
9e3fed6bc0 Merge branch '1651-no-ringtone-picker' into 'master'
Check if ringtone picker is available before launching it

Closes #1485 and #1651

See merge request briar/briar!1188
2019-11-06 09:40:30 +00:00
Torsten Grote
bf9a39cc6c [android] don't show offline snackbar when there's no pending contacts
If the pending contact list is opened concurrently
with the last pending contact being removed (unlikely but possible)
then the "no internet connection" snackbar would be shown
even though the app is connected to Tor.
2019-11-05 15:35:10 -03:00
Torsten Grote
72aa5397f8 [android] fail gracefully when ACTION_CHANNEL_NOTIFICATION_SETTINGS is not available 2019-11-05 15:17:10 -03:00
Torsten Grote
21eaab3259 [android] prevent crash when user has set theme to system default
on an API level that does not support it.
2019-11-05 14:57:25 -03:00
Torsten Grote
92d595da35 [android] make sure group actions can only be made by the correct role 2019-11-05 14:46:10 -03:00
Torsten Grote
5e85566fc3 [android] check if REQUEST_BLUETOOTH_DISCOVERABLE is available before launching 2019-11-05 12:54:02 -03:00
Torsten Grote
1574bf35fc [android] do not use file:// Uris for notification sounds
This causes a FileUriExposedException otherwise.

Closes #1485
2019-11-05 12:31:25 -03:00
Torsten Grote
533e01e881 [android] check if ringtone picker is available before launching
Also refuse file:// Uri as they cause a FileUriExposedException as in #1485
2019-11-05 12:03:00 -03:00
akwizgran
383367f0c8 Merge branch 'remove-remove-contacts-feature-flag' into 'master'
Remove contacts feature flag

See merge request briar/briar!1185
2019-11-01 14:29:37 +00:00
Torsten Grote
ca052ea7dd update translations 2019-11-01 11:12:26 -03:00
Torsten Grote
5147f6b7e6 Remove RemoteContacts feature flag in preparation of 1.2 release 2019-11-01 11:09:43 -03:00
akwizgran
84a8ff1dd8 Merge branch '1629-delete-message-subset' into 'master'
Support for deleting a subset of all conversation messages

Closes #1629

See merge request briar/briar!1180
2019-10-28 16:52:41 +00:00
Torsten Grote
6c489fbea3 [core] also delete attachments when deleting select messages 2019-10-28 10:22:04 -03:00
Torsten Grote
c7200910c9 [core] address feedback for selective conversation message deletion 2019-10-28 09:45:41 -03:00
akwizgran
663e5c4b46 Merge branch '1405-emoji-keyboard' into 'master'
Always show keyboard when clicking text input field

Closes #1405

See merge request briar/briar!1181
2019-10-28 12:16:15 +00:00
Torsten Grote
529eaceec7 [android] show keyboard when clicking text input field 2019-10-22 12:43:35 -03:00
Torsten Grote
f516dbe34f [core] add method to ConversationManager for deleting a set of messages 2019-10-22 11:18:10 -03:00
Torsten Grote
5b515d7e18 [core] implement subset conversation message deletion for IntroductionManager 2019-10-22 11:18:10 -03:00
Torsten Grote
ef04a26cfc [core] implement subset conversation message deletion for GroupInvitationManager 2019-10-22 11:18:09 -03:00
Torsten Grote
2e6fe42074 [core] implement subset conversation message deletion for SharingManager 2019-10-22 11:18:09 -03:00
Torsten Grote
124e2f99b0 [core] Add method to ConversationClient for deleting a set of messages
This also implements the method for MessagingManager
(including integration tests) and adds no-op implementations for other
clients.
2019-10-22 11:18:09 -03:00
Torsten Grote
190a6bff96 [core] Add method to ConversationClient that returns a set of MessageIds it is responsible for 2019-10-22 11:18:08 -03:00
Torsten Grote
01df141c08 Merge branch '843-landscape-keyboard' into 'master'
Raise target API version to 28 and fix soft keyboard issues

Closes #1505

See merge request briar/briar!1043
2019-10-21 12:38:38 +00:00
Torsten Grote
d7c9bf80de Merge branch 'xml-formatting-settings' into 'master'
Update XML code style settings

See merge request briar/briar!1178
2019-10-18 16:51:30 +00:00
akwizgran
3a5e51e248 Update XML code style settings. 2019-10-18 17:38:41 +01:00
akwizgran
a76e3dcec1 Fix bug with enter key when rotating screen. 2019-10-18 14:03:01 +01:00
akwizgran
0fdc7199ed Hide keyboard when contact alias dialog is closed. 2019-10-18 14:03:01 +01:00
akwizgran
248f482fee Use requestFocus tag for RSS import. 2019-10-18 14:03:00 +01:00
akwizgran
4196d046a3 Use stateAlwaysVisible for consistent behaviour. 2019-10-18 14:03:00 +01:00
akwizgran
722ebb22f6 Use requestFocus tag to request initial focus. 2019-10-18 13:45:48 +01:00
akwizgran
a4f561ca1a Request focus when showing soft keyboard. 2019-10-18 13:45:48 +01:00
akwizgran
c7db0bf6fa Remove unused listener implementation. 2019-10-18 13:45:47 +01:00
akwizgran
ca6f458551 Always hide keyboard when importing RSS feed. 2019-10-18 13:45:47 +01:00
akwizgran
c85990408a Remove redundant requestFocus() call. 2019-10-18 13:45:47 +01:00
akwizgran
3ed0204170 Clean up soft input modes. 2019-10-18 13:45:46 +01:00
akwizgran
e2b3340734 Remove redundant methods for showing/hiding keyboard. 2019-10-18 13:45:45 +01:00
akwizgran
78aac8de52 Replace EditText with TextInputEditText. 2019-10-18 13:45:45 +01:00
akwizgran
971ae3a20e Raise target API level to 28. 2019-10-18 13:45:44 +01:00
Torsten Grote
622e7a775a [android] Soft keyboard fixes
1. Manually request focus for input fields and show keyboard

This is needed when targetting API 28 which doesn't give focus anymore
automatically like it used to be.

Closes #1505

2. Remember keyboard states across screen rotations

This also upgrades the emoji library and gets rid of the
KeyboardAwareLinearLayout that is still a relict from the time when we
were using Signal's emoji implementation.

3. Move soft keyboard showing/hiding into UiUtils
2019-10-18 13:44:44 +01:00
akwizgran
103e8482b0 Merge branch 'codeStylesAS3.5' into 'master'
Android Studio 3.5 changed our codeStyles

See merge request briar/briar!1177
2019-10-17 16:56:13 +00:00
Torsten Grote
ddcfc11012 Android Studio 3.5 changed our codeStyles 2019-10-17 13:33:51 -03:00
akwizgran
ab2e40abde Merge branch '1565-duplicate-remote-contacts' into 'master'
UX for handling duplicate handshake links

Closes #1565

See merge request briar/briar!1173
2019-10-16 16:16:08 +00:00
Torsten Grote
1ddceaadd6 Always replace pending contacts no matter their state when link is re-entered 2019-10-16 13:06:21 -03:00
akwizgran
7a644f7d8b Merge branch '1210-fix-list-duplicates' into 'master'
[android] Fix duplicate items in lists

Closes #1210

See merge request briar/briar!1174
2019-10-16 14:32:49 +00:00
Torsten Grote
397afbfec0 Address review comments for detecting duplicate (pending) contacts 2019-10-16 11:15:14 -03:00
Torsten Grote
0d4cb05ac0 [android] fix possible duplicates in list
When doing reloads of list items such as when adding test contacts,
we loaded different versions of those items and added them to the list.
According to the documentation
https://developer.android.com/reference/android/support/v7/util/SortedList.html#add
> If the sorting criteria of the item is changed,
> SortedList won't be able to find its duplicate in the list
> which will result in having a duplicate of the Item in the list.

For the contact list at least, new contacts caused reloads of the entire list
and new messages caused the contacts to be sorted differently.
Thus we ended up with duplicate contacts in the list.

This commit fixes this by replacing the contacts in the list instead of adding them.
It applies the same fix to forums and private groups
which use the same logic and are thus also affected.

Fixes #1210
2019-10-15 16:25:10 -03:00
Torsten Grote
aa0937e6aa [android] Show dialog when (pending) contact already exists
If two different people sent the same link, show warning dialog to the
user to prevent a social attack trying to discover contact
relationships.
2019-10-15 14:47:42 -03:00
Torsten Grote
4bf8d4c0e7 [bramble] add method for getting pending contact state 2019-10-15 14:46:37 -03:00
Torsten Grote
75fcd28071 [bramble] throw exceptions when adding pending contact which exists 2019-10-15 10:32:52 -03:00
Torsten Grote
5f29ab3b40 [bramble-core] Add DB method for getting contact by handshake key 2019-10-15 10:12:59 -03:00
Torsten Grote
f45d00e23c Update translations, add Bosnian and Swahili 2019-10-14 15:11:44 -03:00
akwizgran
2b589c2da6 Merge branch 'tor64' into 'master'
Add support for 64-bit Tor binaries

Closes #1506

See merge request briar/briar!1161
2019-10-14 16:33:53 +00:00
akwizgran
67d15ec82e Merge branch '1633-min-api-16' into 'master'
[android] Raise minimum API level to 16

Closes #1633

See merge request briar/briar!1171
2019-10-14 15:37:43 +00:00
akwizgran
2d44d749ba Merge branch '1627-test-fix' into 'master'
Fix group sharing message deletion test

See merge request briar/briar!1168
2019-10-14 15:36:12 +00:00
Torsten Grote
6ef86c5638 Merge branch 'remove-tor-settings-migration' into 'master'
Remove old migration code for Tor settings

See merge request briar/briar!1172
2019-10-14 15:28:24 +00:00
akwizgran
131f9b9696 Remove old migration code for Tor settings. 2019-10-14 16:00:43 +01:00
akwizgran
a876d4cfb7 Remove a couple of redundant comments. 2019-10-14 15:59:14 +01:00
akwizgran
fafcacf808 Remove a couple more API version checks. 2019-10-14 15:56:44 +01:00
akwizgran
7a0d990f0b Don't include non-PIE binaries in APK.
This shouldn't be merged before raising the minimum
API version to 16.
2019-10-14 15:49:37 +01:00
Torsten Grote
234bdf686e [android] Raise minimum API level to 16 2019-10-14 11:49:06 -03:00
akwizgran
edb9da107f Merge branch '1632-allow-resharing-shareable' into 'master'
Allow sharer to re-share a shareable again after leaving

Closes #1632

See merge request briar/briar!1169
2019-10-14 14:29:38 +00:00
Torsten Grote
d1d4914c6a Merge branch '1582-restore-recycler-view-behaviour' into 'master'
Restore custom layout behaviour for handling snackbar

Closes #1582

See merge request briar/briar!1170
2019-10-14 14:26:01 +00:00
Torsten Grote
9261d23bba [core] allow sharer to re-share a shareable again after leaving 2019-10-14 11:13:01 -03:00
akwizgran
f4febe90c9 Restore custom layout behaviour for handling snackbar. 2019-10-14 14:45:23 +01:00
Torsten Grote
ecd766b204 [core] Fix group sharing message deletion test 2019-10-14 09:40:52 -03:00
akwizgran
ca4fc2dc26 Merge branch '1627-delete-completed-privategroup-sessions' into 'master'
Delete conversation messages belonging to completed private group sessions

Closes #1627

See merge request briar/briar!1167
2019-10-14 11:57:27 +00:00
akwizgran
c3ddcdffe0 Merge branch '1627-delete-completed-sharing-sessions' into 'master'
Delete conversation messages belonging to completed sharing sessions

See merge request briar/briar!1164
2019-10-14 11:45:30 +00:00
Torsten Grote
2e37619357 [android] use new obfs4 release with only pie builds and fixed arm64 2019-10-10 10:01:15 -03:00
Torsten Grote
c247d745df [bramble-android] add support for 64-bit Tor binaries 2019-10-10 09:29:24 -03:00
akwizgran
3a4de3d2cb Merge branch '68-fix-message-tracker' into 'master'
Fix MessageTracker group counts after deleting messages

See merge request briar/briar!1166
2019-10-10 08:54:45 +00:00
Torsten Grote
04f1036dbf [android] Change non-deletion message to refer to ongoing sessions 2019-10-09 17:21:41 -03:00
Torsten Grote
9736f9d31f [core] allow messages from private group sessions with responses get deleted 2019-10-09 17:21:41 -03:00
Torsten Grote
440d5239b1 [core] track GroupCount properly when deleting messages from SharingManager 2019-10-09 13:32:41 -03:00
Torsten Grote
e4a8b10b94 [core] allow messages from shareable sessions with responses get deleted 2019-10-09 13:22:37 -03:00
Torsten Grote
41676065c5 [core] Fix MessageTracker group counts after deleting messages 2019-10-09 13:19:43 -03:00
Torsten Grote
1fcc83a0d0 Merge branch 'feature-flag-message-deletion' into 'master'
Add feature flag for private message deletion

See merge request briar/briar!1165
2019-10-09 15:56:17 +00:00
akwizgran
249b85cd26 Add feature flag for private message deletion. 2019-10-09 16:22:04 +01:00
akwizgran
a23e0699d8 Merge branch '1627-delete-completed-introduction-sessions' into 'master'
Delete conversation messages belonging to completed introduction sessions

See merge request briar/briar!1163
2019-10-09 12:39:05 +00:00
Torsten Grote
e3e47dae48 [core] throw AssertionError if SessionId is missing
Also remove stale comment
2019-10-09 08:27:33 -03:00
Torsten Grote
9660ff2fff [core] delete conversation messages belonging to completed introduction sessions
A session is completed if it returned to the START state
and if all sent messages have been ACKed by the receiver.

The session's metadata is kept in case the user restarts the session
by doing another introduction.
2019-10-09 08:24:16 -03:00
akwizgran
ea810c817b Merge branch '1626-delete-all-messages-ui' into 'master'
Add conversation menu action to delete all messages

Closes #1626

See merge request briar/briar!1159
2019-10-07 16:56:44 +00:00
Torsten Grote
876d50975e [android] fix typo s/can not/cannot/ 2019-10-07 12:08:18 -03:00
akwizgran
bf5bdc52b4 Merge branch '1577-headless-readme-improvements' into 'master'
Clarify minor things in headless readme

Closes #1577

See merge request briar/briar!1157
2019-10-07 14:55:17 +00:00
akwizgran
29320c410e Merge branch '1625-conversation-client-message-deletion' into 'master'
Add ConversationManager method for deleting all messages

Closes #1625

See merge request briar/briar!1158
2019-10-07 14:44:41 +00:00
Nico Alt
d41472a18c Clarify minor things in headless readme
Based on answers received in #1577, I tried to clarify outstanding
questions I had about the Briar Headless API.

Fixes #1577.
2019-10-07 16:35:21 +02:00
akwizgran
c411065255 Merge branch '1582-pending-contacts-snackbar-fab' into 'master'
Use snackbar-aware behaviour for FAB.

Closes #1582

See merge request briar/briar!1156
2019-10-07 13:48:01 +00:00
Torsten Grote
3ac5646355 [briar-android] Add conversation menu action to delete all messages 2019-10-03 15:24:36 -03:00
Torsten Grote
c46fdce277 Add ConversationManager method for deleting all messages
Note that this does not yet delete special conversation messages
such as invitations or introductions and their responses.
2019-10-03 14:47:12 -03:00
akwizgran
643ef593e1 Use dodgeInsetEdges to make room for the snackbar. 2019-10-02 12:16:24 +01:00
akwizgran
eda17449be Merge branch '1582-pending-contacts-snackbar' into 'master'
Prevent pending contacts snackbar from covering contact list

See merge request briar/briar!1152
2019-10-01 12:18:11 +00:00
Torsten Grote
28f82a1507 Use snackbar-aware behaviour for FAB. 2019-10-01 13:01:44 +01:00
Torsten Grote
8734825346 [android] prevent pending contacts snackbar from covering contact list 2019-10-01 08:53:14 -03:00
akwizgran
640f3d63b0 Merge branch '1583-remote-contacts-small-screens' into 'master'
Make Remote Contact layouts work on small screens

Closes #1583

See merge request briar/briar!1155
2019-09-27 14:58:43 +00:00
akwizgran
b1dfd867f0 Bump version numbers for 1.1.9 release. 2019-07-03 12:16:52 +01:00
Torsten Grote
ff76900d74 Merge branch '1609-trimmed-text-length' into 'master'
Use trimmed length when deciding whether text is empty

Closes #1609

See merge request briar/briar!1153
2019-07-01 16:25:15 +00:00
Torsten Grote
945fdb8ee4 [android] Make Remote Contact layouts work on small screens 2019-07-01 17:56:28 +02:00
Torsten Grote
53fe3e1592 Merge branch '1428-android-debug-logging' into 'master'
Enable debug logging for debug and beta builds

Closes #1428

See merge request briar/briar!1154
2019-07-01 14:34:06 +00:00
akwizgran
be76c5b7db Add safety annotations. 2019-07-01 14:38:28 +01:00
akwizgran
909e946e58 Enable debug logging for debug and beta builds. 2019-07-01 14:34:51 +01:00
akwizgran
408d9ddee4 Rename directory for traditional Chinese translation. 2019-07-01 10:14:08 +01:00
akwizgran
0e5027e725 Update list of translations. 2019-07-01 01:35:52 +01:00
akwizgran
2d4c97a69e Update translations, add new translations. 2019-07-01 01:26:27 +01:00
akwizgran
7d62ae5fa8 Use trimmed length when deciding whether text is empty. 2019-07-01 01:13:24 +01:00
Torsten Grote
bd616853cf Merge branch '1607-upgrade-rome' into 'master'
Upgrade Rome to fix memory allocation bug

Closes #1607

See merge request briar/briar!1151
2019-06-28 14:17:46 +00:00
akwizgran
32e1d6c748 Upgrade Rome to fix memory allocation bug. 2019-06-28 15:09:09 +01:00
akwizgran
6b022afa67 Bump version numbers for 1.1.8 release. 2019-06-28 14:48:00 +01:00
akwizgran
e8b454b25b Update translations. 2019-06-28 14:47:03 +01:00
Torsten Grote
54c05b5ffe Merge branch '1606-bump-client-minor-version' into 'master'
Bump client minor version to avoid triggering crash

Closes #1606

See merge request briar/briar!1150
2019-06-28 13:28:37 +00:00
akwizgran
d145a082f5 Bump client minor version to avoid triggering crash. 2019-06-28 14:07:28 +01:00
akwizgran
4fd012c31a Merge branch 'compress-images' into 'master'
Compress images

See merge request briar/briar!1147
2019-06-26 14:21:24 +00:00
akwizgran
95d06770bf Rename 'scale' to 'inSampleSize' for clarity. 2019-06-26 14:36:40 +01:00
akwizgran
428247b7b2 Initialise result LiveData before starting task. 2019-06-26 14:31:40 +01:00
akwizgran
a921361a56 Inject ImageSizeCalculator. 2019-06-26 12:40:28 +01:00
akwizgran
fe7dfa721e Compress image attachments. 2019-06-25 16:55:09 +01:00
akwizgran
92eb06a9e9 Refactor attachment creation to use injection. 2019-06-25 16:29:54 +01:00
Torsten Grote
5beed1a748 Merge branch '1594-preview-fails-to-load' into 'master'
Use a fresh LiveData for each attachment creation task

Closes #1594

See merge request briar/briar!1144
2019-06-20 14:05:43 +00:00
Torsten Grote
774047d856 Merge branch '1585-check-attachment-content-type' into 'master'
Improve handling of missing attachments in UI

See merge request briar/briar!1142
2019-06-20 14:04:02 +00:00
Torsten Grote
fc28e7aa88 Merge branch 'nickname-nitpicks' into 'master'
Nickname nitpicks

See merge request briar/briar!1143
2019-06-20 13:41:25 +00:00
Torsten Grote
78459499b2 Merge branch '1593-qr-code-assertion-error' into 'master'
Keep enum methods used by ZXing

Closes #1593

See merge request briar/briar!1146
2019-06-19 23:45:49 +00:00
akwizgran
c2973608d7 Keep enum methods used by ZXing. 2019-06-19 16:36:39 +01:00
akwizgran
be1c33cb42 Use a fresh LiveData for each attachment creation task. 2019-06-19 13:43:04 +01:00
akwizgran
9ab9e02f8a Trim whitespace from nicknames (useful for auto-complete). 2019-06-18 17:24:08 +01:00
akwizgran
3f70ae3c8c Use same input type for nicknames everywhere. 2019-06-18 17:19:39 +01:00
167 changed files with 8849 additions and 1646 deletions

View File

@@ -1,16 +1,7 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="100" />
<AndroidXmlCodeStyleSettings>
<option name="USE_CUSTOM_SETTINGS" value="true" />
</AndroidXmlCodeStyleSettings>
<JavaCodeStyleSettings>
<option name="ANNOTATION_PARAMETER_WRAP" value="1" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value />
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="false" />
@@ -77,7 +68,6 @@
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
@@ -90,7 +80,8 @@
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_NAMESPACE>Namespace:</XML_NAMESPACE>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
@@ -100,7 +91,8 @@
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_NAMESPACE>Namespace:</XML_NAMESPACE>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
@@ -111,6 +103,7 @@
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@@ -121,6 +114,7 @@
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@@ -131,6 +125,7 @@
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -141,6 +136,7 @@
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -151,6 +147,7 @@
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -161,64 +158,12 @@
<rule>
<match>
<AND>
<NAME>.*:layout_width</NAME>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_height</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:width</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:height</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
@@ -226,6 +171,7 @@
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>

View File

@@ -9,10 +9,10 @@ android {
buildToolsVersion '28.0.3'
defaultConfig {
minSdkVersion 14
targetSdkVersion 26
versionCode 10107
versionName "1.1.7"
minSdkVersion 16
targetSdkVersion 28
versionCode 10204
versionName "1.2.4"
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@@ -30,8 +30,8 @@ configurations {
dependencies {
implementation project(path: ':bramble-core', configuration: 'default')
tor 'org.briarproject:tor-android:0.3.5.8@zip'
tor 'org.briarproject:obfs4proxy-android:0.0.9@zip'
tor 'org.briarproject:tor-android:0.3.5.8-64@zip'
tor 'org.briarproject:obfs4proxy-android:0.0.11-2@zip'
annotationProcessor 'com.google.dagger:dagger-compiler:2.22.1'
@@ -59,6 +59,8 @@ task unpackTorBinaries {
copy {
from configurations.tor.collect { zipTree(it) }
into torBinariesDir
// TODO: Remove after next Tor upgrade, which won't include non-PIE binaries
include 'geoip.zip', '*_pie.zip'
}
}
dependsOn cleanTorBinaries

View File

@@ -1,7 +1,6 @@
package org.briarproject.bramble.plugin.tor;
import android.content.Context;
import android.os.Build;
import org.briarproject.bramble.api.battery.BatteryManager;
import org.briarproject.bramble.api.event.EventBus;
@@ -89,9 +88,15 @@ public class AndroidTorPluginFactory implements DuplexPluginFactory {
// Check that we have a Tor binary for this architecture
String architecture = null;
for (String abi : AndroidUtils.getSupportedArchitectures()) {
if (abi.startsWith("x86")) {
if (abi.startsWith("x86_64")) {
architecture = "x86_64";
break;
} else if (abi.startsWith("x86")) {
architecture = "x86";
break;
} else if (abi.startsWith("arm64")) {
architecture = "arm64";
break;
} else if (abi.startsWith("armeabi")) {
architecture = "arm";
break;
@@ -101,8 +106,8 @@ public class AndroidTorPluginFactory implements DuplexPluginFactory {
LOG.info("Tor is not supported on this architecture");
return null;
}
// Use position-independent executable for SDK >= 16
if (Build.VERSION.SDK_INT >= 16) architecture += "_pie";
// Use position-independent executable
architecture += "_pie";
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE);

View File

@@ -23,6 +23,7 @@ import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static android.content.Context.WIFI_SERVICE;
import static android.os.Build.VERSION.SDK_INT;
import static android.provider.Settings.Secure.ANDROID_ID;
@Immutable
@@ -74,8 +75,7 @@ class AndroidSecureRandomProvider extends UnixSecureRandomProvider {
// Silence strict mode
StrictMode.ThreadPolicy tp = StrictMode.allowThreadDiskWrites();
super.writeSeed();
if (Build.VERSION.SDK_INT >= 16 && Build.VERSION.SDK_INT <= 18)
applyOpenSslFix();
if (SDK_INT <= 18) applyOpenSslFix();
StrictMode.setThreadPolicy(tp);
}

View File

@@ -66,8 +66,8 @@ dependencyVerification {
'org.beanshell:bsh:1.3.0:bsh-1.3.0.jar:9b04edc75d19db54f1b4e8b5355e9364384c6cf71eb0a1b9724c159d779879f8',
'org.bouncycastle:bcpkix-jdk15on:1.56:bcpkix-jdk15on-1.56.jar:7043dee4e9e7175e93e0b36f45b1ec1ecb893c5f755667e8b916eb8dd201c6ca',
'org.bouncycastle:bcprov-jdk15on:1.56:bcprov-jdk15on-1.56.jar:963e1ee14f808ffb99897d848ddcdb28fa91ddda867eb18d303e82728f878349',
'org.briarproject:obfs4proxy-android:0.0.9:obfs4proxy-android-0.0.9.zip:9b7e9181535ea8d8bbe8ae6338e08cf4c5fc1e357a779393e0ce49586d459ae0',
'org.briarproject:tor-android:0.3.5.8:tor-android-0.3.5.8.zip:42a13a6f185be1a62f42e3f30ce66a3c099ac5ec890a65e7593111b65b44a54a',
'org.briarproject:obfs4proxy-android:0.0.11-2:obfs4proxy-android-0.0.11-2.zip:57e55cbe87aa2aac210fdbb6cd8cdeafe15f825406a08ebf77a8b787aa2c6a8a',
'org.briarproject:tor-android:0.3.5.8-64:tor-android-0.3.5.8-64.zip:9f144088c0fe845d1cf3232cdc2b51c68e6f9a22660592009f43a5633fca8824',
'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d',
'org.checkerframework:checker-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a',
'org.codehaus.groovy:groovy-all:2.4.15:groovy-all-2.4.15.jar:51d6c4e71782e85674239189499854359d380fb75e1a703756e3aaa5b98a5af0',

View File

@@ -7,5 +7,5 @@ public interface FeatureFlags {
boolean shouldEnableImageAttachments();
boolean shouldEnableRemoteContacts();
boolean shouldEnablePrivateMessageDeletion();
}

View File

@@ -4,8 +4,10 @@ import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.UnsupportedVersionException;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.ContactExistsException;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.db.PendingContactExistsException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorId;
@@ -117,9 +119,14 @@ public interface ContactManager {
* @throws FormatException If the link is invalid
* @throws GeneralSecurityException If the pending contact's handshake
* public key is invalid
* @throws ContactExistsException If a contact with the same handshake
* public key already exists
* @throws PendingContactExistsException If a pending contact with the same
* handshake public key already exists
*/
PendingContact addPendingContact(String link, String alias)
throws DbException, FormatException, GeneralSecurityException;
throws DbException, FormatException, GeneralSecurityException,
ContactExistsException, PendingContactExistsException;
/**
* Returns the pending contact with the given ID.

View File

@@ -83,7 +83,7 @@ public interface DatabaseComponent extends TransactionManager {
/**
* Stores a pending contact.
*/
void addPendingContact(Transaction txn, PendingContact p)
void addPendingContact(Transaction txn, PendingContact p, AuthorId local)
throws DbException;
/**

View File

@@ -1,9 +1,21 @@
package org.briarproject.bramble.api.db;
import org.briarproject.bramble.api.contact.PendingContact;
/**
* Thrown when a duplicate pending contact is added to the database. This
* exception may occur due to concurrent updates and does not indicate a
* database error.
*/
public class PendingContactExistsException extends DbException {
private final PendingContact pendingContact;
public PendingContactExistsException(PendingContact pendingContact) {
this.pendingContact = pendingContact;
}
public PendingContact getPendingContact() {
return pendingContact;
}
}

View File

@@ -139,7 +139,8 @@ class ContactManagerImpl implements ContactManager, EventListener {
pendingContactFactory.createPendingContact(link, alias);
Transaction txn = db.startTransaction(false);
try {
db.addPendingContact(txn, p);
AuthorId local = identityManager.getLocalAuthor(txn).getId();
db.addPendingContact(txn, p, local);
KeyPair ourKeyPair = identityManager.getHandshakeKeys(txn);
keyManager.addPendingContact(txn, p.getId(), p.getPublicKey(),
ourKeyPair);

View File

@@ -267,6 +267,16 @@ interface Database<T> {
*/
Collection<ContactId> getContacts(T txn, AuthorId local) throws DbException;
/**
* Returns the contact with the given {@code handshakePublicKey}
* for the given local pseudonym or {@code null} if none exists.
* <p/>
* Read-only.
*/
@Nullable
Contact getContact(T txn, PublicKey handshakePublicKey, AuthorId local)
throws DbException;
/**
* Returns the group with the given ID.
* <p/>

View File

@@ -291,12 +291,17 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
}
@Override
public void addPendingContact(Transaction transaction, PendingContact p)
throws DbException {
public void addPendingContact(Transaction transaction, PendingContact p,
AuthorId local) throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (db.containsPendingContact(txn, p.getId()))
throw new PendingContactExistsException();
Contact contact = db.getContact(txn, p.getPublicKey(), local);
if (contact != null)
throw new ContactExistsException(local, contact.getAuthor());
if (db.containsPendingContact(txn, p.getId())) {
PendingContact existing = db.getPendingContact(txn, p.getId());
throw new PendingContactExistsException(existing);
}
db.addPendingContact(txn, p);
transaction.attach(new PendingContactAddedEvent(p));
}

View File

@@ -1465,6 +1465,47 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Nullable
@Override
public Contact getContact(Connection txn, PublicKey handshakePublicKey,
AuthorId localAuthorId) throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT contactId, authorId, formatVersion, name,"
+ " alias, publicKey, verified"
+ " FROM contacts"
+ " WHERE handshakePublicKey = ? AND localAuthorId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, handshakePublicKey.getEncoded());
ps.setBytes(2, localAuthorId.getBytes());
rs = ps.executeQuery();
if (!rs.next()) {
rs.close();
ps.close();
return null;
}
ContactId contactId = new ContactId(rs.getInt(1));
AuthorId authorId = new AuthorId(rs.getBytes(2));
int formatVersion = rs.getInt(3);
String name = rs.getString(4);
String alias = rs.getString(5);
PublicKey publicKey = new SignaturePublicKey(rs.getBytes(6));
boolean verified = rs.getBoolean(7);
if (rs.next()) throw new DbStateException();
rs.close();
ps.close();
Author author =
new Author(authorId, formatVersion, name, publicKey);
return new Contact(contactId, author, localAuthorId, alias,
handshakePublicKey, verified);
} catch (SQLException e) {
tryToClose(rs, LOG, WARNING);
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override
public Group getGroup(Connection txn, GroupId g) throws DbException {
PreparedStatement ps = null;

View File

@@ -195,8 +195,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
if (!assetsAreUpToDate()) installAssets();
if (cookieFile.exists() && !cookieFile.delete())
LOG.warning("Old auth cookie not deleted");
// Migrate old settings before having a chance to stop
migrateSettings();
// Start a new Tor process
LOG.info("Starting Tor");
String torPath = torFile.getAbsolutePath();
@@ -816,21 +814,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
controlConnection.setConf("ConnectionPadding", enable ? "1" : "0");
}
// TODO remove when sufficient time has passed. Added 2018-08-15
private void migrateSettings() {
Settings sOld = callback.getSettings();
int oldNetwork = sOld.getInt("network", -1);
if (oldNetwork == -1) return;
Settings s = new Settings();
if (oldNetwork == 0) {
s.putInt(PREF_TOR_NETWORK, PREF_TOR_NETWORK_NEVER);
} else if (oldNetwork == 1) {
s.putBoolean(PREF_TOR_MOBILE, false);
}
s.putInt("network", -1);
callback.mergeSettings(s);
}
private static class ConnectionStatus {
// All of the following are locking: this

View File

@@ -34,6 +34,7 @@ import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.SettableClock;
import org.briarproject.bramble.test.TestDatabaseConfig;
import org.briarproject.bramble.test.TestMessageFactory;
import org.briarproject.bramble.test.TestUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -1149,6 +1150,43 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.close();
}
@Test
public void testGetContactsByHandshakePublicKey() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add an identity for a local author - no contacts should be
// associated
db.addIdentity(txn, identity);
PublicKey handshakePublicKey = TestUtils.getSignaturePublicKey();
Contact contact =
db.getContact(txn, handshakePublicKey, localAuthor.getId());
assertNull(contact);
// Add a contact associated with the local author
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
handshakePublicKey, true));
contact = db.getContact(txn, handshakePublicKey, localAuthor.getId());
assertNotNull(contact);
assertEquals(contactId, contact.getId());
assertEquals(author, contact.getAuthor());
assertNull(contact.getAlias());
assertEquals(handshakePublicKey, contact.getHandshakePublicKey());
assertTrue(contact.isVerified());
assertEquals(author.getName(), contact.getAuthor().getName());
assertEquals(author.getPublicKey(), contact.getAuthor().getPublicKey());
assertEquals(author.getFormatVersion(),
contact.getAuthor().getFormatVersion());
// Ensure no contacts are returned after contact was deleted
db.removeContact(txn, contactId);
contact = db.getContact(txn, handshakePublicKey, localAuthor.getId());
assertNull(contact);
db.commitTransaction(txn);
db.close();
}
@Test
public void testOfferedMessages() throws Exception {
Database<Connection> db = open(false);

View File

@@ -26,7 +26,7 @@ public class BrambleCoreIntegrationTestModule {
}
@Override
public boolean shouldEnableRemoteContacts() {
public boolean shouldEnablePrivateMessageDeletion() {
return true;
}
};

View File

@@ -1,6 +1,6 @@
[main]
host = https://www.transifex.com
lang_map = pt_BR: pt-rBR, nb_NO: nb, zh-Hans: zh-rCN
lang_map = pt_BR: pt-rBR, nb_NO: nb, zh-Hans: zh-rCN, zh-Hant: zh-rTW
[briar.stringsxml-5]
file_filter = src/main/res/values-<lang>/strings.xml

View File

@@ -20,10 +20,10 @@ android {
buildToolsVersion '28.0.3'
defaultConfig {
minSdkVersion 15
targetSdkVersion 26
versionCode 10107
versionName "1.1.7"
minSdkVersion 16
targetSdkVersion 28
versionCode 10204
versionName "1.2.4"
applicationId "org.briarproject.briar.android"
buildConfigField "String", "GitHash",
"\"${getStdout(['git', 'rev-parse', '--short=7', 'HEAD'], 'No commit hash')}\""
@@ -117,7 +117,7 @@ dependencies {
implementation 'de.hdodenhof:circleimageview:2.2.0'
implementation 'com.google.zxing:core:3.3.3'
implementation 'uk.co.samuelwall:material-tap-target-prompt:2.14.0'
implementation 'com.vanniktech:emoji-google:0.5.1'
implementation 'com.vanniktech:emoji-google:0.6.0' // later versions already use androidx
implementation 'com.github.kobakei:MaterialFabSpeedDial:1.2.1' // later versions already use androidx
def glideVersion = '4.9.0'
implementation("com.github.bumptech.glide:glide:$glideVersion") {

View File

@@ -5,6 +5,10 @@
# QR codes
-keep class com.google.zxing.Result
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# RSS libraries
-keep,includedescriptorclasses class com.rometools.rome.feed.synd.impl.** { *; }

View File

@@ -4,6 +4,7 @@ import org.briarproject.bramble.BrambleAndroidModule;
import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.account.BriarAccountModule;
import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.navdrawer.NavDrawerActivityTest;
import javax.inject.Singleton;
@@ -13,6 +14,7 @@ import dagger.Component;
@Singleton
@Component(modules = {
AppModule.class,
AttachmentModule.class,
BriarCoreModule.class,
BrambleAndroidModule.class,
BriarAccountModule.class,

View File

@@ -47,8 +47,10 @@ public class AttachmentRetrieverIntegrationTest {
);
private final MessageId msgId = new MessageId(getRandomId());
private final ImageHelper imageHelper = new ImageHelperImpl();
private final AttachmentRetriever retriever =
new AttachmentRetriever(null, dimensions);
new AttachmentRetrieverImpl(null, dimensions, imageHelper,
new ImageSizeCalculator(imageHelper));
@Test
public void testSmallJpegImage() throws Exception {

View File

@@ -73,7 +73,7 @@
android:label="@string/crash_report_title"
android:launchMode="singleInstance"
android:theme="@style/BriarTheme.NoActionBar"
android:windowSoftInputMode="stateHidden">
android:windowSoftInputMode="adjustResize|stateHidden">
</activity>
<activity
@@ -89,7 +89,7 @@
<activity
android:name="org.briarproject.briar.android.account.SetupActivity"
android:label="@string/setup_title"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
</activity>
<activity
@@ -126,7 +126,7 @@
android:label="@string/app_name"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:theme="@style/BriarTheme.NoActionBar"
android:windowSoftInputMode="stateHidden|adjustResize">
android:windowSoftInputMode="adjustResize|stateUnchanged">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
@@ -145,7 +145,7 @@
android:name="org.briarproject.briar.android.privategroup.creation.CreateGroupActivity"
android:label="@string/groups_create_group_title"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
@@ -174,8 +174,7 @@
<activity
android:name="org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity"
android:label="@string/groups_member_list"
android:parentActivityName="org.briarproject.briar.android.privategroup.conversation.GroupActivity"
android:windowSoftInputMode="adjustResize|stateHidden">
android:parentActivityName="org.briarproject.briar.android.privategroup.conversation.GroupActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.privategroup.conversation.GroupActivity"/>
@@ -184,8 +183,7 @@
<activity
android:name="org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity"
android:label="@string/groups_reveal_contacts"
android:parentActivityName="org.briarproject.briar.android.privategroup.conversation.GroupActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden">
android:parentActivityName="org.briarproject.briar.android.privategroup.conversation.GroupActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.privategroup.conversation.GroupActivity"/>
@@ -223,7 +221,7 @@
android:name="org.briarproject.briar.android.forum.CreateForumActivity"
android:label="@string/create_forum_title"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
@@ -292,7 +290,7 @@
android:name="org.briarproject.briar.android.blog.WriteBlogPostActivity"
android:label="@string/blogs_write_blog_post"
android:parentActivityName="org.briarproject.briar.android.blog.BlogActivity"
android:windowSoftInputMode="stateVisible|adjustResize">
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.blog.BlogActivity"/>
@@ -302,7 +300,7 @@
android:name="org.briarproject.briar.android.blog.ReblogActivity"
android:label="@string/blogs_reblog_button"
android:parentActivityName="org.briarproject.briar.android.blog.BlogActivity"
android:windowSoftInputMode="stateHidden">
android:windowSoftInputMode="adjustResize|stateHidden">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.blog.BlogActivity"/>
@@ -312,7 +310,7 @@
android:name="org.briarproject.briar.android.blog.RssFeedImportActivity"
android:label="@string/blogs_rss_feeds_import"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:windowSoftInputMode="stateVisible|adjustResize">
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
@@ -341,7 +339,7 @@
android:name="org.briarproject.briar.android.introduction.IntroductionActivity"
android:label="@string/introduction_activity_title"
android:parentActivityName="org.briarproject.briar.android.conversation.ConversationActivity"
android:windowSoftInputMode="stateHidden|adjustResize">
android:windowSoftInputMode="adjustResize|stateHidden">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.conversation.ConversationActivity"/>
@@ -369,7 +367,8 @@
<activity
android:name="org.briarproject.briar.android.login.ChangePasswordActivity"
android:label="@string/change_password"
android:parentActivityName="org.briarproject.briar.android.settings.SettingsActivity">
android:parentActivityName="org.briarproject.briar.android.settings.SettingsActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.settings.SettingsActivity"/>
@@ -424,7 +423,7 @@
android:name=".android.contact.add.remote.AddContactActivity"
android:label="@string/add_contact_remotely_title_case"
android:theme="@style/BriarTheme"
android:windowSoftInputMode="stateHidden|adjustResize"/>
android:windowSoftInputMode="adjustResize|stateHidden"/>
<activity
android:name=".android.contact.add.remote.PendingContactListActivity"

View File

@@ -30,6 +30,7 @@ import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
import org.briarproject.briar.BriarCoreEagerSingletons;
import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
import org.briarproject.briar.android.login.SignInReminderReceiver;
import org.briarproject.briar.android.reporting.BriarReportSender;
@@ -68,7 +69,8 @@ import dagger.Component;
BriarCoreModule.class,
BrambleAndroidModule.class,
BriarAccountModule.class,
AppModule.class
AppModule.class,
AttachmentModule.class
})
public interface AndroidComponent
extends BrambleCoreEagerSingletons, BrambleAndroidEagerSingletons,

View File

@@ -347,8 +347,10 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
if (currentTime - lastSound > SOUND_DELAY) {
boolean sound = settings.getBoolean(PREF_NOTIFY_SOUND, true);
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
if (sound && !StringUtils.isNullOrEmpty(ringtoneUri))
b.setSound(Uri.parse(ringtoneUri));
if (sound && !StringUtils.isNullOrEmpty(ringtoneUri)) {
Uri uri = Uri.parse(ringtoneUri);
if (!"file".equals(uri.getScheme())) b.setSound(uri);
}
b.setDefaults(getDefaults());
lastSound = currentTime;
}
@@ -359,7 +361,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
int defaults = DEFAULT_LIGHTS;
boolean sound = settings.getBoolean(PREF_NOTIFY_SOUND, true);
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
if (sound && StringUtils.isNullOrEmpty(ringtoneUri))
if (sound && (StringUtils.isNullOrEmpty(ringtoneUri) ||
"file".equals(Uri.parse(ringtoneUri).getScheme())))
defaults |= DEFAULT_SOUND;
if (settings.getBoolean(PREF_NOTIFY_VIBRATION, true))
defaults |= DEFAULT_VIBRATE;

View File

@@ -243,7 +243,7 @@ public class AppModule {
}
@Override
public boolean shouldEnableRemoteContacts() {
public boolean shouldEnablePrivateMessageDeletion() {
return IS_DEBUG_BUILD;
}
};

View File

@@ -1,54 +0,0 @@
package org.briarproject.briar.android;
import android.app.Activity;
import android.app.Application.ActivityLifecycleCallbacks;
import android.os.Bundle;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
@ThreadSafe
@NotNullByDefault
class BackgroundMonitor implements ActivityLifecycleCallbacks {
private final AtomicInteger foregroundActivities = new AtomicInteger(0);
boolean isRunningInBackground() {
return foregroundActivities.get() == 0;
}
@Override
public void onActivityCreated(Activity a, @Nullable Bundle state) {
}
@Override
public void onActivityStarted(Activity a) {
foregroundActivities.incrementAndGet();
}
@Override
public void onActivityResumed(Activity a) {
}
@Override
public void onActivityPaused(Activity a) {
}
@Override
public void onActivityStopped(Activity a) {
foregroundActivities.decrementAndGet();
}
@Override
public void onActivitySaveInstanceState(Activity a,
@Nullable Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity a) {
}
}

View File

@@ -34,9 +34,9 @@ import java.util.logging.LogRecord;
import java.util.logging.Logger;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.acra.ReportField.ANDROID_VERSION;
import static org.acra.ReportField.APP_VERSION_CODE;
import static org.acra.ReportField.APP_VERSION_NAME;
@@ -82,10 +82,9 @@ public class BriarApplicationImpl extends Application
implements BriarApplication {
private static final Logger LOG =
Logger.getLogger(BriarApplicationImpl.class.getName());
getLogger(BriarApplicationImpl.class.getName());
private final CachingLogHandler logHandler = new CachingLogHandler();
private final BackgroundMonitor backgroundMonitor = new BackgroundMonitor();
private AndroidComponent applicationComponent;
private volatile SharedPreferences prefs;
@@ -108,12 +107,16 @@ public class BriarApplicationImpl extends Application
if (IS_DEBUG_BUILD) enableStrictMode();
Logger rootLogger = Logger.getLogger("");
if (!IS_DEBUG_BUILD && !IS_BETA_BUILD) {
// Remove default log handlers so system log is not used
for (Handler handler : rootLogger.getHandlers()) {
rootLogger.removeHandler(handler);
}
Logger rootLogger = getLogger("");
Handler[] handlers = rootLogger.getHandlers();
// Disable the Android logger for release builds
for (Handler handler : handlers) rootLogger.removeHandler(handler);
if (IS_DEBUG_BUILD || IS_BETA_BUILD) {
// We can't set the level of the Android logger at runtime, so
// raise records to the logger's default level
rootLogger.addHandler(new LevelRaisingHandler(FINE, INFO));
// Restore the default handlers after the level raising handler
for (Handler handler : handlers) rootLogger.addHandler(handler);
}
rootLogger.addHandler(logHandler);
rootLogger.setLevel(IS_DEBUG_BUILD || IS_BETA_BUILD ? FINE : INFO);
@@ -122,9 +125,6 @@ public class BriarApplicationImpl extends Application
applicationComponent = createApplicationComponent();
EmojiManager.install(new GoogleEmojiProvider());
if (SDK_INT < 16)
registerActivityLifecycleCallbacks(backgroundMonitor);
}
protected AndroidComponent createApplicationComponent() {
@@ -186,12 +186,8 @@ public class BriarApplicationImpl extends Application
@Override
public boolean isRunningInBackground() {
if (SDK_INT >= 16) {
RunningAppProcessInfo info = new RunningAppProcessInfo();
ActivityManager.getMyMemoryState(info);
return (info.importance != IMPORTANCE_FOREGROUND);
} else {
return backgroundMonitor.isRunningInBackground();
}
RunningAppProcessInfo info = new RunningAppProcessInfo();
ActivityManager.getMyMemoryState(info);
return (info.importance != IMPORTANCE_FOREGROUND);
}
}

View File

@@ -238,8 +238,6 @@ public class BriarService extends Service {
} else if (level == TRIM_MEMORY_RUNNING_LOW) {
LOG.info("Trim memory: running low");
} else if (level == TRIM_MEMORY_RUNNING_CRITICAL) {
// This level may be received if SDK_INT < 16, although the
// constant isn't declared until API level 16
LOG.warning("Trim memory: running critically low");
// If we're not in the foreground, clear the UI to save memory
if (app.isRunningInBackground()) hideUi();

View File

@@ -0,0 +1,42 @@
package org.briarproject.briar.android;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import javax.annotation.concurrent.Immutable;
/**
* Log handler that raises all records at or above a given source level to a
* given destination level. This affects the level seen by subsequent handlers.
*/
@Immutable
@NotNullByDefault
class LevelRaisingHandler extends Handler {
private final Level dest;
private final int srcInt, destInt;
LevelRaisingHandler(Level src, Level dest) {
this.dest = dest;
srcInt = src.intValue();
destInt = dest.intValue();
if (srcInt > destInt) throw new IllegalArgumentException();
}
@Override
public void publish(LogRecord record) {
int recordInt = record.getLevel().intValue();
if (recordInt >= srcInt && recordInt < destInt) record.setLevel(dest);
}
@Override
public void flush() {
}
@Override
public void close() {
}
}

View File

@@ -150,7 +150,7 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor, Service {
// Get permissions
String[] requestedPermissions = packageInfo.requestedPermissions;
if (requestedPermissions == null) return false;
if (SDK_INT >= 16 && SDK_INT < 23) {
if (SDK_INT < 23) {
// Check whether the permission has been requested and granted
int[] flags = packageInfo.requestedPermissionsFlags;
for (int i = 0; i < requestedPermissions.length; i++) {

View File

@@ -3,6 +3,7 @@ package org.briarproject.briar.android.account;
import android.os.Bundle;
import android.support.design.widget.TextInputEditText;
import android.support.design.widget.TextInputLayout;
import android.text.Editable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -10,18 +11,15 @@ import android.widget.Button;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import javax.annotation.Nullable;
import static android.view.inputmethod.EditorInfo.IME_ACTION_NEXT;
import static android.view.inputmethod.EditorInfo.IME_ACTION_NONE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.util.StringUtils.toUtf8;
import static org.briarproject.briar.android.util.UiUtils.setError;
import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -64,12 +62,6 @@ public class AuthorNameFragment extends SetupFragment {
return TAG;
}
@Override
public void onResume() {
super.onResume();
showSoftKeyboard(authorNameInput);
}
@Override
protected String getHelpText() {
return getString(R.string.setup_name_explanation);
@@ -77,20 +69,21 @@ public class AuthorNameFragment extends SetupFragment {
@Override
public void onTextChanged(CharSequence authorName, int i, int i1, int i2) {
int authorNameLength = StringUtils.toUtf8(authorName.toString()).length;
int authorNameLength = toUtf8(authorName.toString().trim()).length;
boolean error = authorNameLength > MAX_AUTHOR_NAME_LENGTH;
setError(authorNameWrapper, getString(R.string.name_too_long), error);
boolean enabled = authorNameLength > 0 && !error;
authorNameInput
.setImeOptions(enabled ? IME_ACTION_NEXT : IME_ACTION_NONE);
authorNameInput.setOnEditorActionListener(enabled ? this : null);
nextButton.setEnabled(enabled);
}
@Override
public void onClick(View view) {
setupController.setAuthorName(authorNameInput.getText().toString());
setupController.showPasswordFragment();
Editable text = authorNameInput.getText();
if (text != null) {
setupController.setAuthorName(text.toString().trim());
setupController.showPasswordFragment();
}
}
}

View File

@@ -61,7 +61,6 @@ public class SetPasswordFragment extends SetupFragment {
strengthMeter = v.findViewById(R.id.strength_meter);
passwordEntryWrapper = v.findViewById(R.id.password_entry_wrapper);
passwordEntry = v.findViewById(R.id.password_entry);
passwordEntry.requestFocus();
passwordConfirmationWrapper =
v.findViewById(R.id.password_confirm_wrapper);
passwordConfirmation = v.findViewById(R.id.password_confirm);

View File

@@ -2,7 +2,6 @@ package org.briarproject.briar.android.activity;
import android.content.Context;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.LayoutRes;
import android.support.annotation.UiThread;
import android.support.v4.app.Fragment;
@@ -12,7 +11,6 @@ import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.inputmethod.InputMethodManager;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
@@ -44,10 +42,10 @@ import javax.inject.Inject;
import static android.arch.lifecycle.Lifecycle.State.STARTED;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
/**
* Warning: Some activities don't extend {@link BaseActivity}.
@@ -217,17 +215,6 @@ public abstract class BaseActivity extends AppCompatActivity
});
}
public void showSoftKeyboard(View view) {
Object o = getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).showSoftInput(view, SHOW_IMPLICIT);
}
public void hideSoftKeyboard(View view) {
IBinder token = view.getWindowToken();
Object o = getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).hideSoftInputFromWindow(token, 0);
}
@UiThread
public void handleDbException(DbException e) {
supportFinishAfterTransition();

View File

@@ -1,6 +1,8 @@
package org.briarproject.briar.android.attachment;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory.Options;
import android.net.Uri;
import android.support.annotation.Nullable;
@@ -12,11 +14,17 @@ import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.jsoup.UnsupportedMimeTypeException;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.logging.Logger;
import static android.graphics.Bitmap.CompressFormat.JPEG;
import static android.graphics.BitmapFactory.decodeStream;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
@@ -24,6 +32,7 @@ import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.api.messaging.MessagingConstants.IMAGE_MIME_TYPES;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
@NotNullByDefault
class AttachmentCreationTask {
@@ -31,8 +40,11 @@ class AttachmentCreationTask {
private static Logger LOG =
getLogger(AttachmentCreationTask.class.getName());
private static final int MAX_ATTACHMENT_DIMENSION = 1000;
private final MessagingManager messagingManager;
private final ContentResolver contentResolver;
private final ImageSizeCalculator imageSizeCalculator;
private final GroupId groupId;
private final Collection<Uri> uris;
private final boolean needsSize;
@@ -43,24 +55,26 @@ class AttachmentCreationTask {
AttachmentCreationTask(MessagingManager messagingManager,
ContentResolver contentResolver,
AttachmentCreator attachmentCreator, GroupId groupId,
Collection<Uri> uris, boolean needsSize) {
AttachmentCreator attachmentCreator,
ImageSizeCalculator imageSizeCalculator,
GroupId groupId, Collection<Uri> uris, boolean needsSize) {
this.messagingManager = messagingManager;
this.contentResolver = contentResolver;
this.imageSizeCalculator = imageSizeCalculator;
this.groupId = groupId;
this.uris = uris;
this.needsSize = needsSize;
this.attachmentCreator = attachmentCreator;
}
public void cancel() {
void cancel() {
canceled = true;
attachmentCreator = null;
}
@IoExecutor
public void storeAttachments() {
for (Uri uri: uris) processUri(uri);
void storeAttachments() {
for (Uri uri : uris) processUri(uri);
AttachmentCreator attachmentCreator = this.attachmentCreator;
if (!canceled && attachmentCreator != null)
attachmentCreator.onAttachmentCreationFinished();
@@ -98,6 +112,8 @@ class AttachmentCreationTask {
}
InputStream is = contentResolver.openInputStream(uri);
if (is == null) throw new IOException();
is = compressImage(is, contentType);
contentType = "image/jpeg";
long timestamp = System.currentTimeMillis();
AttachmentHeader h = messagingManager
.addLocalAttachment(groupId, timestamp, contentType, is);
@@ -113,4 +129,48 @@ class AttachmentCreationTask {
return false;
}
private InputStream compressImage(InputStream is, String contentType)
throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
Bitmap bitmap = createBitmap(is, contentType);
for (int quality = 100; quality >= 0; quality -= 10) {
if (!bitmap.compress(JPEG, quality, out))
throw new IOException();
if (out.size() <= MAX_IMAGE_SIZE) {
if (LOG.isLoggable(INFO)) {
LOG.info("Compressed image to "
+ out.size() + " bytes, quality " + quality);
}
return new ByteArrayInputStream(out.toByteArray());
}
out.reset();
}
throw new IOException();
} finally {
tryToClose(is, LOG, WARNING);
}
}
private Bitmap createBitmap(InputStream is, String contentType)
throws IOException {
is = new BufferedInputStream(is);
Size size = imageSizeCalculator.getSize(is, contentType);
if (size.error) throw new IOException();
if (LOG.isLoggable(INFO))
LOG.info("Original image size: " + size.width + "x" + size.height);
int dimension = Math.max(size.width, size.height);
int inSampleSize = 1;
while (dimension > MAX_ATTACHMENT_DIMENSION) {
inSampleSize *= 2;
dimension /= 2;
}
if (LOG.isLoggable(INFO))
LOG.info("Scaling attachment by factor of " + inSampleSize);
Options options = new Options();
options.inSampleSize = inSampleSize;
Bitmap bitmap = decodeStream(is, null, options);
if (bitmap == null) throw new IOException();
return bitmap;
}
}

View File

@@ -1,82 +1,24 @@
package org.briarproject.briar.android.attachment;
import android.app.Application;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.FileTooBigException;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.jsoup.UnsupportedMimeTypeException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
@NotNullByDefault
public class AttachmentCreator {
private static Logger LOG = getLogger(AttachmentCreator.class.getName());
private final Application app;
@IoExecutor
private final Executor ioExecutor;
private final MessagingManager messagingManager;
private final AttachmentRetriever retriever;
private final CopyOnWriteArrayList<Uri> uris = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<AttachmentItemResult> itemResults =
new CopyOnWriteArrayList<>();
private final MutableLiveData<AttachmentResult> result =
new MutableLiveData<>();
@Nullable
private AttachmentCreationTask task;
public AttachmentCreator(Application app, @IoExecutor Executor ioExecutor,
MessagingManager messagingManager, AttachmentRetriever retriever) {
this.app = app;
this.ioExecutor = ioExecutor;
this.messagingManager = messagingManager;
this.retriever = retriever;
}
public interface AttachmentCreator {
@UiThread
public LiveData<AttachmentResult> storeAttachments(
LiveData<GroupId> groupId, Collection<Uri> newUris) {
if (task != null || !uris.isEmpty())
throw new IllegalStateException();
uris.addAll(newUris);
observeForeverOnce(groupId, id -> {
if (id == null) throw new IllegalStateException();
boolean needsSize = uris.size() == 1;
task = new AttachmentCreationTask(messagingManager,
app.getContentResolver(), this, id, uris, needsSize);
ioExecutor.execute(() -> task.storeAttachments());
});
return result;
}
LiveData<AttachmentResult> storeAttachments(LiveData<GroupId> groupId,
Collection<Uri> newUris);
/**
* This should be only called after configuration changes.
@@ -84,68 +26,10 @@ public class AttachmentCreator {
* They are already being created and returned by the existing LiveData.
*/
@UiThread
public LiveData<AttachmentResult> getLiveAttachments() {
if (task == null || uris.isEmpty())
throw new IllegalStateException();
// A task is already running. It will update the result LiveData.
// So nothing more to do here.
return result;
}
@IoExecutor
void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h,
boolean needsSize) {
// get and cache AttachmentItem for ImagePreview
try {
Attachment a = retriever.getMessageAttachment(h);
AttachmentItem item = retriever.getAttachmentItem(a, needsSize);
if (item.hasError()) throw new IOException();
AttachmentItemResult itemResult =
new AttachmentItemResult(uri, item);
itemResults.add(itemResult);
result.postValue(getResult(false));
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
onAttachmentError(uri, e);
}
}
@IoExecutor
void onAttachmentError(Uri uri, Throwable t) {
// get error message
String errorMsg;
if (t instanceof UnsupportedMimeTypeException) {
String mimeType = ((UnsupportedMimeTypeException) t).getMimeType();
errorMsg = app.getString(
R.string.image_attach_error_invalid_mime_type, mimeType);
} else if (t instanceof FileTooBigException) {
int mb = MAX_IMAGE_SIZE / 1024 / 1024;
errorMsg = app.getString(R.string.image_attach_error_too_big, mb);
} else {
errorMsg = null; // generic error
}
AttachmentItemResult itemResult =
new AttachmentItemResult(uri, errorMsg);
itemResults.add(itemResult);
result.postValue(getResult(false));
// expect to receive a cancel from the UI
}
@IoExecutor
void onAttachmentCreationFinished() {
result.postValue(getResult(true));
}
LiveData<AttachmentResult> getLiveAttachments();
@UiThread
public List<AttachmentHeader> getAttachmentHeadersForSending() {
List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
headers.add(itemResult.getItem().getHeader());
}
return headers;
}
List<AttachmentHeader> getAttachmentHeadersForSending();
/**
* Marks the attachments as sent and adds the items to the cache for display
@@ -153,66 +37,24 @@ public class AttachmentCreator {
* @param id The MessageId of the sent message.
*/
@UiThread
public void onAttachmentsSent(MessageId id) {
List<AttachmentItem> items = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
items.add(itemResult.getItem());
}
retriever.cachePut(id, items);
resetState();
}
void onAttachmentsSent(MessageId id);
/**
* Needs to be called when created attachments will not be sent anymore.
*/
@UiThread
public void cancel() {
if (task == null) throw new AssertionError();
task.cancel();
deleteUnsentAttachments();
resetState();
}
void cancel();
@UiThread
private void resetState() {
task = null;
uris.clear();
itemResults.clear();
result.setValue(null);
}
void deleteUnsentAttachments();
@UiThread
public void deleteUnsentAttachments() {
// Make a copy for the IoExecutor as we clear the itemResults soon
List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() != null)
headers.add(itemResult.getItem().getHeader());
}
ioExecutor.execute(() -> {
for (AttachmentHeader header : headers) {
try {
messagingManager.removeAttachment(header);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
});
}
@IoExecutor
void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h,
boolean needsSize);
private AttachmentResult getResult(boolean finished) {
// Make a copy of the list,
// because our copy will continue to change in the background.
// (As it's a CopyOnWriteArrayList,
// the code that receives the result can safely do simple things
// like iterating over the list,
// but anything that involves calling more than one list method
// is still unsafe.)
Collection<AttachmentItemResult> items = new ArrayList<>(itemResults);
return new AttachmentResult(items, finished);
}
@IoExecutor
void onAttachmentError(Uri uri, Throwable t);
}
@IoExecutor
void onAttachmentCreationFinished();
}

View File

@@ -0,0 +1,233 @@
package org.briarproject.briar.android.attachment;
import android.app.Application;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.FileTooBigException;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.jsoup.UnsupportedMimeTypeException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
@NotNullByDefault
class AttachmentCreatorImpl implements AttachmentCreator {
private static Logger LOG =
getLogger(AttachmentCreatorImpl.class.getName());
private final Application app;
@IoExecutor
private final Executor ioExecutor;
private final MessagingManager messagingManager;
private final AttachmentRetriever retriever;
private final ImageSizeCalculator imageSizeCalculator;
private final CopyOnWriteArrayList<Uri> uris = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<AttachmentItemResult> itemResults =
new CopyOnWriteArrayList<>();
@Nullable
private AttachmentCreationTask task;
@Nullable
private volatile MutableLiveData<AttachmentResult> result;
@Inject
AttachmentCreatorImpl(Application app, @IoExecutor Executor ioExecutor,
MessagingManager messagingManager, AttachmentRetriever retriever,
ImageSizeCalculator imageSizeCalculator) {
this.app = app;
this.ioExecutor = ioExecutor;
this.messagingManager = messagingManager;
this.retriever = retriever;
this.imageSizeCalculator = imageSizeCalculator;
}
@Override
@UiThread
public LiveData<AttachmentResult> storeAttachments(
LiveData<GroupId> groupId, Collection<Uri> newUris) {
if (task != null || result != null || !uris.isEmpty())
throw new IllegalStateException();
MutableLiveData<AttachmentResult> result = new MutableLiveData<>();
this.result = result;
uris.addAll(newUris);
observeForeverOnce(groupId, id -> {
if (id == null) throw new IllegalStateException();
boolean needsSize = uris.size() == 1;
task = new AttachmentCreationTask(messagingManager,
app.getContentResolver(), this, imageSizeCalculator, id,
uris, needsSize);
ioExecutor.execute(() -> task.storeAttachments());
});
return result;
}
@Override
@UiThread
public LiveData<AttachmentResult> getLiveAttachments() {
MutableLiveData<AttachmentResult> result = this.result;
if (task == null || result == null || uris.isEmpty())
throw new IllegalStateException();
// A task is already running. It will update the result LiveData.
// So nothing more to do here.
return result;
}
@Override
@IoExecutor
public void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h,
boolean needsSize) {
// get and cache AttachmentItem for ImagePreview
try {
Attachment a = retriever.getMessageAttachment(h);
AttachmentItem item = retriever.getAttachmentItem(a, needsSize);
if (item.hasError()) throw new IOException();
AttachmentItemResult itemResult =
new AttachmentItemResult(uri, item);
itemResults.add(itemResult);
MutableLiveData<AttachmentResult> result = this.result;
if (result != null) result.postValue(getResult(false));
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
onAttachmentError(uri, e);
}
}
@Override
@IoExecutor
public void onAttachmentError(Uri uri, Throwable t) {
// get error message
String errorMsg;
if (t instanceof UnsupportedMimeTypeException) {
String mimeType = ((UnsupportedMimeTypeException) t).getMimeType();
errorMsg = app.getString(
R.string.image_attach_error_invalid_mime_type, mimeType);
} else if (t instanceof FileTooBigException) {
int mb = MAX_IMAGE_SIZE / 1024 / 1024;
errorMsg = app.getString(R.string.image_attach_error_too_big, mb);
} else {
errorMsg = null; // generic error
}
AttachmentItemResult itemResult =
new AttachmentItemResult(uri, errorMsg);
itemResults.add(itemResult);
MutableLiveData<AttachmentResult> result = this.result;
if (result != null) result.postValue(getResult(false));
// expect to receive a cancel from the UI
}
@Override
@IoExecutor
public void onAttachmentCreationFinished() {
MutableLiveData<AttachmentResult> result = this.result;
if (result != null) result.postValue(getResult(true));
}
@Override
@UiThread
public List<AttachmentHeader> getAttachmentHeadersForSending() {
List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
headers.add(itemResult.getItem().getHeader());
}
return headers;
}
@Override
@UiThread
public void onAttachmentsSent(MessageId id) {
List<AttachmentItem> items = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
items.add(itemResult.getItem());
}
retriever.cachePut(id, items);
resetState();
}
@Override
@UiThread
public void cancel() {
if (task == null) throw new AssertionError();
task.cancel();
deleteUnsentAttachments();
resetState();
}
@UiThread
private void resetState() {
task = null;
uris.clear();
itemResults.clear();
MutableLiveData<AttachmentResult> result = this.result;
if (result != null) {
result.setValue(null);
this.result = null;
}
}
@Override
@UiThread
public void deleteUnsentAttachments() {
// Make a copy for the IoExecutor as we clear the itemResults soon
List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() != null)
headers.add(itemResult.getItem().getHeader());
}
ioExecutor.execute(() -> {
for (AttachmentHeader header : headers) {
try {
messagingManager.removeAttachment(header);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
});
}
private AttachmentResult getResult(boolean finished) {
// Make a copy of the list,
// because our copy will continue to change in the background.
// (As it's a CopyOnWriteArrayList,
// the code that receives the result can safely do simple things
// like iterating over the list,
// but anything that involves calling more than one list method
// is still unsafe.)
Collection<AttachmentItemResult> items = new ArrayList<>(itemResults);
return new AttachmentResult(items, finished);
}
}

View File

@@ -10,7 +10,7 @@ import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class AttachmentDimensions {
class AttachmentDimensions {
final int defaultSize;
final int minWidth, maxWidth;
@@ -26,7 +26,7 @@ public class AttachmentDimensions {
this.maxHeight = maxHeight;
}
public static AttachmentDimensions getAttachmentDimensions(Resources res) {
static AttachmentDimensions getAttachmentDimensions(Resources res) {
int defaultSize =
res.getDimensionPixelSize(R.dimen.message_bubble_image_default);
int minWidth = res.getDimensionPixelSize(

View File

@@ -4,12 +4,14 @@ import android.arch.lifecycle.LiveData;
import android.net.Uri;
import android.support.annotation.UiThread;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.Collection;
import java.util.List;
@UiThread
@NotNullByDefault
public interface AttachmentManager {
LiveData<AttachmentResult> storeAttachments(Collection<Uri> uri,

View File

@@ -0,0 +1,43 @@
package org.briarproject.briar.android.attachment;
import android.app.Application;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import static org.briarproject.briar.android.attachment.AttachmentDimensions.getAttachmentDimensions;
@Module
public class AttachmentModule {
@Provides
ImageHelper provideImageHelper(ImageHelperImpl imageHelper) {
return imageHelper;
}
@Provides
ImageSizeCalculator provideImageSizeCalculator(ImageHelper imageHelper) {
return new ImageSizeCalculator(imageHelper);
}
@Provides
AttachmentDimensions provideAttachmentDimensions(Application app) {
return getAttachmentDimensions(app.getResources());
}
@Provides
@Singleton
AttachmentRetriever provideAttachmentRetriever(
AttachmentRetrieverImpl attachmentRetriever) {
return attachmentRetriever;
}
@Provides
@Singleton
AttachmentCreator provideAttachmentCreator(
AttachmentCreatorImpl attachmentCreator) {
return attachmentCreator;
}
}

View File

@@ -1,241 +1,29 @@
package org.briarproject.briar.android.attachment;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.media.ExifInterface;
import android.webkit.MimeTypeMap;
import com.bumptech.glide.util.MarkEnforcingInputStream;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.attachment.ImageHelper.DecodeResult;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import static android.support.media.ExifInterface.ORIENTATION_ROTATE_270;
import static android.support.media.ExifInterface.ORIENTATION_ROTATE_90;
import static android.support.media.ExifInterface.ORIENTATION_TRANSPOSE;
import static android.support.media.ExifInterface.ORIENTATION_TRANSVERSE;
import static android.support.media.ExifInterface.TAG_IMAGE_LENGTH;
import static android.support.media.ExifInterface.TAG_IMAGE_WIDTH;
import static android.support.media.ExifInterface.TAG_ORIENTATION;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
public class AttachmentRetriever {
public interface AttachmentRetriever {
private static final Logger LOG =
getLogger(AttachmentRetriever.class.getName());
private static final int READ_LIMIT = 1024 * 8192;
private final MessagingManager messagingManager;
private final ImageHelper imageHelper;
private final int defaultSize;
private final int minWidth, maxWidth;
private final int minHeight, maxHeight;
private final Map<MessageId, List<AttachmentItem>> attachmentCache =
new ConcurrentHashMap<>();
@VisibleForTesting
AttachmentRetriever(MessagingManager messagingManager,
AttachmentDimensions dimensions, ImageHelper imageHelper) {
this.messagingManager = messagingManager;
this.imageHelper = imageHelper;
defaultSize = dimensions.defaultSize;
minWidth = dimensions.minWidth;
maxWidth = dimensions.maxWidth;
minHeight = dimensions.minHeight;
maxHeight = dimensions.maxHeight;
}
public AttachmentRetriever(MessagingManager messagingManager,
AttachmentDimensions dimensions) {
this(messagingManager, dimensions, new ImageHelper() {
@Override
public DecodeResult decodeStream(InputStream is) {
Options options = new Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
String mimeType = options.outMimeType;
if (mimeType == null) mimeType = "";
return new DecodeResult(options.outWidth, options.outHeight,
mimeType);
}
@Nullable
@Override
public String getExtensionFromMimeType(String mimeType) {
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
return mimeTypeMap.getExtensionFromMimeType(mimeType);
}
});
}
public void cachePut(MessageId messageId,
List<AttachmentItem> attachments) {
attachmentCache.put(messageId, attachments);
}
void cachePut(MessageId messageId, List<AttachmentItem> attachments);
@Nullable
public List<AttachmentItem> cacheGet(MessageId messageId) {
return attachmentCache.get(messageId);
}
List<AttachmentItem> cacheGet(MessageId messageId);
public Attachment getMessageAttachment(AttachmentHeader h)
throws DbException {
return messagingManager.getAttachment(h);
}
Attachment getMessageAttachment(AttachmentHeader h) throws DbException;
/**
* Creates an {@link AttachmentItem} from the {@link Attachment}'s
* {@link InputStream} which will be closed when this method returns.
*/
public AttachmentItem getAttachmentItem(Attachment a, boolean needsSize) {
AttachmentHeader h = a.getHeader();
if (!needsSize) {
String extension =
imageHelper.getExtensionFromMimeType(h.getContentType());
boolean hasError = false;
if (extension == null) {
extension = "";
hasError = true;
}
return new AttachmentItem(h, 0, 0, extension, 0, 0, hasError);
}
Size size = new Size();
InputStream is = new MarkEnforcingInputStream(
new BufferedInputStream(a.getStream()));
is.mark(READ_LIMIT);
try {
// use exif to get size
if (h.getContentType().equals("image/jpeg")) {
size = getSizeFromExif(is);
}
} catch (IOException e) {
logException(LOG, WARNING, e);
}
try {
// use BitmapFactory to get size
if (size.error) {
is.reset();
// need to mark again to re-add read limit
is.mark(READ_LIMIT);
size = getSizeFromBitmap(is);
}
} catch (IOException e) {
logException(LOG, WARNING, e);
} finally {
tryToClose(is, LOG, WARNING);
}
// calculate thumbnail size
Size thumbnailSize = new Size(defaultSize, defaultSize, size.mimeType);
if (!size.error) {
thumbnailSize =
getThumbnailSize(size.width, size.height, size.mimeType);
}
// get file extension
String extension = imageHelper.getExtensionFromMimeType(size.mimeType);
boolean hasError = extension == null || size.error;
if (!h.getContentType().equals(size.mimeType)) {
if (LOG.isLoggable(WARNING)) {
LOG.warning("Header has different mime type (" +
h.getContentType() + ") than image (" + size.mimeType +
").");
}
hasError = true;
}
if (extension == null) extension = "";
return new AttachmentItem(h, size.width, size.height, extension,
thumbnailSize.width, thumbnailSize.height, hasError);
}
/**
* Gets the size of a JPEG {@link InputStream} if EXIF info is available.
*/
private Size getSizeFromExif(InputStream is) throws IOException {
ExifInterface exif = new ExifInterface(is);
// these can return 0 independent of default value
int width = exif.getAttributeInt(TAG_IMAGE_WIDTH, 0);
int height = exif.getAttributeInt(TAG_IMAGE_LENGTH, 0);
if (width == 0 || height == 0) return new Size();
int orientation = exif.getAttributeInt(TAG_ORIENTATION, 0);
if (orientation == ORIENTATION_ROTATE_90 ||
orientation == ORIENTATION_ROTATE_270 ||
orientation == ORIENTATION_TRANSVERSE ||
orientation == ORIENTATION_TRANSPOSE) {
//noinspection SuspiciousNameCombination
return new Size(height, width, "image/jpeg");
}
return new Size(width, height, "image/jpeg");
}
/**
* Gets the size of any image {@link InputStream}.
*/
private Size getSizeFromBitmap(InputStream is) {
DecodeResult result = imageHelper.decodeStream(is);
if (result.width < 1 || result.height < 1) return new Size();
return new Size(result.width, result.height, result.mimeType);
}
private Size getThumbnailSize(int width, int height, String mimeType) {
float widthPercentage = maxWidth / (float) width;
float heightPercentage = maxHeight / (float) height;
float scaleFactor = Math.min(widthPercentage, heightPercentage);
if (scaleFactor > 1) scaleFactor = 1f;
int thumbnailWidth = (int) (width * scaleFactor);
int thumbnailHeight = (int) (height * scaleFactor);
if (thumbnailWidth < minWidth || thumbnailHeight < minHeight) {
widthPercentage = minWidth / (float) width;
heightPercentage = minHeight / (float) height;
scaleFactor = Math.max(widthPercentage, heightPercentage);
thumbnailWidth = (int) (width * scaleFactor);
thumbnailHeight = (int) (height * scaleFactor);
if (thumbnailWidth > maxWidth) thumbnailWidth = maxWidth;
if (thumbnailHeight > maxHeight) thumbnailHeight = maxHeight;
}
return new Size(thumbnailWidth, thumbnailHeight, mimeType);
}
private static class Size {
private final int width;
private final int height;
private final String mimeType;
private final boolean error;
private Size(int width, int height, String mimeType) {
this.width = width;
this.height = height;
this.mimeType = mimeType;
this.error = false;
}
private Size() {
this.width = 0;
this.height = 0;
this.mimeType = "";
this.error = true;
}
}
AttachmentItem getAttachmentItem(Attachment a, boolean needsSize);
}

View File

@@ -0,0 +1,129 @@
package org.briarproject.briar.android.attachment;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
@NotNullByDefault
class AttachmentRetrieverImpl implements AttachmentRetriever {
private static final Logger LOG =
getLogger(AttachmentRetrieverImpl.class.getName());
private final MessagingManager messagingManager;
private final ImageHelper imageHelper;
private final ImageSizeCalculator imageSizeCalculator;
private final int defaultSize;
private final int minWidth, maxWidth;
private final int minHeight, maxHeight;
private final Map<MessageId, List<AttachmentItem>> attachmentCache =
new ConcurrentHashMap<>();
@Inject
AttachmentRetrieverImpl(MessagingManager messagingManager,
AttachmentDimensions dimensions, ImageHelper imageHelper,
ImageSizeCalculator imageSizeCalculator) {
this.messagingManager = messagingManager;
this.imageHelper = imageHelper;
this.imageSizeCalculator = imageSizeCalculator;
defaultSize = dimensions.defaultSize;
minWidth = dimensions.minWidth;
maxWidth = dimensions.maxWidth;
minHeight = dimensions.minHeight;
maxHeight = dimensions.maxHeight;
}
@Override
public void cachePut(MessageId messageId,
List<AttachmentItem> attachments) {
attachmentCache.put(messageId, attachments);
}
@Override
@Nullable
public List<AttachmentItem> cacheGet(MessageId messageId) {
return attachmentCache.get(messageId);
}
@Override
public Attachment getMessageAttachment(AttachmentHeader h)
throws DbException {
return messagingManager.getAttachment(h);
}
@Override
public AttachmentItem getAttachmentItem(Attachment a, boolean needsSize) {
AttachmentHeader h = a.getHeader();
if (!needsSize) {
String extension =
imageHelper.getExtensionFromMimeType(h.getContentType());
boolean hasError = false;
if (extension == null) {
extension = "";
hasError = true;
}
return new AttachmentItem(h, 0, 0, extension, 0, 0, hasError);
}
InputStream is = new BufferedInputStream(a.getStream());
Size size = imageSizeCalculator.getSize(is, h.getContentType());
// calculate thumbnail size
Size thumbnailSize = new Size(defaultSize, defaultSize, size.mimeType);
if (!size.error) {
thumbnailSize =
getThumbnailSize(size.width, size.height, size.mimeType);
}
// get file extension
String extension = imageHelper.getExtensionFromMimeType(size.mimeType);
boolean hasError = extension == null || size.error;
if (!h.getContentType().equals(size.mimeType)) {
if (LOG.isLoggable(WARNING)) {
LOG.warning("Header has different mime type (" +
h.getContentType() + ") than image (" + size.mimeType +
").");
}
hasError = true;
}
if (extension == null) extension = "";
return new AttachmentItem(h, size.width, size.height, extension,
thumbnailSize.width, thumbnailSize.height, hasError);
}
private Size getThumbnailSize(int width, int height, String mimeType) {
float widthPercentage = maxWidth / (float) width;
float heightPercentage = maxHeight / (float) height;
float scaleFactor = Math.min(widthPercentage, heightPercentage);
if (scaleFactor > 1) scaleFactor = 1f;
int thumbnailWidth = (int) (width * scaleFactor);
int thumbnailHeight = (int) (height * scaleFactor);
if (thumbnailWidth < minWidth || thumbnailHeight < minHeight) {
widthPercentage = minWidth / (float) width;
heightPercentage = minHeight / (float) height;
scaleFactor = Math.max(widthPercentage, heightPercentage);
thumbnailWidth = (int) (width * scaleFactor);
thumbnailHeight = (int) (height * scaleFactor);
if (thumbnailWidth > maxWidth) thumbnailWidth = maxWidth;
if (thumbnailHeight > maxHeight) thumbnailHeight = maxHeight;
}
return new Size(thumbnailWidth, thumbnailHeight, mimeType);
}
}

View File

@@ -7,7 +7,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.InputStream;
@NotNullByDefault
interface ImageHelper {
public interface ImageHelper {
DecodeResult decodeStream(InputStream is);

View File

@@ -0,0 +1,39 @@
package org.briarproject.briar.android.attachment;
import android.graphics.BitmapFactory;
import android.support.annotation.Nullable;
import android.webkit.MimeTypeMap;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.InputStream;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
@Immutable
@NotNullByDefault
class ImageHelperImpl implements ImageHelper {
@Inject
ImageHelperImpl() {
}
@Override
public DecodeResult decodeStream(InputStream is) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
String mimeType = options.outMimeType;
if (mimeType == null) mimeType = "";
return new DecodeResult(options.outWidth, options.outHeight,
mimeType);
}
@Nullable
@Override
public String getExtensionFromMimeType(String mimeType) {
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
return mimeTypeMap.getExtensionFromMimeType(mimeType);
}
}

View File

@@ -0,0 +1,94 @@
package org.briarproject.briar.android.attachment;
import android.support.media.ExifInterface;
import com.bumptech.glide.util.MarkEnforcingInputStream;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.attachment.ImageHelper.DecodeResult;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Logger;
import static android.support.media.ExifInterface.ORIENTATION_ROTATE_270;
import static android.support.media.ExifInterface.ORIENTATION_ROTATE_90;
import static android.support.media.ExifInterface.ORIENTATION_TRANSPOSE;
import static android.support.media.ExifInterface.ORIENTATION_TRANSVERSE;
import static android.support.media.ExifInterface.TAG_IMAGE_LENGTH;
import static android.support.media.ExifInterface.TAG_IMAGE_WIDTH;
import static android.support.media.ExifInterface.TAG_ORIENTATION;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class ImageSizeCalculator {
private static final Logger LOG =
getLogger(ImageSizeCalculator.class.getName());
private static final int READ_LIMIT = 1024 * 8192;
private final ImageHelper imageHelper;
ImageSizeCalculator(ImageHelper imageHelper) {
this.imageHelper = imageHelper;
}
Size getSize(InputStream is, String contentType) {
Size size = new Size();
is = new MarkEnforcingInputStream(is);
is.mark(READ_LIMIT);
if (contentType.equals("image/jpeg")) {
try {
// use exif to get size
size = getSizeFromExif(is);
is.reset();
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
if (size.error) {
// need to mark again to re-add read limit
is.mark(READ_LIMIT);
try {
// use BitmapFactory to get size
size = getSizeFromBitmap(is);
is.reset();
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
return size;
}
/**
* Gets the size of a JPEG {@link InputStream} if EXIF info is available.
*/
private Size getSizeFromExif(InputStream is) throws IOException {
ExifInterface exif = new ExifInterface(is);
// these can return 0 independent of default value
int width = exif.getAttributeInt(TAG_IMAGE_WIDTH, 0);
int height = exif.getAttributeInt(TAG_IMAGE_LENGTH, 0);
if (width == 0 || height == 0) return new Size();
int orientation = exif.getAttributeInt(TAG_ORIENTATION, 0);
if (orientation == ORIENTATION_ROTATE_90 ||
orientation == ORIENTATION_ROTATE_270 ||
orientation == ORIENTATION_TRANSVERSE ||
orientation == ORIENTATION_TRANSPOSE) {
//noinspection SuspiciousNameCombination
return new Size(height, width, "image/jpeg");
}
return new Size(width, height, "image/jpeg");
}
/**
* Gets the size of any image {@link InputStream}.
*/
private Size getSizeFromBitmap(InputStream is) {
DecodeResult result = imageHelper.decodeStream(is);
if (result.width < 1 || result.height < 1) return new Size();
return new Size(result.width, result.height, result.mimeType);
}
}

View File

@@ -0,0 +1,23 @@
package org.briarproject.briar.android.attachment;
class Size {
final int width;
final int height;
final String mimeType;
final boolean error;
Size(int width, int height, String mimeType) {
this.width = width;
this.height = height;
this.mimeType = mimeType;
this.error = false;
}
Size() {
this.width = 0;
this.height = 0;
this.mimeType = "";
this.error = true;
}
}

View File

@@ -32,6 +32,7 @@ import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
public class RssFeedImportActivity extends BriarActivity {
@@ -77,7 +78,6 @@ public class RssFeedImportActivity extends BriarActivity {
if (actionId == IME_ACTION_DONE && importButton.isEnabled() &&
importButton.getVisibility() == VISIBLE) {
publish();
hideSoftKeyboard(urlInput);
return true;
}
return false;
@@ -123,6 +123,7 @@ public class RssFeedImportActivity extends BriarActivity {
// hide import button, show progress bar
importButton.setVisibility(GONE);
progressBar.setVisibility(VISIBLE);
hideSoftKeyboard(urlInput);
String url = validateAndNormaliseUrl(urlInput.getText().toString());
if (url == null) throw new AssertionError();

View File

@@ -3,11 +3,8 @@ package org.briarproject.briar.android.blog;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.db.DbException;
@@ -44,7 +41,7 @@ import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_L
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class WriteBlogPostActivity extends BriarActivity
implements OnEditorActionListener, SendListener {
implements SendListener {
private static final Logger LOG =
Logger.getLogger(WriteBlogPostActivity.class.getName());
@@ -113,12 +110,6 @@ public class WriteBlogPostActivity extends BriarActivity
component.inject(this);
}
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent e) {
input.requestFocus();
return true;
}
@Override
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {

View File

@@ -14,7 +14,6 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager;
@@ -57,7 +56,6 @@ import javax.inject.Inject;
import io.github.kobakei.materialfabspeeddial.FabSpeedDial;
import io.github.kobakei.materialfabspeeddial.FabSpeedDial.OnMenuItemClickListener;
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu;
import static android.os.Build.VERSION.SDK_INT;
import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE;
@@ -85,8 +83,6 @@ public class ContactListFragment extends BaseFragment implements EventListener,
EventBus eventBus;
@Inject
AndroidNotificationManager notificationManager;
@Inject
FeatureFlags featureFlags;
private ContactListAdapter adapter;
private BriarRecyclerView list;
@@ -126,19 +122,7 @@ public class ContactListFragment extends BaseFragment implements EventListener,
container, false);
FabSpeedDial speedDial = contentView.findViewById(R.id.speedDial);
if (featureFlags.shouldEnableRemoteContacts()) {
speedDial.addOnMenuItemClickListener(this);
} else {
speedDial.setMenu(new FabSpeedDialMenu(contentView.getContext()));
speedDial.addOnStateChangeListener(open -> {
if (open) {
Intent intent = new Intent(getContext(),
ContactExchangeActivity.class);
startActivity(intent);
speedDial.closeMenu();
}
});
}
speedDial.addOnMenuItemClickListener(this);
OnContactClickListener<ContactListItem> onContactClickListener =
(view, item) -> {
@@ -169,9 +153,10 @@ public class ContactListFragment extends BaseFragment implements EventListener,
startActivity(i);
}
};
adapter = new ContactListAdapter(getContext(), onContactClickListener);
adapter = new ContactListAdapter(requireContext(),
onContactClickListener);
list = contentView.findViewById(R.id.list);
list.setLayoutManager(new LinearLayoutManager(getContext()));
list.setLayoutManager(new LinearLayoutManager(requireContext()));
list.setAdapter(adapter);
list.setEmptyImage(R.drawable.ic_empty_state_contact_list);
list.setEmptyText(getString(R.string.no_contacts));
@@ -267,7 +252,7 @@ public class ContactListFragment extends BaseFragment implements EventListener,
if (revision == adapter.getRevision()) {
adapter.incrementRevision();
if (contacts.isEmpty()) list.showData();
else adapter.addAll(contacts);
else adapter.replaceAll(contacts);
} else {
LOG.info("Concurrent update, reloading");
loadContacts();

View File

@@ -9,8 +9,10 @@ import android.support.annotation.Nullable;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.UnsupportedVersionException;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.contact.PendingContact;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchPendingContactException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.LiveResult;
@@ -118,4 +120,19 @@ public class AddContactViewModel extends AndroidViewModel {
return addContactResult;
}
public void updatePendingContact(String name, PendingContact p) {
dbExecutor.execute(() -> {
try {
contactManager.removePendingContact(p.getId());
addContact(name);
} catch(NoSuchPendingContactException e) {
logException(LOG, WARNING, e);
// no error in UI as pending contact was converted into contact
} catch (DbException e) {
logException(LOG, WARNING, e);
addContactResult.postValue(new LiveResult<>(e));
}
});
}
}

View File

@@ -1,5 +1,6 @@
package org.briarproject.briar.android.contact.add.remote;
import android.animation.ObjectAnimator;
import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.content.ClipData;
@@ -11,7 +12,9 @@ import android.support.v4.app.ShareCompat.IntentBuilder;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.Button;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
@@ -34,7 +37,8 @@ import static org.briarproject.briar.android.util.UiUtils.observeOnce;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class LinkExchangeFragment extends BaseFragment {
public class LinkExchangeFragment extends BaseFragment
implements OnGlobalLayoutListener {
private static final String TAG = LinkExchangeFragment.class.getName();
@@ -90,9 +94,30 @@ public class LinkExchangeFragment extends BaseFragment {
observeOnce(viewModel.getHandshakeLink(), this,
this::onHandshakeLinkLoaded);
if (savedInstanceState == null) {
ScrollView scrollView = (ScrollView) v;
// we need to wait for views to be laid out to get the heights
scrollView.getViewTreeObserver().addOnGlobalLayoutListener(this);
}
return v;
}
@Override
public void onGlobalLayout() {
ScrollView scrollView = (ScrollView) requireNonNull(getView());
View layout = scrollView.getChildAt(0);
int scrollBy = layout.getHeight() - scrollView.getHeight();
if (scrollBy > 0) {
// smoothScrollTo() is too fast due to the transition animation
ObjectAnimator animator = ObjectAnimator
.ofInt(scrollView, "scrollY", scrollBy);
animator.setDuration(1000);
animator.start();
}
layout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
private void onHandshakeLinkLoaded(String link) {
View v = requireNonNull(getView());

View File

@@ -2,10 +2,15 @@ package org.briarproject.briar.android.contact.add.remote;
import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.content.Context;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.annotation.StringRes;
import android.support.design.widget.TextInputEditText;
import android.support.design.widget.TextInputLayout;
import android.support.v7.app.AlertDialog.Builder;
import android.text.Editable;
import android.view.LayoutInflater;
import android.view.View;
@@ -15,6 +20,10 @@ import android.widget.ProgressBar;
import android.widget.Toast;
import org.briarproject.bramble.api.UnsupportedVersionException;
import org.briarproject.bramble.api.contact.PendingContact;
import org.briarproject.bramble.api.db.ContactExistsException;
import org.briarproject.bramble.api.db.PendingContactExistsException;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
@@ -24,9 +33,13 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import javax.annotation.Nullable;
import javax.inject.Inject;
import static android.support.v4.content.ContextCompat.getColor;
import static android.support.v4.content.ContextCompat.getDrawable;
import static android.support.v4.graphics.drawable.DrawableCompat.setTint;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong;
@@ -82,19 +95,20 @@ public class NicknameFragment extends BaseFragment {
@Nullable
private String getNicknameOrNull() {
Editable name = contactNameInput.getText();
if (name == null || name.toString().trim().length() == 0) {
Editable text = contactNameInput.getText();
if (text == null || text.toString().trim().length() == 0) {
contactNameLayout.setError(getString(R.string.nickname_missing));
contactNameInput.requestFocus();
return null;
}
if (utf8IsTooLong(name.toString(), MAX_AUTHOR_NAME_LENGTH)) {
String name = text.toString().trim();
if (utf8IsTooLong(name, MAX_AUTHOR_NAME_LENGTH)) {
contactNameLayout.setError(getString(R.string.name_too_long));
contactNameInput.requestFocus();
return null;
}
contactNameLayout.setError(null);
return name.toString().trim();
return name;
}
private void onAddButtonClicked() {
@@ -106,23 +120,95 @@ public class NicknameFragment extends BaseFragment {
viewModel.getAddContactResult().observe(this, result -> {
if (result == null) return;
if (result.hasError()) {
int stringRes;
if (result
.getException() instanceof UnsupportedVersionException) {
stringRes = R.string.unsupported_link;
} else {
stringRes = R.string.adding_contact_error;
}
Toast.makeText(getContext(), stringRes, LENGTH_LONG).show();
} else {
Intent intent = new Intent(getActivity(),
PendingContactListActivity.class);
startActivity(intent);
}
finish();
if (result.hasError())
handleException(name, requireNonNull(result.getException()));
else
showPendingContactListActivity();
});
viewModel.addContact(name);
}
private void showPendingContactListActivity() {
Intent intent = new Intent(getActivity(),
PendingContactListActivity.class);
startActivity(intent);
finish();
}
private void handleException(String name, Exception e) {
if (e instanceof ContactExistsException) {
ContactExistsException ce = (ContactExistsException) e;
handleExistingContact(name, ce.getRemoteAuthor());
} else if (e instanceof PendingContactExistsException) {
PendingContactExistsException pe =
(PendingContactExistsException) e;
handleExistingPendingContact(name, pe.getPendingContact());
} else if (e instanceof UnsupportedVersionException) {
int stringRes = R.string.unsupported_link;
Toast.makeText(getContext(), stringRes, LENGTH_LONG).show();
finish();
} else {
int stringRes = R.string.adding_contact_error;
Toast.makeText(getContext(), stringRes, LENGTH_LONG).show();
finish();
}
}
private void handleExistingContact(String name, Author existing) {
OnClickListener listener = (d, w) -> {
d.dismiss();
String str = getString(R.string.contact_already_exists, name);
Toast.makeText(getContext(), str, LENGTH_LONG).show();
finish();
};
showSameLinkDialog(existing.getName(), name,
R.string.duplicate_link_dialog_text_1_contact, listener);
}
private void handleExistingPendingContact(String name, PendingContact p) {
OnClickListener listener = (d, w) -> {
viewModel.updatePendingContact(name, p);
Toast.makeText(getContext(), R.string.pending_contact_updated_toast,
LENGTH_LONG).show();
d.dismiss();
showPendingContactListActivity();
};
showSameLinkDialog(p.getAlias(), name,
R.string.duplicate_link_dialog_text_1, listener);
}
private void showSameLinkDialog(String name1, String name2,
@StringRes int existsRes, OnClickListener samePersonListener) {
Context ctx = requireContext();
Builder b = new Builder(ctx, R.style.BriarDialogTheme_Neutral);
b.setTitle(getString(R.string.duplicate_link_dialog_title));
String msg = getString(existsRes, name1) + "\n\n" +
getString(R.string.duplicate_link_dialog_text_2, name2, name1);
b.setMessage(msg);
b.setPositiveButton(R.string.same_person_button, samePersonListener);
b.setNegativeButton(R.string.different_person_button, (d, w) -> {
d.dismiss();
showWarningDialog(name1, name2);
});
b.setCancelable(false);
b.show();
}
private void showWarningDialog(String name1, String name2) {
Context ctx = requireContext();
Builder b = new Builder(ctx, R.style.BriarDialogTheme);
Drawable icon = getDrawable(ctx, R.drawable.alerts_and_states_error);
setTint(requireNonNull(icon), getColor(ctx, R.color.color_primary));
b.setIcon(icon);
b.setTitle(getString(R.string.duplicate_link_dialog_title));
b.setMessage(
getString(R.string.duplicate_link_dialog_text_3, name1, name2));
b.setPositiveButton(R.string.ok, (d, w) -> {
d.dismiss();
finish();
});
b.setCancelable(false);
b.show();
}
}

View File

@@ -91,7 +91,7 @@ public class PendingContactListViewModel extends AndroidViewModel
Collection<Pair<PendingContact, PendingContactState>> pairs =
contactManager.getPendingContacts();
List<PendingContactItem> items = new ArrayList<>(pairs.size());
boolean online = false;
boolean online = items.isEmpty();
for (Pair<PendingContact, PendingContactState> pair : pairs) {
PendingContact p = pair.getFirst();
PendingContactState state = pair.getSecond();

View File

@@ -21,9 +21,12 @@ import org.briarproject.briar.android.activity.BaseActivity;
import javax.inject.Inject;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.util.StringUtils.toUtf8;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -76,13 +79,14 @@ public class AliasDialogFragment extends AppCompatDialogFragment {
setButton.setOnClickListener(v1 -> onSetButtonClicked());
Button cancelButton = v.findViewById(R.id.cancelButton);
cancelButton.setOnClickListener(v1 -> getDialog().cancel());
cancelButton.setOnClickListener(v1 -> onCancelButtonClicked());
return v;
}
private void onSetButtonClicked() {
String alias = aliasEditText.getText().toString();
hideSoftKeyboard(aliasEditText);
String alias = aliasEditText.getText().toString().trim();
if (toUtf8(alias).length > MAX_AUTHOR_NAME_LENGTH) {
aliasEditLayout.setError(getString(R.string.name_too_long));
} else {
@@ -91,4 +95,17 @@ public class AliasDialogFragment extends AppCompatDialogFragment {
}
}
private void onCancelButtonClicked() {
hideSoftKeyboard(aliasEditText);
getDialog().cancel();
}
@Override
public void onStart() {
super.onStart();
requireNonNull(getDialog().getWindow())
.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
showSoftKeyboard(aliasEditText);
}
}

View File

@@ -276,7 +276,7 @@ public class ConversationActivity extends BriarActivity
textInputView.setSendController(sendController);
textInputView.setMaxTextLength(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
textInputView.setReady(false);
textInputView.addOnKeyboardShownListener(this::scrollToBottom);
textInputView.setOnKeyboardShownListener(this::scrollToBottom);
}
private void scrollToBottom() {
@@ -356,6 +356,11 @@ public class ConversationActivity extends BriarActivity
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.conversation_actions, menu);
// Hide private message deletion action if feature is not enabled
if (!featureFlags.shouldEnablePrivateMessageDeletion()) {
menu.removeItem(R.id.action_delete_all_messages);
}
// enable introduction action if available
observeOnce(viewModel.showIntroductionAction(), this, enable -> {
if (enable != null && enable) {
@@ -389,6 +394,9 @@ public class ConversationActivity extends BriarActivity
AliasDialogFragment.newInstance().show(
getSupportFragmentManager(), AliasDialogFragment.TAG);
return true;
case R.id.action_delete_all_messages:
askToDeleteAllMessages();
return true;
case R.id.action_social_remove_person:
askToRemoveContact();
return true;
@@ -727,6 +735,52 @@ public class ConversationActivity extends BriarActivity
addConversationItem(h.accept(visitor));
}
private void askToDeleteAllMessages() {
AlertDialog.Builder builder =
new AlertDialog.Builder(this, R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.dialog_title_delete_all_messages));
builder.setMessage(
getString(R.string.dialog_message_delete_all_messages));
builder.setNegativeButton(R.string.delete,
(dialog, which) -> deleteAllMessages());
builder.setPositiveButton(R.string.cancel, null);
builder.show();
}
private void deleteAllMessages() {
list.showProgressBar();
runOnDbThread(() -> {
try {
boolean allDeleted =
conversationManager.deleteAllMessages(contactId);
reloadConversationAfterDeletingAllMessages(allDeleted);
} catch (DbException e) {
logException(LOG, WARNING, e);
runOnUiThreadUnlessDestroyed(() -> list.showData());
}
});
}
private void reloadConversationAfterDeletingAllMessages(
boolean allDeleted) {
runOnUiThreadUnlessDestroyed(() -> {
adapter.clear();
loadMessages();
if (!allDeleted) showNotAllDeletedDialog();
});
}
private void showNotAllDeletedDialog() {
AlertDialog.Builder builder =
new AlertDialog.Builder(this, R.style.BriarDialogTheme);
builder.setTitle(
getString(R.string.dialog_title_not_all_messages_deleted));
builder.setMessage(
getString(R.string.dialog_message_not_all_messages_deleted));
builder.setPositiveButton(R.string.ok, null);
builder.show();
}
private void askToRemoveContact() {
DialogInterface.OnClickListener okListener =
(dialog, which) -> removeContact();

View File

@@ -18,7 +18,6 @@ import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
@@ -51,7 +50,6 @@ import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.attachment.AttachmentDimensions.getAttachmentDimensions;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
@@ -101,10 +99,13 @@ public class ConversationViewModel extends AndroidViewModel
@Inject
ConversationViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
@IoExecutor Executor ioExecutor, TransactionManager db,
MessagingManager messagingManager, ContactManager contactManager,
TransactionManager db,
MessagingManager messagingManager,
ContactManager contactManager,
SettingsManager settingsManager,
PrivateMessageFactory privateMessageFactory) {
PrivateMessageFactory privateMessageFactory,
AttachmentRetriever attachmentRetriever,
AttachmentCreator attachmentCreator) {
super(application);
this.dbExecutor = dbExecutor;
this.db = db;
@@ -112,10 +113,8 @@ public class ConversationViewModel extends AndroidViewModel
this.contactManager = contactManager;
this.settingsManager = settingsManager;
this.privateMessageFactory = privateMessageFactory;
this.attachmentRetriever = new AttachmentRetriever(messagingManager,
getAttachmentDimensions(application.getResources()));
this.attachmentCreator = new AttachmentCreator(getApplication(),
ioExecutor, messagingManager, attachmentRetriever);
this.attachmentRetriever = attachmentRetriever;
this.attachmentCreator = attachmentCreator;
messagingGroupId = Transformations
.map(contact, c -> messagingManager.getContactGroup(c).getId());
contactDeleted.setValue(false);

View File

@@ -142,11 +142,8 @@ public class ImageActivity extends BriarActivity
viewPager.setAdapter(pagerAdapter);
viewPager.setCurrentItem(position);
if (SDK_INT >= 16) {
viewModel.getOnImageClicked()
.observeEvent(this, this::onImageClicked);
window.getDecorView().setSystemUiVisibility(UI_FLAGS_DEFAULT);
}
viewModel.getOnImageClicked().observeEvent(this, this::onImageClicked);
window.getDecorView().setSystemUiVisibility(UI_FLAGS_DEFAULT);
}
@Override
@@ -174,11 +171,7 @@ public class ImageActivity extends BriarActivity
viewModel.setToolbarPosition(
appBarLayout.getTop(), appBarLayout.getBottom()
);
if (SDK_INT >= 16) {
layout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
layout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
layout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
@Override
@@ -265,7 +258,7 @@ public class ImageActivity extends BriarActivity
* when the previous activity (with visible status bar) is shown.
*/
private void showStatusBarBeforeFinishing() {
if (SDK_INT >= 16 && appBarLayout.getVisibility() == GONE) {
if (appBarLayout.getVisibility() == GONE) {
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(UI_FLAGS_DEFAULT);
}

View File

@@ -34,6 +34,8 @@ import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.util.UiUtils.enterPressed;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
@MethodsNotNullByDefault
@@ -91,12 +93,6 @@ public class CreateForumActivity extends BriarActivity {
progress = findViewById(R.id.createForumProgressBar);
}
@Override
public void onStart() {
super.onStart();
showSoftKeyboard(nameEntry);
}
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);

View File

@@ -182,7 +182,7 @@ public class ForumListFragment extends BaseEventFragment implements
if (revision == adapter.getRevision()) {
adapter.incrementRevision();
if (forums.isEmpty()) list.showData();
else adapter.addAll(forums);
else adapter.replaceAll(forums);
} else {
LOG.info("Concurrent update, reloading");
loadForums();

View File

@@ -43,6 +43,7 @@ import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_TEXT_LENGTH;
@MethodsNotNullByDefault
@@ -184,7 +185,7 @@ public class IntroductionMessageFragment extends BaseFragment
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
introductionActivity.hideSoftKeyboard(ui.message);
hideSoftKeyboard(ui.message);
introductionActivity.onBackPressed();
return true;
default:
@@ -201,7 +202,7 @@ public class IntroductionMessageFragment extends BaseFragment
makeIntroduction(contact1, contact2, text);
// don't wait for the introduction to be made before finishing activity
introductionActivity.hideSoftKeyboard(ui.message);
hideSoftKeyboard(ui.message);
introductionActivity.setResult(RESULT_OK);
introductionActivity.supportFinishAfterTransition();
}

View File

@@ -35,7 +35,6 @@ import static android.hardware.Camera.Parameters.FOCUS_MODE_FIXED;
import static android.hardware.Camera.Parameters.FOCUS_MODE_MACRO;
import static android.hardware.Camera.Parameters.SCENE_MODE_AUTO;
import static android.hardware.Camera.Parameters.SCENE_MODE_BARCODE;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
@@ -340,7 +339,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
@UiThread
private void setVideoStabilisation(Parameters params) {
if (SDK_INT >= 15 && params.isVideoStabilizationSupported()) {
if (params.isVideoStabilizationSupported()) {
params.setVideoStabilization(true);
}
}
@@ -415,10 +414,8 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
} catch (RuntimeException e) {
throw new CameraException(e);
}
if (SDK_INT >= 15) {
LOG.info("Video stabilisation enabled: "
+ params.getVideoStabilization());
}
LOG.info("Video stabilisation enabled: "
+ params.getVideoStabilization());
LOG.info("Scene mode: " + params.getSceneMode());
LOG.info("Focus mode: " + params.getFocusMode());
LOG.info("Flash mode: " + params.getFlashMode());

View File

@@ -62,8 +62,7 @@ public class ContactExchangeActivity extends KeyAgreementActivity {
@UiThread
private void contactExchangeSucceeded(Author remoteAuthor) {
String contactName = remoteAuthor.getName();
String format = getString(R.string.contact_added_toast);
String text = String.format(format, contactName);
String text = getString(R.string.contact_added_toast, contactName);
Toast.makeText(this, text, LENGTH_LONG).show();
supportFinishAfterTransition();
}

View File

@@ -183,10 +183,14 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
if (bt == null) {
setBluetoothState(BluetoothState.NO_ADAPTER);
} else {
setBluetoothState(BluetoothState.WAITING);
wasAdapterEnabled = bt.isEnabled();
Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE);
startActivityForResult(i, REQUEST_BLUETOOTH_DISCOVERABLE);
if (i.resolveActivity(getPackageManager()) != null) {
setBluetoothState(BluetoothState.WAITING);
wasAdapterEnabled = bt.isEnabled();
startActivityForResult(i, REQUEST_BLUETOOTH_DISCOVERABLE);
} else {
setBluetoothState(BluetoothState.NO_ADAPTER);
}
}
}

View File

@@ -26,6 +26,8 @@ import javax.inject.Inject;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
public class ChangePasswordActivity extends BriarActivity
implements OnClickListener, OnEditorActionListener {

View File

@@ -83,12 +83,6 @@ public class PasswordFragment extends BaseFragment implements TextWatcher {
return v;
}
@Override
public void onResume() {
super.onResume();
showSoftKeyboard(password);
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {

View File

@@ -47,7 +47,9 @@ public class GroupActivity extends
@Inject
GroupController controller;
private boolean isCreator, isDissolved = false;
@Nullable
private Boolean isCreator = null;
private boolean isDissolved = false;
private MenuItem revealMenuItem, inviteMenuItem, leaveMenuItem,
dissolveMenuItem;
@@ -137,6 +139,14 @@ public class GroupActivity extends
inviteMenuItem = menu.findItem(R.id.action_group_invite);
leaveMenuItem = menu.findItem(R.id.action_group_leave);
dissolveMenuItem = menu.findItem(R.id.action_group_dissolve);
// all role-dependent items are invisible until we know our role
revealMenuItem.setVisible(false);
inviteMenuItem.setVisible(false);
leaveMenuItem.setVisible(false);
dissolveMenuItem.setVisible(false);
// show items based on role
showMenuItems();
return super.onCreateOptionsMenu(menu);
@@ -151,19 +161,27 @@ public class GroupActivity extends
startActivity(i1);
return true;
case R.id.action_group_reveal:
if (isCreator == null || isCreator)
throw new IllegalStateException();
Intent i2 = new Intent(this, RevealContactsActivity.class);
i2.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i2);
return true;
case R.id.action_group_invite:
if (isCreator == null || !isCreator)
throw new IllegalStateException();
Intent i3 = new Intent(this, GroupInviteActivity.class);
i3.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i3, REQUEST_GROUP_INVITE);
return true;
case R.id.action_group_leave:
if (isCreator == null || isCreator)
throw new IllegalStateException();
showLeaveGroupDialog();
return true;
case R.id.action_group_dissolve:
if (isCreator == null || !isCreator)
throw new IllegalStateException();
showDissolveGroupDialog();
default:
return super.onOptionsItemSelected(item);
@@ -209,18 +227,12 @@ public class GroupActivity extends
}
private void showMenuItems() {
if (leaveMenuItem == null || dissolveMenuItem == null) return;
if (isCreator) {
revealMenuItem.setVisible(false);
inviteMenuItem.setVisible(true);
leaveMenuItem.setVisible(false);
dissolveMenuItem.setVisible(true);
} else {
revealMenuItem.setVisible(true);
inviteMenuItem.setVisible(false);
leaveMenuItem.setVisible(true);
dissolveMenuItem.setVisible(false);
}
// we need to have the menu items and know if we are the creator
if (leaveMenuItem == null || isCreator == null) return;
revealMenuItem.setVisible(!isCreator);
inviteMenuItem.setVisible(isCreator);
leaveMenuItem.setVisible(!isCreator);
dissolveMenuItem.setVisible(isCreator);
}
private void showLeaveGroupDialog() {

View File

@@ -24,6 +24,7 @@ import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static org.briarproject.briar.android.util.UiUtils.enterPressed;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_NAME_LENGTH;
@MethodsNotNullByDefault
@@ -91,12 +92,6 @@ public class CreateGroupFragment extends BaseFragment {
return v;
}
@Override
public void onStart() {
super.onStart();
listener.showSoftKeyboard(nameEntry);
}
@Override
public String getUniqueTag() {
return TAG;
@@ -120,7 +115,7 @@ public class CreateGroupFragment extends BaseFragment {
private void createGroup() {
if (!validateName()) return;
listener.hideSoftKeyboard(nameEntry);
hideSoftKeyboard(nameEntry);
createGroupButton.setVisibility(GONE);
progress.setVisibility(VISIBLE);
listener.onGroupNameChosen(nameEntry.getText().toString());

View File

@@ -1,15 +1,8 @@
package org.briarproject.briar.android.privategroup.creation;
import android.view.View;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
interface CreateGroupListener extends BaseFragmentListener {
void onGroupNameChosen(String name);
void showSoftKeyboard(View view);
void hideSoftKeyboard(View view);
}

View File

@@ -194,7 +194,7 @@ public class GroupListFragment extends BaseFragment implements
if (revision == adapter.getRevision()) {
adapter.incrementRevision();
if (groups.isEmpty()) list.showData();
else adapter.addAll(groups);
else adapter.replaceAll(groups);
} else {
LOG.info("Concurrent update, reloading");
loadGroups();

View File

@@ -90,14 +90,9 @@ public class BriarReportPrimer implements ReportPrimer {
ActivityManager.MemoryInfo mem = new ActivityManager.MemoryInfo();
am.getMemoryInfo(mem);
String systemMemory;
if (Build.VERSION.SDK_INT >= 16) {
systemMemory = (mem.totalMem / 1024 / 1024) + " MiB total, "
+ (mem.availMem / 1024 / 1204) + " MiB free, "
+ (mem.threshold / 1024 / 1024) + " MiB threshold";
} else {
systemMemory = (mem.availMem / 1024 / 1204) + " MiB free, "
+ (mem.threshold / 1024 / 1024) + " MiB threshold";
}
systemMemory = (mem.totalMem / 1024 / 1024) + " MiB total, "
+ (mem.availMem / 1024 / 1204) + " MiB free, "
+ (mem.threshold / 1024 / 1024) + " MiB threshold";
customData.put("System memory", systemMemory);
// Virtual machine memory

View File

@@ -218,11 +218,17 @@ public class SettingsFragment extends PreferenceFragmentCompat
});
if (SDK_INT < 27) {
// remove System Default Theme option
// remove System Default Theme option from preference entries
// as it is not functional on this API anyway
List<CharSequence> entries =
new ArrayList<>(Arrays.asList(theme.getEntries()));
entries.remove(getString(R.string.pref_theme_system));
theme.setEntries(entries.toArray(new CharSequence[0]));
// also remove corresponding value
List<CharSequence> values =
new ArrayList<>(Arrays.asList(theme.getEntryValues()));
values.remove(getString(R.string.pref_theme_system_value));
theme.setEntryValues(values.toArray(new CharSequence[0]));
}
if (IS_DEBUG_BUILD) {
findPreference("pref_key_explode").setOnPreferenceClickListener(
@@ -489,7 +495,13 @@ public class SettingsFragment extends PreferenceFragmentCompat
Intent intent = new Intent(ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(EXTRA_APP_PACKAGE, packageName)
.putExtra(EXTRA_CHANNEL_ID, channelId);
startActivity(intent);
Context ctx = requireContext();
if (intent.resolveActivity(ctx.getPackageManager()) != null) {
startActivity(intent);
} else {
Toast.makeText(ctx, R.string.error_start_activity, LENGTH_SHORT)
.show();
}
return true;
});
}
@@ -511,7 +523,12 @@ public class SettingsFragment extends PreferenceFragmentCompat
else uri = Uri.parse(ringtoneUri);
i.putExtra(EXTRA_RINGTONE_EXISTING_URI, uri);
}
startActivityForResult(i, REQUEST_RINGTONE);
if (i.resolveActivity(requireActivity().getPackageManager()) != null) {
startActivityForResult(i, REQUEST_RINGTONE);
} else {
Toast.makeText(getContext(), R.string.cannot_load_ringtone,
LENGTH_SHORT).show();
}
return true;
}
@@ -646,7 +663,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
} else {
// The user chose a ringtone other than the default
Ringtone r = RingtoneManager.getRingtone(getContext(), uri);
if (r == null) {
if (r == null || "file".equals(uri.getScheme())) {
Toast.makeText(getContext(), R.string.cannot_load_ringtone,
LENGTH_SHORT).show();
} else {

View File

@@ -64,12 +64,6 @@ public abstract class BaseMessageFragment extends BaseFragment
@StringRes
protected abstract int getHintText();
@Override
public void onStart() {
super.onStart();
message.showSoftKeyboard();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {

View File

@@ -27,7 +27,6 @@ import org.briarproject.briar.android.threaded.ThreadListController.ThreadListDa
import org.briarproject.briar.android.threaded.ThreadListController.ThreadListListener;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.view.KeyboardAwareLinearLayout;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
@@ -284,14 +283,10 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
scrollToItemAtTop(item);
} else {
// wait with scrolling until keyboard opened
textInput.addOnKeyboardShownListener(
new KeyboardAwareLinearLayout.OnKeyboardShownListener() {
@Override
public void onKeyboardShown() {
scrollToItemAtTop(item);
textInput.removeOnKeyboardShownListener(this);
}
});
textInput.setOnKeyboardShownListener(() -> {
scrollToItemAtTop(item);
textInput.setOnKeyboardShownListener(null);
});
}
}
@@ -332,7 +327,6 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
private void updateTextInput() {
if (replyId != null) {
textInput.setHint(R.string.forum_message_reply_hint);
textInput.requestFocus();
textInput.showSoftKeyboard();
} else {
textInput.setHint(R.string.forum_new_message_hint);

View File

@@ -79,6 +79,10 @@ public abstract class BriarAdapter<T, V extends ViewHolder>
this.items.addAll(items);
}
public void replaceAll(Collection<T> items) {
this.items.replaceAll(items);
}
public void setItems(Collection<T> items) {
this.items.beginBatchedUpdates();
this.items.clear();

View File

@@ -4,13 +4,18 @@ import android.support.annotation.ColorRes;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.support.design.widget.Snackbar.Callback;
import android.view.View;
import android.view.View.OnClickListener;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import static android.os.Build.VERSION.SDK_INT;
import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE;
import static android.support.v4.content.ContextCompat.getColor;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
@NotNullByDefault
public class BriarSnackbarBuilder {
@@ -30,6 +35,24 @@ public class BriarSnackbarBuilder {
R.color.briar_button_text_positive));
s.setAction(actionResId, onClickListener);
}
// Workaround for https://issuetracker.google.com/issues/64285517
if (duration == LENGTH_INDEFINITE && SDK_INT < 21) {
// Hide snackbar while it's opening to make bouncing less noticeable
s.getView().setVisibility(INVISIBLE);
s.addCallback(new Callback() {
@Override
public void onShown(Snackbar snackbar) {
snackbar.getView().setVisibility(VISIBLE);
// Request layout again in case snackbar is in wrong place
snackbar.getView().requestLayout();
}
@Override
public void onDismissed(Snackbar snackbar, int event) {
snackbar.getView().setVisibility(INVISIBLE);
}
});
}
return s;
}

View File

@@ -92,9 +92,11 @@ public class UiUtils {
public static final float GREY_OUT = 0.5f;
public static void showSoftKeyboard(View view) {
InputMethodManager imm = requireNonNull(
getSystemService(view.getContext(), InputMethodManager.class));
imm.showSoftInput(view, SHOW_IMPLICIT);
if (view.requestFocus()) {
InputMethodManager imm = requireNonNull(getSystemService(
view.getContext(), InputMethodManager.class));
imm.showSoftInput(view, SHOW_IMPLICIT);
}
}
public static void hideSoftKeyboard(View view) {

View File

@@ -1,44 +0,0 @@
package org.briarproject.briar.android.view;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.Snackbar;
import android.util.AttributeSet;
import android.view.View;
public class BriarRecyclerViewBehavior
extends CoordinatorLayout.Behavior<BriarRecyclerView> {
public BriarRecyclerViewBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent,
BriarRecyclerView child, View dependency) {
// FIXME the below code works, but does not reset margin when snackbar is dismissed
/*
int margin = 0;
if (dependency.isShown()) margin = dependency.getHeight();
// set snackbar height as bottom margin if it is shown
CoordinatorLayout.LayoutParams params =
(CoordinatorLayout.LayoutParams) child.getLayoutParams();
params.setMargins(0, 0, 0, margin);
child.setLayoutParams(params);
child.scrollToPosition(0);
*/
return true;
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent,
BriarRecyclerView child, View dependency) {
// we only want to trigger the change
// only when the changes is from a snackbar
return dependency instanceof Snackbar.SnackbarLayout;
}
}

View File

@@ -13,7 +13,6 @@ import android.widget.ProgressBar;
import org.briarproject.briar.R;
import static android.content.Context.LAYOUT_INFLATER_SERVICE;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.Objects.requireNonNull;
public class CompositeSendButton extends FrameLayout {
@@ -75,33 +74,24 @@ public class CompositeSendButton extends FrameLayout {
if (showImageButton) {
imageButton.setVisibility(VISIBLE);
sendButton.setEnabled(false);
if (SDK_INT <= 15) {
sendButton.clearAnimation();
sendButton.animate().alpha(0f).withEndAction(() -> {
sendButton.setVisibility(INVISIBLE);
imageButton.setEnabled(true);
} else {
sendButton.clearAnimation();
sendButton.animate().alpha(0f).withEndAction(() -> {
sendButton.setVisibility(INVISIBLE);
imageButton.setEnabled(true);
}).start();
imageButton.clearAnimation();
imageButton.animate().alpha(1f).start();
}
}).start();
imageButton.clearAnimation();
imageButton.animate().alpha(1f).start();
} else {
sendButton.setVisibility(VISIBLE);
// enable/disable buttons right away to allow fast sending
sendButton.setEnabled(sendEnabled);
imageButton.setEnabled(false);
if (SDK_INT <= 15) {
imageButton.setVisibility(INVISIBLE);
} else {
sendButton.clearAnimation();
sendButton.animate().alpha(1f).start();
imageButton.clearAnimation();
imageButton.animate().alpha(0f).withEndAction(() ->
imageButton.setVisibility(INVISIBLE)
).start();
}
sendButton.clearAnimation();
sendButton.animate().alpha(1f).start();
imageButton.clearAnimation();
imageButton.animate().alpha(0f).withEndAction(() ->
imageButton.setVisibility(INVISIBLE)
).start();
}
}

View File

@@ -13,8 +13,8 @@ import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import com.vanniktech.emoji.EmojiEditText;
import com.vanniktech.emoji.EmojiPopup;
import com.vanniktech.emoji.RecentEmoji;
@@ -28,10 +28,12 @@ import static android.content.Context.LAYOUT_INFLATER_SERVICE;
import static android.view.KeyEvent.KEYCODE_ENTER;
import static android.view.inputmethod.EditorInfo.IME_ACTION_SEND;
import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT;
import static java.lang.Character.isWhitespace;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong;
import static org.briarproject.briar.android.util.UiUtils.resolveColorAttribute;
public class EmojiTextInputView extends KeyboardAwareLinearLayout implements
public class EmojiTextInputView extends LinearLayout implements
TextWatcher {
@Inject
@@ -40,12 +42,16 @@ public class EmojiTextInputView extends KeyboardAwareLinearLayout implements
private final AppCompatImageButton emojiToggle;
private final EmojiPopup emojiPopup;
private final EditText editText;
private final InputMethodManager imm;
@Nullable
private TextInputListener listener;
@Nullable
private OnKeyboardShownListener keyboardShownListener;
private int maxLength = Integer.MAX_VALUE;
private boolean emptyTextAllowed = false;
private boolean isEmpty = true;
private boolean keyboardOpen = false;
public EmojiTextInputView(Context context) {
this(context, null);
@@ -79,7 +85,6 @@ public class EmojiTextInputView extends KeyboardAwareLinearLayout implements
editText = findViewById(R.id.input_text);
editText.setPadding(0, 0, paddingEnd, paddingBottom);
if (maxLines > 0) editText.setMaxLines(maxLines);
editText.setOnClickListener(v -> showSoftKeyboard());
editText.addTextChangedListener(this);
editText.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == IME_ACTION_SEND) {
@@ -103,18 +108,30 @@ public class EmojiTextInputView extends KeyboardAwareLinearLayout implements
// stuff we can't do in edit mode goes below
if (isInEditMode()) {
emojiPopup = null;
imm = null;
return;
}
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
imm = (InputMethodManager) requireNonNull(o);
BriarApplication app =
(BriarApplication) context.getApplicationContext();
app.getApplicationComponent().inject(this);
emojiPopup = EmojiPopup.Builder
.fromRootView(this)
.fromRootView(getRootView())
.setRecentEmoji(recentEmoji)
.setOnEmojiPopupShownListener(this::showKeyboardIcon)
.setOnEmojiPopupDismissListener(this::showEmojiIcon)
.build((EmojiEditText) editText);
.setKeyboardAnimationStyle(R.style.emoji_fade_animation_style)
.setOnSoftKeyboardOpenListener(this::onKeyboardOpened)
.setOnSoftKeyboardCloseListener(this::onKeyboardClosed)
.setIconColor(resolveColorAttribute(getContext(),
R.attr.colorControlNormal))
.build(editText);
emojiToggle.setOnClickListener(v -> emojiPopup.toggle());
editText.setOnClickListener(v -> {
if (emojiPopup.isShowing()) emojiPopup.dismiss();
});
}
@Override
@@ -125,19 +142,31 @@ public class EmojiTextInputView extends KeyboardAwareLinearLayout implements
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
// Need to start at position 0 to change empty
if (start != 0 || emptyTextAllowed || listener == null) return;
if (s.length() == 0) {
if (!isEmpty) {
if (emptyTextAllowed || listener == null) return;
// Work out whether the trimmed text has become empty or non-empty
if (isEmpty) {
// We only need to check the characters that were added
if (countLeadingWhitespace(s, start, count) < count) {
isEmpty = false;
listener.onTextIsEmptyChanged(false);
}
} else if (before > 0) {
// Characters have been removed or replaced - check from the start
int length = s.length();
if (countLeadingWhitespace(s, 0, length) == length) {
isEmpty = true;
listener.onTextIsEmptyChanged(true);
}
} else if (isEmpty) {
isEmpty = false;
listener.onTextIsEmptyChanged(false);
}
}
private int countLeadingWhitespace(CharSequence s, int off, int len) {
for (int i = 0; i < len; i++) {
if (!isWhitespace(s.charAt(off + i))) return i;
}
return len;
}
@Override
public void afterTextChanged(Editable s) {
}
@@ -218,6 +247,10 @@ public class EmojiTextInputView extends KeyboardAwareLinearLayout implements
editText.setHint(hint);
}
boolean isKeyboardOpen() {
return keyboardOpen || imm.isFullscreenMode();
}
private void showEmojiIcon() {
emojiToggle.setImageResource(R.drawable.ic_emoji_toggle);
}
@@ -227,22 +260,43 @@ public class EmojiTextInputView extends KeyboardAwareLinearLayout implements
}
void showSoftKeyboard() {
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
InputMethodManager imm = (InputMethodManager) requireNonNull(o);
imm.showSoftInput(editText, SHOW_IMPLICIT);
if (editText.requestFocus()) imm.showSoftInput(editText, SHOW_IMPLICIT);
}
void hideSoftKeyboard() {
if (emojiPopup.isShowing()) emojiPopup.dismiss();
IBinder token = editText.getWindowToken();
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
InputMethodManager imm = (InputMethodManager) requireNonNull(o);
imm.hideSoftInputFromWindow(token, 0);
}
private void onKeyboardOpened(
@SuppressWarnings("unused") int keyboardHeight) {
keyboardOpen = true;
if (keyboardShownListener != null)
keyboardShownListener.onKeyboardShown();
}
private void onKeyboardClosed() {
if (imm.isFullscreenMode()) {
onKeyboardOpened(0);
return;
}
keyboardOpen = false;
}
void setOnKeyboardShownListener(
@Nullable OnKeyboardShownListener listener) {
keyboardShownListener = listener;
}
interface TextInputListener {
void onTextIsEmptyChanged(boolean isEmpty);
void onSendEvent();
}
public interface OnKeyboardShownListener {
void onKeyboardShown();
}
}

View File

@@ -1,225 +0,0 @@
/*
Taken from Signal, licences under GPLv3
*/
package org.briarproject.briar.android.view;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Rect;
import android.os.Build;
import android.preference.PreferenceManager;
import android.support.annotation.UiThread;
import android.util.AttributeSet;
import android.view.View;
import android.view.WindowManager;
import android.widget.LinearLayout;
import org.briarproject.briar.R;
import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import static android.content.Context.WINDOW_SERVICE;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
/**
* RelativeLayout that, when a view container, will report back when it thinks
* a soft keyboard has been opened and what its height would be.
*/
@UiThread
public class KeyboardAwareLinearLayout extends LinearLayout {
private static final Logger LOG =
Logger.getLogger(KeyboardAwareLinearLayout.class.getName());
private final Rect rect = new Rect();
private final Set<OnKeyboardShownListener> shownListeners = new HashSet<>();
private final int minKeyboardSize;
private final int minCustomKeyboardSize;
private final int defaultCustomKeyboardSize;
private final int minCustomKeyboardTopMargin;
private final int statusBarHeight;
private int viewInset;
private boolean keyboardOpen = false;
private int rotation = -1;
public KeyboardAwareLinearLayout(Context context) {
this(context, null);
}
public KeyboardAwareLinearLayout(Context context,
@Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public KeyboardAwareLinearLayout(Context context,
@Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
rotation = getDeviceRotation();
int statusBarRes = getResources()
.getIdentifier("status_bar_height", "dimen", "android");
minKeyboardSize =
getResources().getDimensionPixelSize(R.dimen.min_keyboard_size);
minCustomKeyboardSize = getResources()
.getDimensionPixelSize(R.dimen.min_custom_keyboard_size);
defaultCustomKeyboardSize = getResources()
.getDimensionPixelSize(R.dimen.default_custom_keyboard_size);
minCustomKeyboardTopMargin = getResources()
.getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin);
statusBarHeight = statusBarRes > 0 ?
getResources().getDimensionPixelSize(statusBarRes) : 0;
viewInset = getViewInset();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
updateRotation();
updateKeyboardState();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private void updateRotation() {
int oldRotation = rotation;
rotation = getDeviceRotation();
if (oldRotation != rotation) {
LOG.info("Rotation changed");
onKeyboardClose();
}
}
private void updateKeyboardState() {
if (isLandscape()) {
if (keyboardOpen) onKeyboardClose();
return;
}
if (viewInset == 0 && Build.VERSION.SDK_INT >= 21)
viewInset = getViewInset();
int availableHeight =
getRootView().getHeight() - statusBarHeight - viewInset;
getWindowVisibleDisplayFrame(rect);
int keyboardHeight = availableHeight - (rect.bottom - rect.top);
if (keyboardHeight > minKeyboardSize) {
if (getKeyboardHeight() != keyboardHeight)
setKeyboardPortraitHeight(keyboardHeight);
if (!keyboardOpen) onKeyboardOpen(keyboardHeight);
} else if (keyboardOpen) {
onKeyboardClose();
}
}
@TargetApi(21)
private int getViewInset() {
try {
Field attachInfoField = View.class.getDeclaredField("mAttachInfo");
attachInfoField.setAccessible(true);
Object attachInfo = attachInfoField.get(this);
if (attachInfo != null) {
Field stableInsetsField =
attachInfo.getClass().getDeclaredField("mStableInsets");
stableInsetsField.setAccessible(true);
Rect insets = (Rect) stableInsetsField.get(attachInfo);
return insets.bottom;
}
} catch (NoSuchFieldException e) {
LOG.log(WARNING,
"field reflection error when measuring view inset", e);
} catch (IllegalAccessException e) {
LOG.log(WARNING,
"access reflection error when measuring view inset", e);
}
return 0;
}
protected void onKeyboardOpen(int keyboardHeight) {
if (LOG.isLoggable(INFO))
LOG.info("onKeyboardOpen(" + keyboardHeight + ")");
keyboardOpen = true;
notifyShownListeners();
}
protected void onKeyboardClose() {
LOG.info("onKeyboardClose()");
keyboardOpen = false;
}
public boolean isKeyboardOpen() {
return keyboardOpen;
}
public int getKeyboardHeight() {
return isLandscape() ? getKeyboardLandscapeHeight() :
getKeyboardPortraitHeight();
}
public boolean isLandscape() {
int rotation = getDeviceRotation();
return rotation == ROTATION_90 || rotation == ROTATION_270;
}
private int getDeviceRotation() {
WindowManager windowManager =
(WindowManager) getContext().getSystemService(WINDOW_SERVICE);
return requireNonNull(windowManager).getDefaultDisplay().getRotation();
}
private int getKeyboardLandscapeHeight() {
return Math.max(getHeight(), getRootView().getHeight()) / 2;
}
private int getKeyboardPortraitHeight() {
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(getContext());
int keyboardHeight = prefs.getInt("keyboard_height_portrait",
defaultCustomKeyboardSize);
return clamp(keyboardHeight, minCustomKeyboardSize,
getRootView().getHeight() - minCustomKeyboardTopMargin);
}
private int clamp(int value, int min, int max) {
return Math.min(Math.max(value, min), max);
}
private void setKeyboardPortraitHeight(int height) {
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(getContext());
prefs.edit().putInt("keyboard_height_portrait", height).apply();
}
public void addOnKeyboardShownListener(OnKeyboardShownListener listener) {
shownListeners.add(listener);
}
public void removeOnKeyboardShownListener(
OnKeyboardShownListener listener) {
shownListeners.remove(listener);
}
private void notifyShownListeners() {
// Make a copy as listeners may remove themselves when called
Set<OnKeyboardShownListener> listeners = new HashSet<>(shownListeners);
for (OnKeyboardShownListener listener : listeners) {
listener.onKeyboardShown();
}
}
public interface OnKeyboardShownListener {
void onKeyboardShown();
}
}

View File

@@ -0,0 +1,50 @@
package org.briarproject.briar.android.view;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.CoordinatorLayout.Behavior;
import android.support.design.widget.CoordinatorLayout.LayoutParams;
import android.support.design.widget.Snackbar.SnackbarLayout;
import android.util.AttributeSet;
import android.view.View;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
/**
* This behavior makes room for a snackbar at the bottom of the screen. The
* proper solution is to use layout_dodgeInsetEdges="bottom", but when used on
* a scrollable view that results in the view being pushed under the app bar
* (see https://issuetracker.google.com/issues/116541304).
*/
@NotNullByDefault
public class SnackbarAwareBehavior<V extends View> extends Behavior<V> {
public SnackbarAwareBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent,
V child, View snackbar) {
setMargin(child, snackbar.getHeight());
return true;
}
@Override
public void onDependentViewRemoved(CoordinatorLayout parent,
V child, View snackbar) {
setMargin(child, 0);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent,
V child, View dependency) {
return dependency instanceof SnackbarLayout;
}
private void setMargin(V child, int margin) {
LayoutParams params = (LayoutParams) child.getLayoutParams();
params.setMargins(0, 0, 0, margin);
child.setLayoutParams(params);
}
}

View File

@@ -16,7 +16,7 @@ import android.widget.LinearLayout;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.view.KeyboardAwareLinearLayout.OnKeyboardShownListener;
import org.briarproject.briar.android.view.EmojiTextInputView.OnKeyboardShownListener;
import static android.content.Context.LAYOUT_INFLATER_SERVICE;
import static java.util.Objects.requireNonNull;
@@ -139,13 +139,9 @@ public class TextInputView extends LinearLayout {
textInput.hideSoftKeyboard();
}
public void addOnKeyboardShownListener(OnKeyboardShownListener listener) {
textInput.addOnKeyboardShownListener(listener);
}
public void removeOnKeyboardShownListener(
OnKeyboardShownListener listener) {
textInput.removeOnKeyboardShownListener(listener);
public void setOnKeyboardShownListener(
@Nullable OnKeyboardShownListener listener) {
textInput.setOnKeyboardShownListener(listener);
}
}

View File

@@ -29,13 +29,17 @@
app:layout_constraintTop_toTopOf="parent"
app:passwordToggleEnabled="true">
<EditText
<android.support.design.widget.TextInputEditText
android:id="@+id/current_password_entry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/current_password"
android:importantForAutofill="no"
android:inputType="textPassword"
android:maxLines="1"/>
<requestFocus/>
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
@@ -49,11 +53,12 @@
app:layout_constraintTop_toBottomOf="@id/current_password_entry_wrapper"
app:passwordToggleEnabled="true">
<EditText
<android.support.design.widget.TextInputEditText
android:id="@+id/new_password_entry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/choose_new_password"
android:importantForAutofill="no"
android:inputType="textPassword"
android:maxLines="1"/>
</android.support.design.widget.TextInputLayout>
@@ -69,12 +74,13 @@
app:layout_constraintTop_toBottomOf="@id/new_password_entry_wrapper"
app:passwordToggleEnabled="true">
<EditText
<android.support.design.widget.TextInputEditText
android:id="@+id/new_password_confirm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/confirm_new_password"
android:imeOptions="actionDone"
android:importantForAutofill="no"
android:inputType="textPassword"
android:maxLines="1"/>
</android.support.design.widget.TextInputLayout>

View File

@@ -15,14 +15,17 @@
app:errorEnabled="true"
app:hintEnabled="false">
<EditText
<android.support.design.widget.TextInputEditText
android:id="@+id/createForumNameEntry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/choose_forum_hint"
android:importantForAutofill="no"
android:inputType="text|textCapSentences"
android:maxLines="1"/>
<requestFocus/>
</android.support.design.widget.TextInputLayout>
<Button

View File

@@ -18,7 +18,7 @@
app:cardCornerRadius="0dp"
app:cardUseCompatPadding="false">
<EditText
<android.support.design.widget.TextInputEditText
android:id="@+id/urlInput"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -26,9 +26,14 @@
android:gravity="top"
android:hint="@string/blogs_rss_feeds_import_hint"
android:imeOptions="actionDone"
android:importantForAutofill="no"
android:inputType="textUri"
android:padding="@dimen/margin_medium"
android:textColor="?android:attr/textColorPrimary"/>
android:textColor="?android:attr/textColorPrimary">
<requestFocus/>
</android.support.design.widget.TextInputEditText>
</android.support.v7.widget.CardView>

View File

@@ -14,7 +14,11 @@
android:gravity="bottom"
app:buttonText="@string/blogs_publish_blog_post"
app:fillHeight="true"
app:hint="@string/blogs_write_blog_post_body_hint"/>
app:hint="@string/blogs_write_blog_post_body_hint">
<requestFocus/>
</org.briarproject.briar.android.view.LargeTextInputView>
<ProgressBar
android:id="@+id/progressBar"

View File

@@ -6,7 +6,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
tools:parentTag="org.briarproject.briar.android.view.KeyboardAwareLinearLayout"
tools:parentTag="android.widget.LinearLayout"
tools:showIn="@layout/fragment_reblog">
<android.support.v7.widget.AppCompatImageButton

View File

@@ -31,7 +31,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/set_contact_alias_hint"
android:inputType="textPersonName"
android:inputType="text|textCapWords"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/text_size_medium"/>

View File

@@ -9,6 +9,7 @@
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="org.briarproject.briar.android.view.SnackbarAwareBehavior"
app:scrollToEnd="false"/>
<io.github.kobakei.materialfabspeeddial.FabSpeedDial
@@ -19,6 +20,8 @@
app:fab_fabRippleColor="@android:color/transparent"
app:fab_menu="@menu/contact_list_actions"
app:fab_miniFabTextBackground="@color/briar_accent"
app:fab_miniFabTextColor="@android:color/white"/>
app:fab_miniFabTextColor="@android:color/white"
app:layout_anchorGravity="bottom|right|end"
app:layout_behavior="org.briarproject.briar.android.view.SnackbarAwareBehavior"/>
</android.support.design.widget.CoordinatorLayout>

View File

@@ -15,14 +15,17 @@
app:errorEnabled="true"
app:hintEnabled="false">
<EditText
<android.support.design.widget.TextInputEditText
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/groups_create_group_hint"
android:importantForAutofill="no"
android:inputType="text|textCapSentences"
android:maxLines="1"/>
<requestFocus/>
</android.support.design.widget.TextInputLayout>
<Button

View File

@@ -26,7 +26,8 @@
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"/>
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_chainStyle="packed"/>
<TextView
android:id="@+id/stepOneText"
@@ -74,12 +75,14 @@
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="32dp"
android:src="@drawable/ic_nickname"
app:layout_constraintBottom_toTopOf="@+id/nicknameIcon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="256dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/stepOneText"
tools:ignore="ContentDescription"/>
@@ -118,6 +121,7 @@
android:layout_marginTop="16dp"
app:errorEnabled="true"
app:hintEnabled="false"
app:layout_constraintBottom_toTopOf="@+id/space"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
@@ -133,6 +137,16 @@
</android.support.design.widget.TextInputLayout>
<Space
android:id="@+id/space"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/addButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_default="wrap"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/contactNameLayout"/>
<Button
android:id="@+id/addButton"
style="@style/BriarButton"

View File

@@ -31,6 +31,9 @@
android:imeOptions="actionDone"
android:inputType="textPassword"
android:maxLines="1"/>
<requestFocus/>
</android.support.design.widget.TextInputLayout>
<Button

View File

@@ -38,6 +38,7 @@
android:maxLines="1"/>
<requestFocus/>
</android.support.design.widget.TextInputLayout>
<Button

View File

@@ -39,6 +39,7 @@
android:maxLines="1">
<requestFocus/>
</android.support.design.widget.TextInputEditText>
</android.support.design.widget.TextInputLayout>

View File

@@ -16,6 +16,11 @@
android:enabled="false"
app:showAsAction="never"/>
<item
android:id="@+id/action_delete_all_messages"
android:title="@string/delete_all_messages"
app:showAsAction="never"/>
<item
android:id="@+id/action_social_remove_person"
android:icon="@drawable/action_delete_white"

View File

@@ -175,6 +175,13 @@
<string name="step_1">‮واحدة</string>
<!--This is a numeral indicating the second step in a series of screens-->
<string name="step_2">2</string>
<!--This is a question asking whether two nicknames refer to the same person-->
<!--This is a button for answering that two nicknames do indeed refer to the same person. This
string will be used in a dialog button, so if the translation of this string is longer than 20
characters, please use "Yes" instead, and use "No" for the "Different Person" button-->
<!--This is a button for answering that two nicknames refer to different people. This string
will be used in a dialog button, so if the translation of this string longer than 20 characters,
please use "No" instead, and use "Yes" for the "Same Person" button-->
<!--Introductions-->
<string name="introduction_onboarding_title">قم بتقديم جهات إتصالك</string>
<string name="introduction_onboarding_text">يمكنك أن تقدم جهات إتصالك لبعضها البعض، فلا يحتاجون للمقابلة الشخصية ليتواصلوا عبر Briar (براير).</string>

View File

@@ -118,7 +118,9 @@
<string name="message_hint">Mesaj yazın</string>
<string name="image_caption_hint">Mövzu əlavə edin (isteyə bağlı)</string>
<string name="image_attach">Şəkil əlavə edin</string>
<string name="image_attach_error">Şəkil əlavə edilmədi</string>
<string name="image_attach_error">Şəkil (lər) əlavə etmək alınmadı</string>
<string name="image_attach_error_too_big">Şəkilin həcmi böyükdür. Limit %d MB.</string>
<string name="image_attach_error_invalid_mime_type">Şəkil formatı dəstəklənmir: %s</string>
<string name="set_contact_alias">Kontakt adı dəyişdirin</string>
<string name="set_contact_alias_hint">Əlaqə adı</string>
<string name="set_alias_button">Dəyiş</string>
@@ -138,6 +140,7 @@
<string name="dialog_title_image_support">İndi bu kontakta şəkilləri göndərə bilərsiniz</string>
<string name="dialog_message_image_support">Şəkilləri əlavə etmək üçün bu simvola toxunun.</string>
<!--Adding Contacts-->
<string name="add_contact_title">Yaxında kontakt əlavə etmək</string>
<string name="face_to_face">Kontakta əlavə etmək istədiyiniz şəxslə tanış olmalısınız. Bu, hər kəsin kimliyinizi və ya mesajlarınızı gələcəkdə oxumasını maneə törədir.</string>
<string name="continue_button">Davam et</string>
<string name="try_again_button">Yenidən cəhd elə</string>
@@ -155,12 +158,56 @@
<string name="connection_error_explanation">Həmin Wi-Fi şəbəkəsinə qoşulduğunuzu yoxlayın.</string>
<string name="connection_error_feedback">Bu problem davam edərsə, tətbiqin təkmilləşdirilməsinə kömək etmək üçün rəy göndərin.</string>
<!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">Məsafədə kontakt əlavə etmək </string>
<string name="add_contact_nearby_title">Yaxında kontakt əlavə etmək </string>
<string name="add_contact_remotely_title">Məsafədə kontakt əlavə etmək </string>
<string name="contact_name_hint">Niklə kontakt saxlayın</string>
<string name="contact_link_intro">Kontaktın linkini buraya daxil edin</string>
<string name="contact_link_hint">Kontaktın linki</string>
<string name="paste_button">Yapışdır</string>
<string name="add_contact_button">Kontankt əlavə et</string>
<string name="copy_button">Kopyala</string>
<string name="share_button">Paylaş</string>
<string name="send_link_title">Link mübadiləsi</string>
<string name="add_contact_choose_nickname">Ad seç</string>
<string name="add_contact_choose_a_nickname">Ad daxil et</string>
<string name="nickname_intro">Niklə kontakt saxlayın. Bunu ancaq siz görəcəksiniz. </string>
<string name="your_link">Əlavə etmək istədiyiniz kontakt üçün bu linki verin</string>
<string name="link_clip_label">Briar link</string>
<string name="link_copied_toast">Link kopyalandı</string>
<string name="adding_contact_error">Kontaktı əlavə edərkən səhv baş verdi.</string>
<string name="pending_contact_requests_snackbar">Gözləyən kontakt sorğuları var. </string>
<string name="pending_contact_requests">Kənara qoyulmuş kontak sorğuları</string>
<string name="no_pending_contacts">Gözləyən kontakt yoxdur</string>
<string name="add_contact_remote_connecting">Qoşulur...</string>
<string name="waiting_for_contact_to_come_online">Kontaktı xəttdə gözləyin ...</string>
<string name="connecting">Qoşulur...</string>
<string name="adding_contact">Kontaktın əlavə etməsi... </string>
<string name="adding_contact_failed">Kontakt əlavə etməsi alınmadı</string>
<string name="dialog_title_remove_pending_contact">Silməyi təstiqlə</string>
<string name="dialog_message_remove_pending_contact">Bu kontakt hələ də əlavə olunur. İndi onu çıxararsanız, əlavə olunmayacaq.</string>
<string name="own_link_error">Kontaktınızın linkini daxil edin, özünüzü deyil</string>
<string name="nickname_missing">Zəhmət olmasa adı daxil edin</string>
<string name="invalid_link">Səhv link</string>
<string name="unsupported_link">Bu link Briar-ın daha yeni versiyasından gəlir. Ən son versiyaya keçin və yenidən cəhd edin.</string>
<string name="intent_own_link">Öz linkini açdın. Əlavə etmək istədiyiniz kontaktlardan birini istifadə edin!</string>
<string name="missing_link">Zəhmət olmasa linki daxil edin</string>
<!--This is a numeral indicating the first step in a series of screens-->
<string name="step_1">1</string>
<!--This is a numeral indicating the second step in a series of screens-->
<string name="step_2">2</string>
<string name="offline_state">İnternet bağlantısı yoxdur</string>
<string name="duplicate_link_dialog_title">Dublikat Link</string>
<!--This is a question asking whether two nicknames refer to the same person-->
<!--This is a button for answering that two nicknames do indeed refer to the same person. This
string will be used in a dialog button, so if the translation of this string is longer than 20
characters, please use "Yes" instead, and use "No" for the "Different Person" button-->
<string name="same_person_button">Eyni şəxs</string>
<!--This is a button for answering that two nicknames refer to different people. This string
will be used in a dialog button, so if the translation of this string longer than 20 characters,
please use "No" instead, and use "Yes" for the "Same Person" button-->
<string name="different_person_button">Fərqli şəxs</string>
<string name="pending_contact_updated_toast">Gözləyən kontakt yeniləndi</string>
<!--Introductions-->
<string name="introduction_onboarding_title">Kontaktınızı tanıtın</string>
<string name="introduction_onboarding_text">Kontaktlarınızı bir-birinizə təqdim edə bilərsiniz, lakin Briar-a qoşulmaq üçün şəxsən görüşmək lazım deyil.</string>

View File

@@ -0,0 +1,501 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--Setup-->
<string name="setup_title">Dobrodošli u Briar</string>
<string name="setup_name_explanation">Vaše korisničko ime će biti prikazano pored sadržaja posta. Nemožete ga mijenjati nakon kreiranje računa.</string>
<string name="setup_next">Dalje</string>
<string name="setup_password_intro">Izaberite lozinku</string>
<string name="setup_password_explanation">Vaš Briar račun je kriptovan i sačuvan na vašem uređaju, ne na serveru ili oblaku. Ako zaboravite šifru ili deinstalirate Briar, ne postoji način da povratite vaš račun.\n\nIzaberite dugačku lozinku koju je teško pogoditi, kao četiri nasumične riječi ili deset nasumičnih slova, brojeva i simbola.</string>
<string name="setup_doze_title">Konekcije u pozadinskom modu</string>
<string name="setup_doze_intro">Da bi primao poruke, Briar mora da ostane konektovan u pozadinskom modu.</string>
<string name="setup_doze_explanation">Da bi primao poruke, Briar mora da ostane konektovan u pozadinskom modu. Molim vas da isključite optimizacije baterije kako bi Briar ostao konektovan.</string>
<string name="setup_doze_button">Dozvolite konekcije</string>
<string name="choose_nickname">Izaberite vaše korisničko ime</string>
<string name="choose_password">Izaberite vašu lozinku</string>
<string name="confirm_password">Potvrdite vašu lozinku</string>
<string name="name_too_long">Ime je predugačko</string>
<string name="password_too_weak">Lozinka nije dovoljno jaka</string>
<string name="passwords_do_not_match">Lozinke se ne poklapaju</string>
<string name="create_account_button">Kreirajte korisnički račun</string>
<string name="more_info">Više informacija</string>
<string name="don_t_ask_again">Ne pitaj ponovo</string>
<string name="setup_huawei_text">Molim dotaknite dugme ispod i provjerite da je Briar zaštićen na \"Protected Apps\" listi.</string>
<string name="setup_huawei_button">Zaštiti Briar</string>
<string name="setup_huawei_help">Ako Briar nije dodat u listu zaštićenih aplikacija - protected apps, neće moći da radi u pozadinskom modu.</string>
<string name="warning_dozed">%s nije mogao da se pokrene u pozadinskom modu</string>
<!--Login-->
<string name="enter_password">Lozinka</string>
<string name="try_again">Pogrešna lozinka, probajte ponovo</string>
<string name="sign_in_button">Prijavite se</string>
<string name="forgotten_password">Zaboravio/la sam lozinku</string>
<string name="dialog_title_lost_password">Izgubljena Lozinka</string>
<string name="dialog_message_lost_password">Vaš Briar račun je kriptovan i sačuvan na vašem uređaju, ne na serveru ili oblaku, pa nemožemo resetovati vašu lozinku. Želite li da izbrišete račun i počnete iz početka?\n\nPažnja: Vaši identiteti, kontakti i poruke će biti premanentno izgubljene.</string>
<string name="startup_failed_notification_title">Brirar nije mogao da se pokrene</string>
<string name="startup_failed_notification_text">Dotaknite za više informacija.</string>
<string name="startup_failed_activity_title">Briar neuspješno pokretanje</string>
<string name="startup_failed_db_error">Iz nekog razloga, Briar baza podataka je nepovratno oštećena. Vaš račun, vaši podaci i svi kontakti su izgubljeni. Nažalost, morate reinstalirati Briar ili podesiti novi račun tako što ćete izabrati opciju \'ZaboraviIi ste lozinku?\' .</string>
<string name="startup_failed_data_too_old_error">Vaš račun je napravljen sa starom verzijom aplikacije i nemože se otvoriti novom verzijom. Morate reinstalirati staru verziju ili podesiti novi račun tako što ćete izabrati opciju \'ZaboraviIi ste lozinku?\' kod unosa lozinke.</string>
<string name="startup_failed_data_too_new_error">Ova verzija aplikacije je stara. Molim vas da aplikaciju ažurirate na posljednju verziju a zatim pokušate ponovo.</string>
<string name="startup_failed_service_error">Briar nije mogao da pokrene potreban plugin-dodatak. Reinstaliranje Briara obično riješi problem. Međutim, imajte na umu da ćete izgubiti vaš račun i sve podatke vezane za njega pošto Briar ne koristi centralne servere da sačuva vaše podatke.</string>
<string name="expiry_update">Krajnji datum testiranja je pomjeren. Vaš račun će sada isteći za %d dana.</string>
<string name="expiry_date_reached">Ovaj softver nije više aktuelan.\nHvala na testiranju!</string>
<string name="download_briar">Da bi nastavili koristiti Briar, molimo preuzmite verziju 1.0</string>
<string name="create_new_account">Morate kreirati novi račun, ali možete koristiti isto korisničko ime.</string>
<string name="download_briar_button">Preuzmite Briar 1.0</string>
<string name="startup_open_database">Dekriptujem bazu...</string>
<string name="startup_migrate_database">Ažuriram bazu...</string>
<string name="startup_compact_database">Baza se kompaktuje...</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Otvori navigacionu ladicu</string>
<string name="nav_drawer_close_description">Zatvori navigacionu ladicu</string>
<string name="contact_list_button">Kontakti</string>
<string name="groups_button">Privatne Grupe</string>
<string name="forums_button">Forumi</string>
<string name="blogs_button">Blogovi</string>
<!--This is part of the main menu. The app will be locked when this is tapped.-->
<string name="lock_button">Zaključaj aplikaciju</string>
<string name="settings_button">Podešavanja</string>
<string name="sign_out_button">Odjava</string>
<!--Transports-->
<string name="transport_tor">Internet</string>
<string name="transport_bt">Bluetooth</string>
<string name="transport_lan">Wi-Fi</string>
<!--Notifications-->
<string name="reminder_notification_title">Odjavljeni ste iz Briara</string>
<string name="reminder_notification_text">Dotaknite da se prijavite nazad.</string>
<string name="reminder_notification_channel_title">Briarov podsjetnik za prijavu</string>
<string name="reminder_notification_dismiss">Obustavi</string>
<string name="ongoing_notification_title">Prijavljeni ste u Briar</string>
<string name="ongoing_notification_text">Dodirnite da otvorite Briar.</string>
<!--Misc-->
<string name="now">sada</string>
<string name="show">Prikaži</string>
<string name="hide">Sakrij</string>
<string name="ok">OK</string>
<string name="cancel">Odustani</string>
<string name="got_it">Jasno</string>
<string name="delete">Obriši</string>
<string name="accept">Prihvati</string>
<string name="decline">Odbij</string>
<string name="options">Opcije</string>
<string name="online">Online</string>
<string name="offline">Offline</string>
<string name="send">Pošalji</string>
<string name="allow">Dozvoli</string>
<string name="open">Otvori</string>
<string name="no_data">Nema podataka</string>
<string name="ellipsis">...</string>
<string name="text_too_long">Tekst koji ste unijeli je predugačak</string>
<string name="show_onboarding">Otvori dijalog za Pomoć</string>
<string name="fix">Popravi</string>
<string name="help">Pomoć</string>
<string name="sorry">Izvini</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">Nema kontakata za prikazivanje</string>
<string name="no_contacts_action">Taknite + ikonu da dodate kontakt</string>
<string name="date_no_private_messages">Nema poruka.</string>
<string name="no_private_messages">Nema poruka za prikazivanje</string>
<string name="message_hint">Ukucajte poruku</string>
<string name="image_caption_hint">Dodaj naslov (opciono)</string>
<string name="image_attach">Priloži fotografiju</string>
<string name="image_attach_error">Nije moguće priložiti fotografiju/e</string>
<string name="image_attach_error_too_big">Fotografija je prevelika. Limit je %d MB.</string>
<string name="image_attach_error_invalid_mime_type">Fromat fotografije nije podržan: %s</string>
<string name="set_contact_alias">Promijeni ime kontakta</string>
<string name="set_contact_alias_hint">Ime kontakta</string>
<string name="set_alias_button">Promijeni</string>
<string name="delete_contact">Izbrišite kontakt</string>
<string name="dialog_title_delete_contact">Potvrdite brisanje kontakta</string>
<string name="dialog_message_delete_contact">Jeste li sigurni da želite da uklonite ovaj kontakt i sve poruke koje ste razmijenili?</string>
<string name="contact_deleted_toast">Kontakt je izbrisan</string>
<!--This is shown in the action bar when opening an image in fullscreen that the user sent-->
<string name="you">Vi</string>
<string name="save_image">Sačuvaj fotografiju</string>
<string name="dialog_title_save_image">Želite li sačuvati fotografiju?</string>
<string name="dialog_message_save_image">Čuvanje ove fotografije će omogućiti drugim aplikacijama da joj pristupe.\n\nJeste li sigurni da želite da je sačuvate?</string>
<string name="save_image_success">Fotografija je sačuvana</string>
<string name="save_image_error">Nije moguće sačuvati fotografiju</string>
<string name="dialog_title_no_image_support">Slike nisu dostupne</string>
<string name="dialog_message_no_image_support">Briar ne podržava prložene fotografije. Nakon nadogradnje vidjet ćete drugu ikonu.</string>
<string name="dialog_title_image_support">Sada možete slati fotografije ovom kontaktu</string>
<string name="dialog_message_image_support">Dodirni ovu ikonu da priložiš fotografije</string>
<!--Adding Contacts-->
<string name="add_contact_title">Dodaj kontakt u blizini vas</string>
<string name="face_to_face">Morate se sresti sa osobom koju želite da dodate kao kontakt.\n\nOvo će spriječiti bilo koga da se predstavi kao vi ili da ubuduće čita poruke.</string>
<string name="continue_button">Nastavi</string>
<string name="try_again_button">Probajte ponovo</string>
<string name="waiting_for_contact_to_scan">Čekam da kontakt skenira i poveže se\u2026</string>
<string name="exchanging_contact_details">Razmjenjujem detalje o kontaktu\u2026</string>
<string name="contact_added_toast">Dodani kontakt: %s</string>
<string name="contact_already_exists">Kontakt %s već postoji</string>
<string name="qr_code_invalid">QR kod nije validan</string>
<string name="qr_code_too_old">Skenirani QR kod pripada staroj verziji %s.\n\nMolimo vas da zatražite od vašeg kontakta da ažurira aplikaciju na posljednjiu verziju, a zatim pokušajte ponovo</string>
<string name="qr_code_too_new">QR kod koji skenirate pripada novijoj verziji %s.\n\nMolimo vas nadogradite aplikaciju na posljednju verziju, a zatim pokušajte ponovo.</string>
<string name="camera_error">Greska kamere</string>
<string name="connecting_to_device">Povezujem se sa uređajem\u2026</string>
<string name="authenticating_with_device">Autentikacija sa uređajem\u2026</string>
<string name="connection_error_title">Nije moguće povezivanje sa vašim kontaktom</string>
<string name="connection_error_explanation">Provjerite jeste li oboje povezani na ist Wi-Fi mrežu.</string>
<string name="connection_error_feedback">Ako se problem ponavlja, Molimo vas <a href="feedback">pošaljite povratne informacije</a> kako bi pomogli poboljšanju aplikacije.</string>
<!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">Dodaj udaljeni konktakt</string>
<string name="add_contact_nearby_title">Dodaj kontakt koji je u blizini</string>
<string name="add_contact_remotely_title">Dodaj udaljeni kontakt</string>
<string name="contact_name_hint">Dodjeli kontaktu nadimak</string>
<string name="contact_link_intro">Unesite link vašeg kontakta ovdje</string>
<string name="contact_link_hint">Kontaktov link</string>
<string name="paste_button">Zalepi</string>
<string name="add_contact_button">Dodaj kontakt</string>
<string name="copy_button">Kopiraj</string>
<string name="share_button">Podijeli</string>
<string name="send_link_title">Razmjena linkova</string>
<string name="add_contact_choose_nickname">Izaberi korisničko ime</string>
<string name="add_contact_choose_a_nickname">Unesi korisničko ime</string>
<string name="nickname_intro">Dodjeli vašem kontaktu nadimak. Samo vi možete vidjeti dodjeljeni nadimak.</string>
<string name="your_link">Dodjeli link kontaktu kojeg želite dodati</string>
<string name="link_clip_label">Briar link</string>
<string name="link_copied_toast">Link je kopiran</string>
<string name="adding_contact_error">Došlo je do greške tokom dodavanja kontakta.</string>
<string name="pending_contact_requests_snackbar">Postoje zahtjevi za kontaktima koji čekanju</string>
<string name="pending_contact_requests">Zahtjevi za kontaktima na čekanju</string>
<string name="no_pending_contacts">Nema Zahtjeva za kontaktima na čekanju</string>
<string name="add_contact_remote_connecting">Konektovanje…</string>
<string name="waiting_for_contact_to_come_online">Čekanje da kontakt bude online…</string>
<string name="connecting">Konektovanje…</string>
<string name="adding_contact">Dodavanje kontakta...</string>
<string name="adding_contact_failed">Dodavanje kontakta nije uspjelo</string>
<string name="dialog_title_remove_pending_contact">Potvrda Uklanjanja</string>
<string name="dialog_message_remove_pending_contact">Ovaj se kontakt još dodaje. Ako ga sada uklonite, neće biti dodan.</string>
<string name="own_link_error">Unesite link vašeg kontakta, a ne vaš vlastiti link.</string>
<string name="nickname_missing">Molim vas unesite korisničko ime</string>
<string name="invalid_link">Neispravan link</string>
<string name="unsupported_link">Ovaj link dolazi sa nove verzije Briar-a. Molim vas ažurirajte aplikaciju na posljednju verziju, a zatim pokušajte ponovo.</string>
<string name="intent_own_link">Otvorili ste vaš vlastiti link. Koristite link od kontakta kojeg želite dodati.</string>
<string name="missing_link">Molim vas unesite link</string>
<!--This is a numeral indicating the first step in a series of screens-->
<string name="step_1">1</string>
<!--This is a numeral indicating the second step in a series of screens-->
<string name="step_2">2</string>
<string name="adding_contact_slow_warning">Dodavanje ovog kontakta traje duže nego obično</string>
<string name="adding_contact_slow_title">Nije moguće povezivanje sa kontaktom</string>
<string name="adding_contact_slow_text">Dodavanje ovog kontakta traje duže nego obično.\n\nMolim vas provjerite da li je vaš kontakt dobio vaš link i dodao vas:</string>
<string name="offline_state">Nema konekcije na internet</string>
<string name="duplicate_link_dialog_title">Duliciran Link</string>
<string name="duplicate_link_dialog_text_1">Već imate kontakta na čekanju sa ovim linkom: %s</string>
<!--This is a question asking whether two nicknames refer to the same person-->
<string name="duplicate_link_dialog_text_2">Da li su %s i %s ista osoba?</string>
<!--This is a button for answering that two nicknames do indeed refer to the same person. This
string will be used in a dialog button, so if the translation of this string is longer than 20
characters, please use "Yes" instead, and use "No" for the "Different Person" button-->
<string name="same_person_button">Ista osoba</string>
<!--This is a button for answering that two nicknames refer to different people. This string
will be used in a dialog button, so if the translation of this string longer than 20 characters,
please use "No" instead, and use "Yes" for the "Same Person" button-->
<string name="different_person_button">Različita osoba</string>
<string name="duplicate_link_dialog_text_3">%s i %s poslsli su vam isti link.\n\nJedan od njih moguće da pokušava otkriti ko su vaši kontakti.\n\nNemojte im reći da ste primili isti link od druge osobe.</string>
<string name="pending_contact_updated_toast">Ažuriranje kontakta je na čekanju</string>
<!--Introductions-->
<string name="introduction_onboarding_title">Upoznajte vaše kontakte</string>
<string name="introduction_onboarding_text">Vi možete da upoznate vaše kontakte međusobno, nije potrebno da se oni sreću licem u lice kako bi se povezali na Briar-u.</string>
<string name="introduction_menu_item">Izvršite upoznavanje</string>
<string name="introduction_activity_title">Izaberite kontakt</string>
<string name="introduction_not_possible">Vi već imate započeto jedno upoznavanje ovim kontaktom. Molimo vas dozvolite da ono prvo završi. Ako ste vi ili vaš kontakt rijetko na vezi, ovo može da potraje.</string>
<string name="introduction_message_title">Upoznajte kontakte</string>
<string name="introduction_message_hint">Dodajte poruku (opciono)</string>
<string name="introduction_button">Izvršite upoznavanje</string>
<string name="introduction_sent">Vaše poziv na upoznavanje je poslat</string>
<string name="introduction_error">Došlo je do greške pri izvršenju upoznavanja.</string>
<string name="introduction_response_error">Greška pri odgovoru na upoznavanje</string>
<string name="introduction_request_sent">Tražili ste da se %1$s i %2$s upoznaju.</string>
<string name="introduction_request_received">%1$s je tražio da vas upozna sa %2$s. Da li želite da dodate %2$s u vašu listu kontakata?</string>
<string name="introduction_request_exists_received">%1$s je tražio da vas upozna sa %2$s, ali %2$s je već u vašoj listi kontakata. Pošto %1$s to možda ne zna, vi i dalje možete da odgovorite: </string>
<string name="introduction_request_answered_received">%1$s je tražio da vas upozna sa %2$s.</string>
<string name="introduction_response_accepted_sent">Prihvatili ste upoznavanje sa %1$s.</string>
<string name="introduction_response_accepted_sent_info">Prije nego što %1$s bude u vašim kontaktima, mora da prihvati i upoznavanje. Ovo može potrajati.</string>
<string name="introduction_response_declined_sent">Odbili ste upoznavanje sa %1$s.</string>
<string name="introduction_response_accepted_received">%1$s prihvata upoznavanje sa %2$s.</string>
<string name="introduction_response_declined_received">%1$s odbija upoznavanje sa %2$s.</string>
<string name="introduction_response_declined_received_by_introducee">%1$s kaže da je %2$s odbio-la upoznavanje.</string>
<!--Private Groups-->
<string name="groups_list_empty">Nema grupa za prikazivanje</string>
<string name="groups_list_empty_action">Dotaknite + ikonu da kreirate grupu, ili pitajte vaše kontakte da podijele grupe sa vama</string>
<string name="groups_created_by">Kreator je %s</string>
<string name="groups_group_is_empty">Grupa je prazna</string>
<string name="groups_group_is_dissolved">Ova grupa je rasformirana</string>
<string name="groups_remove">Ukloni</string>
<string name="groups_create_group_title">Kreiraj privatnu grupu</string>
<string name="groups_create_group_button">Kreiraj grupu</string>
<string name="groups_create_group_invitation_button">Pošalji poziv</string>
<string name="groups_create_group_hint">Izaberite ime za vašu privatnu grupu</string>
<string name="groups_invitation_sent">Grupna pozivnica je poslata</string>
<string name="groups_member_list">Lista članova</string>
<string name="groups_invite_members">Pošaljite poziv članovima</string>
<string name="groups_member_created_you">Vi ste kreirali grupu</string>
<string name="groups_member_created">%s je kreator grupe</string>
<string name="groups_member_joined_you">Pristupili ste grupi</string>
<string name="groups_member_joined">%s je pristupio-la grupi</string>
<string name="groups_leave">Napusti grupu</string>
<string name="groups_leave_dialog_title">Potvrdite napuštanje grupe</string>
<string name="groups_leave_dialog_message">Jeste li sigurni da želite da napustite grupu?</string>
<string name="groups_dissolve">Raspustite grupu</string>
<string name="groups_dissolve_dialog_title">Potvrdite raspuštanje grupe</string>
<string name="groups_dissolve_dialog_message">Jeste li sigurni da želite raspustiti ovu grupu?\n\nSvi ostali članovi neće moći nastaviti konverzaciju i možda neće primiti najnovije poruke.</string>
<string name="groups_dissolve_button">Raspusti</string>
<string name="groups_dissolved_dialog_title">Grupa je raspuštena</string>
<string name="groups_dissolved_dialog_message">Kreator je raspustio ovu grupu.\n\nNe možete više pisati poruke u grupi i možda nećete primiti poljednje poruke koje su napisane.</string>
<!--Private Group Invitations-->
<string name="groups_invitations_title">Grupni pozivi</string>
<string name="groups_invitations_invitation_sent">Pozvali ste %1$s da se pridruži grupi \"%2$s\".</string>
<string name="groups_invitations_invitation_received">%1$s je poslao-la poziv da pristupite grupi \"%2$s\".</string>
<string name="groups_invitations_joined">Pristupili ste grupi</string>
<string name="groups_invitations_declined">Grupni poziv je odbijen</string>
<string name="groups_invitations_response_accepted_sent">Prihvatili ste grupni poziv od %s.</string>
<string name="groups_invitations_response_declined_sent">Odbili ste grupni poziv od %s.</string>
<string name="groups_invitations_response_accepted_received">%s je prihvatio-la grupni poziv.</string>
<string name="groups_invitations_response_declined_received">1%s je odbio-la grupni poziv.</string>
<string name="sharing_status_groups">Samo kreator moze da pozove nove članove u grupu. Ispod su trenutni članovi grupe.</string>
<!--Private Groups Revealing Contacts-->
<string name="groups_reveal_contacts">Otkrijte kontakte</string>
<string name="groups_reveal_dialog_message">Možete odlučiti da otkrijete kontakte svim trenutnim i budućim članovima ove grupe.\n\nOtkrivanje kontakata čini konekciju ka članovima grupe bržom i pouzdanijom, jer možete da komunicirate sa otkrivenim kontaktima čak i kad kreator grupe nije na vezi.</string>
<string name="groups_reveal_visible">Veze kontakata su vidljive grupi</string>
<string name="groups_reveal_visible_revealed_by_us">Veze kontakata su vidljive grupi (vi ste je otkrili)</string>
<string name="groups_reveal_visible_revealed_by_contact">Veze kontakata su vidljive grupi (otkriveno od strane %s)</string>
<string name="groups_reveal_invisible">Veze kontakata nisu vidljive grupi</string>
<!--Forums-->
<string name="no_forums">Nema foruma za prikazivanje</string>
<string name="no_forums_action">Dotaknite + ikonu da kreirate forum, ili pitajte vaše kontakte da podijele forume sa vama</string>
<string name="create_forum_title">Kreiraj forum</string>
<string name="choose_forum_hint">Izaberite ime za vaš forum</string>
<string name="create_forum_button">Kreiraj forum</string>
<string name="forum_created_toast">Forum je kreiran</string>
<string name="no_forum_posts">Nema postova za prikazivanje</string>
<string name="no_posts">Nema postova</string>
<string name="forum_new_message_hint">Novi post</string>
<string name="forum_message_reply_hint">Novi odgovor</string>
<string name="btn_reply">Odgovor</string>
<string name="forum_leave">Napusti forum</string>
<string name="dialog_title_leave_forum">Potvrdite napuštanje foruma</string>
<string name="dialog_message_leave_forum">Jeste li sigurni da želite da napustite ovaj forum?\n\nKontakti sa kojima ste podijelili ovaj forum možda prestanu dobijati novosti.</string>
<string name="dialog_button_leave">Napusti</string>
<string name="forum_left_toast">Napušten forum</string>
<!--Forum Sharing-->
<string name="forum_share_button">Podijeli forum</string>
<string name="contacts_selected">Kontakti selektovani</string>
<string name="activity_share_toolbar_header">Izaberite kontakte</string>
<string name="no_contacts_selector">Nema kontakata za prikazivanje</string>
<string name="no_contacts_selector_action">Molimo vas vratite se ovdje nakon dodavanja kontakta</string>
<string name="forum_shared_snackbar">Forum je podijeljen sa selektovanim kontaktima</string>
<string name="forum_share_message">Dodajte poruku (opciono)</string>
<string name="forum_share_error">Došlo je do greške prilikom dijeljenja ovog foruma.</string>
<string name="forum_invitation_received">%1$s je podijelio-la forum \"%2$s\" sa vama.</string>
<string name="forum_invitation_sent">Podijelili ste forum \"%1$s\" sa %2$s.</string>
<string name="forum_invitations_title">Pozivnice za forum</string>
<string name="forum_invitation_exists">Već ste prihvatili poziv u ovaj forum.\n\nPrihvatanje dodatnih poziva će učiniti vašu vezu sa forumom bržom i pouzdanijom.</string>
<string name="forum_joined_toast">Forumu ste pristupili</string>
<string name="forum_declined_toast">Poziv odbijen</string>
<string name="shared_by_format">Podijelio-la %s</string>
<string name="forum_invitation_already_sharing">Već se dijeli</string>
<string name="forum_invitation_response_accepted_sent">Prihvatili ste poziv u forum od %s.</string>
<string name="forum_invitation_response_declined_sent">Odbili ste poziv u forum od %s.</string>
<string name="forum_invitation_response_accepted_received">1%s prihvata vaš poziv u forum.</string>
<string name="forum_invitation_response_declined_received">%s odbija vaš poziv u forum.</string>
<string name="sharing_status">Status dijeljenja</string>
<string name="sharing_status_forum">Bilo koji član foruma može forum podijeliti sa svojim kontaktima. Vi dijelite forum sa slijedećim kontaktima. Moguće je da ima drugih članova koje ne vidite.</string>
<string name="shared_with">Pdijeljeno sa %1$d (%2$d na vezi)</string>
<string name="nobody">Niko</string>
<!--Blogs-->
<string name="blogs_other_blog_empty_state">Nema postova za prikazivanje</string>
<string name="read_more">pročitaj više</string>
<string name="blogs_write_blog_post">Napiši Blog Post</string>
<string name="blogs_write_blog_post_body_hint">Ukucajte vaš blog post</string>
<string name="blogs_publish_blog_post">Objavi</string>
<string name="blogs_blog_post_created">Blog post je kreiran</string>
<string name="blogs_blog_post_received">Primljen je novi blog post</string>
<string name="blogs_blog_post_scroll_to">Skroluj do</string>
<string name="blogs_feed_empty_state">Nema postova za prikazivanje</string>
<string name="blogs_feed_empty_state_action">Postovi od vaših kontakata i blogova na koje se prijavite će se prikazivati ovdje.\n\nDotaknite ikonu olovke da napišete post</string>
<string name="blogs_remove_blog">Ukloni blog</string>
<string name="blogs_remove_blog_dialog_message">Jeste li sigurni da želite da uklonite blog?\n\nPostovi će biti uklonjeni sa vašeg uređaja ali ne is uređaja drugih ljudi.\n\nKontakti kojima ste podijelili ovaj blog će možda prestati da dobijaju novosti.</string>
<string name="blogs_remove_blog_ok">Ukloni</string>
<string name="blogs_blog_removed">Blog je uklonjen</string>
<string name="blogs_reblog_comment_hint">Dodajte komentar (opciono)</string>
<string name="blogs_reblog_button">Rebloguj</string>
<!--Blog Sharing-->
<string name="blogs_sharing_share">Podijeli blog</string>
<string name="blogs_sharing_error">Došlo je do greške pri podjeli bloga.</string>
<string name="blogs_sharing_button">Podijeli blog</string>
<string name="blogs_sharing_snackbar">Blog je podijeljen sa izabranim kontaktima</string>
<string name="blogs_sharing_response_accepted_sent">Prihvatili ste blog pozivnicu od %s.</string>
<string name="blogs_sharing_response_declined_sent">Odbili ste blog pozivnicu od %s.</string>
<string name="blogs_sharing_response_accepted_received">%s je prihvatio-la blog pozivnicu.</string>
<string name="blogs_sharing_response_declined_received">%s je odbio-la blog pozivnicu.</string>
<string name="blogs_sharing_invitation_received">%1$s je podijelio-la blog \"%2$s\" sa vama.</string>
<string name="blogs_sharing_invitation_sent">Podijelili ste blog \"%1$s\" sa %2$s.</string>
<string name="blogs_sharing_invitations_title">Blog Pozivnice</string>
<string name="blogs_sharing_joined_toast">Prijavljeni ste na blog</string>
<string name="blogs_sharing_declined_toast">Poziv odbijen</string>
<string name="sharing_status_blog">Bilo ko, ko se prijavi za ovaj blog, može blog da podijeli sa svojim kontaktima. Vi dijelite blog sa slijedećim kontaktima. Moguće je da ima još potpisnika koje nemožete da vidite.</string>
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import">Uvezi RSS kanal</string>
<string name="blogs_rss_feeds_import_button">Uvezi</string>
<string name="blogs_rss_feeds_import_hint">Unesi URL od RSS kanala</string>
<string name="blogs_rss_feeds_import_error">Žao nam je! Došlo je do greške pri unosu vašeg kanala.</string>
<string name="blogs_rss_feeds_manage">Upravljanje RSS kanalima</string>
<string name="blogs_rss_feeds_manage_imported">Uvezeno:</string>
<string name="blogs_rss_feeds_manage_author">Autor:</string>
<string name="blogs_rss_feeds_manage_updated">Zadnje ažuriranje:</string>
<string name="blogs_rss_remove_feed">Uklonite kanal</string>
<string name="blogs_rss_remove_feed_dialog_message">Jeste li sigurni da želite da uklonite kanal?\n\nPostovi će biti uklonjeni sa vašeg uređaja ali ne is uređaja drugih ljudi.\n\nKontakti kojima ste podijelili ovaj blog će možda prestati da dobijaju novosti.</string>
<string name="blogs_rss_remove_feed_ok">Ukloni</string>
<string name="blogs_rss_feeds_manage_delete_error">Kanal nije bilo moguće ukloniti!</string>
<string name="blogs_rss_feeds_manage_empty_state">Nema RSS kanala za prikazivanje\n\nDotaknite + ikonu da uvezete kanal</string>
<string name="blogs_rss_feeds_manage_error">Došlo je do problema pri učitavanju vaših kanala. Probajte opet kasnije.</string>
<!--Settings Display-->
<string name="pref_language_title">Jezik i regija</string>
<string name="pref_language_changed">Ovo podešavanje će stupiti na snagu kada restartujete Briar. Molim vas da se odjavite i restartujete Briar.</string>
<string name="pref_language_default">Podrazumjevane postavke sistema</string>
<string name="display_settings_title">Ekran</string>
<string name="pref_theme_title">Tema</string>
<string name="pref_theme_light">Svijetlost</string>
<string name="pref_theme_dark">Tamna</string>
<string name="pref_theme_auto">Automatski (Dnevno)</string>
<string name="pref_theme_system">Podrazumjevane postavke sistema</string>
<!--Settings Network-->
<string name="network_settings_title">Mreže</string>
<string name="bluetooth_setting">Povežite se preko Bluetooth-a</string>
<string name="bluetooth_setting_enabled">Kad god su kontakti blizu</string>
<string name="bluetooth_setting_disabled">Samo pri dodavanju kontakata</string>
<string name="tor_network_setting">Konektuj se preko Interneta (Tor)</string>
<string name="tor_network_setting_automatic">Automatski prema na lokaciji</string>
<string name="tor_network_setting_without_bridges">Koristi Tor bez mostova</string>
<string name="tor_network_setting_with_bridges">Koristi Tor sa mostovima</string>
<string name="tor_network_setting_never">Ne konektuj se</string>
<!--How and when Tor will connect after Automatic: E.g. Don't connect (in China) or Use Tor with bridges (in Belarus)-->
<string name="tor_network_setting_summary">Automatski: %1$s (za %2$s)</string>
<string name="tor_mobile_data_title">Koristi mobilinu mrežu</string>
<string name="tor_only_when_charging_title">Konektuj se preko Interneta (Tor) samo tokom punjenja uređaja</string>
<string name="tor_only_when_charging_summary">Onemogući interent konekciju kada se uređaj napaja samo sa baterijie</string>
<!--Settings Security and Panic-->
<string name="security_settings_title">Sigurnost</string>
<string name="pref_lock_title">Zaključavanje applikacije</string>
<string name="pref_lock_summary">Koristi zaključavanje ekrana uređaja da bi zaštitili Briar dok ste prijavljeni</string>
<string name="pref_lock_disabled_summary">Da bi koristili ovu mogućnost, podesite zaključavanje ekrana za vašem uređaju</string>
<string name="pref_lock_timeout_title">Zaključavanje aplikacije zbog neaktivnosti</string>
<!--The %s placeholder is replaced with the following time spans, e.g. 5 Minutes, 1 Hour-->
<string name="pref_lock_timeout_summary">Kada se ne koristi Briar, automatski zaključaj poslije %s</string>
<!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"-->
<string name="pref_lock_timeout_1">1 minute</string>
<!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"-->
<string name="pref_lock_timeout_5">5 minuta</string>
<!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"-->
<string name="pref_lock_timeout_15">15 minuta</string>
<!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"-->
<string name="pref_lock_timeout_30">30 minuta</string>
<!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"-->
<string name="pref_lock_timeout_60">1 sata</string>
<string name="pref_lock_timeout_never">Nikad</string>
<string name="pref_lock_timeout_never_summary">Ne zaključavaj Briar automatski</string>
<string name="change_password">Promijeni lozinku</string>
<string name="current_password">Trenutna lozinka</string>
<string name="choose_new_password">Nova lozinka</string>
<string name="confirm_new_password">Potvrdi novu lozinku</string>
<string name="password_changed">Lozinka je promijenjena</string>
<string name="panic_setting">Podešavanje Panik dugmeta</string>
<string name="panic_setting_title">Panik dugme</string>
<string name="panic_setting_hint">Podesite kako Briar reaguje kad koristite Panik dugme app</string>
<string name="panic_app_setting_title">Panik Dugne App</string>
<string name="unknown_app">nepoznata aplikacija</string>
<string name="panic_app_setting_summary">Nema postavljene aplikacije</string>
<string name="panic_app_setting_none">Ništa</string>
<string name="dialog_title_connect_panic_app">Potvrdi Panic App</string>
<string name="dialog_message_connect_panic_app">Jeste li sigurni da želite da dozvolite %1$s da pokrene destruktivne akcije panik dugmeta?</string>
<string name="panic_setting_destructive_action">Destruktivne akcije</string>
<string name="panic_setting_signout_title">Odjava</string>
<string name="panic_setting_signout_summary">Izloguj se iz Briar-a ako je pritisnuto panik dugme</string>
<string name="purge_setting_title">Izbriši račun</string>
<string name="purge_setting_summary">Izbriši Briar račun ako je panik dugme pritisnuto. Pažnja: Ovo će trajno izbrisati vaše identitete, kontakte i poruke</string>
<string name="uninstall_setting_title">Deinstaliraj Briar</string>
<string name="uninstall_setting_summary">Ovo zahtijeva ručnu potvrdu u slučaju panike</string>
<!--Settings Notifications-->
<string name="notification_settings_title">Obavještenja</string>
<string name="notify_sign_in_title">Podsjeti me da se prijavim</string>
<string name="notify_sign_in_summary">Prikazi podsjetnik kada se telefon uključi ili kada se aplikacija ažurira</string>
<string name="notify_private_messages_setting_title">Privatne poruke</string>
<string name="notify_private_messages_setting_summary">Prikaži upozorenja za privatne poruke</string>
<string name="notify_private_messages_setting_summary_26">Podesite upozorenja za privatne poruke</string>
<string name="notify_group_messages_setting_title">Grupne poruke</string>
<string name="notify_group_messages_setting_summary">Prikaži upozorenja za grupne poruke</string>
<string name="notify_group_messages_setting_summary_26">Podesite upozorenja za grupne poruke</string>
<string name="notify_forum_posts_setting_title">Forum postovi</string>
<string name="notify_forum_posts_setting_summary">Prikaži upozorenja za forum postove</string>
<string name="notify_forum_posts_setting_summary_26">Podesite upozorenja za forum postove</string>
<string name="notify_blog_posts_setting_title">Blog postovi</string>
<string name="notify_blog_posts_setting_summary">Prikaži upozorenja na blog postove</string>
<string name="notify_blog_posts_setting_summary_26">Podesite upozorenja za blog postove</string>
<string name="notify_vibration_setting">Vibriranje</string>
<string name="notify_sound_setting">Zvuk</string>
<string name="notify_sound_setting_default">Podrazumijevana melodija</string>
<string name="notify_sound_setting_disabled">Ništa</string>
<string name="choose_ringtone_title">Izaberi melodiju</string>
<string name="cannot_load_ringtone">Ne mogu otvoriti melodiju</string>
<!--Settings Feedback-->
<string name="feedback_settings_title">Povratne informacije</string>
<string name="send_feedback">Šalji povratne informacije</string>
<!--Link Warning-->
<string name="link_warning_title">Link upozorenja</string>
<string name="link_warning_intro">Upravo ćete otvoriti slijedeći link sa eksternom aplikacijom</string>
<string name="link_warning_text">Ovo se može koristiti da se otkrije vaš identitet. Razmislite o tome vjerujete li osobi koja vam je poslala ovaj link i razmislite o otvaranju s Tor Browser-om.</string>
<string name="link_warning_open_link">Otvori link</string>
<!--Crash Reporter-->
<string name="crash_report_title">Briar izvještaj o rušenju aplikacije</string>
<string name="briar_crashed">Izvinite, Briar aplikacija se srušila.</string>
<string name="not_your_fault">Greška nije do vas.</string>
<string name="please_send_report">Molimo pomozite nam da napravimo bolji Briar tako što ćete nam poslati izvještaj o rušenju aplikacije.</string>
<string name="report_is_encrypted">Obećavamo da je izvještaj kriptovan i sigurno poslat.</string>
<string name="feedback_title">Povratne informacije</string>
<string name="describe_crash">Opišite šta se desilo (opciono)</string>
<string name="enter_feedback">Unesite vašu povratnu informaciju</string>
<string name="optional_contact_email">Vaša email adresa (opciono)</string>
<string name="include_debug_report_crash">Uključite anonimne podatke o rušenju</string>
<string name="include_debug_report_feedback">Uključite anonimne podatke o ovom uređaju</string>
<string name="could_not_load_report_data">Nije bilo moguće učitati podatke izvještaja</string>
<string name="send_report">Pošalji izvještaj</string>
<string name="close">Zatvori</string>
<string name="dev_report_saved">Izvještaj je sačuvan. Biće poslat slijedeći put kada se ulogujete u Briar.</string>
<!--Sign Out-->
<string name="progress_title_logout">Odjavite se iz Briara...</string>
<!--Screen Filters & Tapjacking-->
<string name="screen_filter_title">Detektovano je prekrivanje ekrana</string>
<string name="screen_filter_body">Druga aplikacija precrtava preko Briara. Da bi vas zaštitio, Briar neće reagovati na dodir dok druga aplikacija precrtava preko ekrana.\n\nSlijedeće aplikacije mogu precrtavati preko:\n\n%1$s</string>
<string name="screen_filter_allow">Dozvoli ovim aplikacijama da precrtavaju ekran</string>
<!--Permission Requests-->
<string name="permission_camera_title">Dozvole kamere</string>
<string name="permission_camera_request_body">Da bi skenirao QR kod, Briaru je potreban pristup kameri.</string>
<string name="permission_location_title">Dozvole lokacije</string>
<string name="permission_location_request_body">Da bi otkrio Bluetooth uređaj, Brair treba douzvolu za pristup vašoj lokaciji.\n\nBriar neće snimati vašu lokaciju ili je dijeliti sa bilo kime.</string>
<string name="permission_camera_location_title">Kamera i locationkacija</string>
<string name="permission_camera_location_request_body">Da bi skenirao QR kod, Briar treba pristup vašoj kameri.\n\nDa bi otkrio Bluetooth uređaj, Brair treba douzvolu za pristup vašoj lokaciji.\n\nBriar neće snimati vašu lokaciju ili je dijeliti sa bilo kime.</string>
<string name="permission_camera_denied_body">Uskratili ste pristup kameri, ali dodavanje kontakata zahtijeva pristup kameri.\n\nMolim vas da uzmete u obzir odobravanja pristupa.</string>
<string name="qr_code">QR kod</string>
<string name="show_qr_code_fullscreen">Prikazi QR kod na puni ekran</string>
<!--App Locking-->
<string name="lock_unlock">Otključaj Briar</string>
<string name="lock_unlock_verbose">Unesite PIN uređaja, figuru ili lozinku</string>
<string name="lock_unlock_fingerprint_description">Dodirnite senzor otiska sa registrovanim prstom da bi nastavili</string>
<string name="lock_unlock_password">Koristi lozinku</string>
<string name="lock_is_locked">Briar je zaključan</string>
<string name="lock_tap_to_unlock">Dotakni da otključaš</string>
<!--Screenshots-->
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_alice">Fata</string>
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_bob">Mujo</string>
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_carol">Suljo</string>
<!--This is a message to be used in screenshots. Please use the same translation for Bob!-->
<string name="screenshot_message_1">Zdravo Mujo!</string>
<!--This is a message to be used in screenshots. Please use the same translation for Alice!-->
<string name="screenshot_message_2">Zdravo Fato! Hvala što mi govoriš o Briar-u!</string>
<!--This is a message to be used in screenshots.-->
<string name="screenshot_message_3">Nema problema, nadam se da ti se sviđa 😀</string>
</resources>

View File

@@ -48,7 +48,7 @@
<string name="download_briar_button">Descarrega Briar 1.0</string>
<string name="startup_open_database">S\'està desxifrant la base de dades...</string>
<string name="startup_migrate_database">S\'està actualitzant la base de dades...</string>
<string name="startup_compact_database">Compactant la base de dades...</string>
<string name="startup_compact_database">S\'està compactant la base de dades...</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Obre el calaix de navegació</string>
<string name="nav_drawer_close_description">Tanca el calaix de navegació</string>
@@ -116,9 +116,11 @@
<string name="date_no_private_messages">Sense missatges.</string>
<string name="no_private_messages">No hi ha cap missatge</string>
<string name="message_hint">Escriviu un missatge</string>
<string name="image_caption_hint">Afegeix una captura (opcional)</string>
<string name="image_caption_hint">Afegiu una nota (opcional)</string>
<string name="image_attach">Afegeix una imatge</string>
<string name="image_attach_error">La imatge no s\'ha pogut afegir </string>
<string name="image_attach_error">La imatge(s) no s\'ha pogut afegir </string>
<string name="image_attach_error_too_big">La imatge és massa gran. El límit és %d MB.</string>
<string name="image_attach_error_invalid_mime_type">El format de la imatge no s\'admet: %s</string>
<string name="set_contact_alias">Canvia el nom del contacte</string>
<string name="set_contact_alias_hint">Nom del contacte</string>
<string name="set_alias_button">Canvia</string>
@@ -127,18 +129,19 @@
<string name="dialog_message_delete_contact">Segur que voleu suprimir aquest contacte i tots els missatges que us heu intercanviat?</string>
<string name="contact_deleted_toast">S\'ha suprimit el contacte</string>
<!--This is shown in the action bar when opening an image in fullscreen that the user sent-->
<string name="you">Vos</string>
<string name="save_image">Desa imatge</string>
<string name="you">Vós</string>
<string name="save_image">Desa la imatge</string>
<string name="dialog_title_save_image">Voleu desar la imatge?</string>
<string name="dialog_message_save_image">Desant aquesta imatge es permetrà a altres aplicacions accedir-hi.\n\nConfirmeu que la voleu desar?</string>
<string name="dialog_message_save_image">Si deseu aquesta imatge la resta d\'aplicacions també hi podran accedir.\n\nSegur que voleu desar-la?</string>
<string name="save_image_success">La imatge s\'ha desat</string>
<string name="save_image_error">La imatge no s\'ha pogut desar </string>
<string name="dialog_title_no_image_support">Cap imatge disponible</string>
<string name="dialog_message_no_image_support">El vostre contacte de Briar encara no admet afegir imatges.
Un cop s\'actualitzi ja li veureu una icona diferent </string>
<string name="dialog_title_image_support">Ara podeu enviar imatges a aquest contacte</string>
<string name="dialog_title_no_image_support">No hi ha cap imatge disponible</string>
<string name="dialog_message_no_image_support">La versió de Briar del vostre contacte no permet afegir imatges.
Així que l\'actualitzi li veureu una icona diferent .</string>
<string name="dialog_title_image_support">Ara ja podeu enviar imatges a aquest contacte</string>
<string name="dialog_message_image_support">Premeu aquesta icona per afegir imatges.</string>
<!--Adding Contacts-->
<string name="add_contact_title">Afegeix un contacte proper</string>
<string name="face_to_face">Heu de coincidir en el mateix lloc amb la persona que voleu afegir com a contacte.\n\nD\'aquesta manera evitareu que algú suplanti les vostres identitats o pugui llegir els vostres missatges en el futur.</string>
<string name="continue_button">Continua</string>
<string name="try_again_button">Torna-ho a provar</string>
@@ -147,8 +150,8 @@ Un cop s\'actualitzi ja li veureu una icona diferent </string>
<string name="contact_added_toast">S\'ha afegit el contacte %s</string>
<string name="contact_already_exists">El contacte %s ja existia</string>
<string name="qr_code_invalid">El codi QR és invàlid</string>
<string name="qr_code_too_old">El codi QR que heu escanejat prové d\'una versió antiga de %s.\n\nDemaneu al vostre contacte que actualitzi a l\'última versió i torneu a provar-ho.</string>
<string name="qr_code_too_new">El codi QR que heu escanejat prové d\'una versió nova de %s.\n\nActualitzeu a l\'última versió i torneu a provar-ho.</string>
<string name="qr_code_too_old">El codi QR que heu escanejat prové d\'una versió antiga de %s.\n\nDemaneu al vostre contacte que l\'actualitzi i torneu a provar-ho.</string>
<string name="qr_code_too_new">El codi QR que heu escanejat prové d\'una versió de %s més nova que la vostra.\n\nActualitzeu la vostra aplicació i torneu a provar-ho.</string>
<string name="camera_error">Error de la càmera</string>
<string name="connecting_to_device">Connectant-se al dispositiu\u2026</string>
<string name="authenticating_with_device">Autenticant-se amb el dispositiu\u2026</string>
@@ -156,14 +159,66 @@ Un cop s\'actualitzi ja li veureu una icona diferent </string>
<string name="connection_error_explanation">Comproveu que esteu connectats a la mateixa xarxa Wi-Fi.</string>
<string name="connection_error_feedback">Si aquest problema persisteix, <a href="feedback">envieu-nos un comentari</a> per ajudar-nos a millorar l\'aplicació.</string>
<!--Adding Contacts Remotely-->
<string name="paste_button">Pegar</string>
<string name="add_contact_remotely_title_case">Afegeix un contacte llunyà</string>
<string name="add_contact_nearby_title">Afegeix un contacte proper</string>
<string name="add_contact_remotely_title">Afegeix un contacte llunyà</string>
<string name="contact_name_hint">Doneu un sobrenom al contacte</string>
<string name="contact_link_intro">Escriviu l\'enllaç del vostre contacte</string>
<string name="contact_link_hint">Enllaç del contacte</string>
<string name="paste_button">Enganxa</string>
<string name="add_contact_button">Afegiu un contacte</string>
<string name="copy_button">Copia</string>
<string name="share_button">Compartir</string>
<string name="share_button">Comparteix</string>
<string name="send_link_title">Intercanvieu els enllaços</string>
<string name="add_contact_choose_nickname">Trieu un sobrenom</string>
<string name="add_contact_choose_a_nickname">Escriviu un sobrenom</string>
<string name="nickname_intro">Doneu un sobrenom al contacte. Només vós podreu veure\'l.</string>
<string name="your_link">Lliureu aquest enllaç al contacte que voleu afegir</string>
<string name="link_clip_label">Enllaç de Briar</string>
<string name="link_copied_toast">L\'enllaç s\'ha copiat</string>
<string name="adding_contact_error">Hi ha hagut un error en afegir el contacte</string>
<string name="pending_contact_requests_snackbar">Hi ha sol·licituds de contacte pendents</string>
<string name="pending_contact_requests">Sol·licituds de contacte pendents</string>
<string name="no_pending_contacts">No hi ha sol·licituds de contacte pendents</string>
<string name="add_contact_remote_connecting">S\'està connectant...</string>
<string name="waiting_for_contact_to_come_online">S\'està esperant que el contacte estigui connectat…</string>
<string name="connecting">S\'està connectant...</string>
<string name="adding_contact">S\'està afegint el contacte…</string>
<string name="adding_contact_failed">No s\'ha pogut afegir el contacte</string>
<string name="dialog_title_remove_pending_contact">Confirmació de la supressió</string>
<string name="dialog_message_remove_pending_contact">Aquest contacte encara està essent afegit. Si el suprimiu ara ja no s\'acabarà d\'afegir.</string>
<string name="own_link_error">Escriviu l\'enllaç del vostre contacte i no el vostre enllaç</string>
<string name="nickname_missing">Escriviu un sobrenom</string>
<string name="invalid_link">Enllaç erroni</string>
<string name="unsupported_link">Aquest enllaç prové d\'una versió de Biar més nova que la vostra.\n\nActualitzeu la vostra aplicació i torneu a provar-ho.</string>
<string name="intent_own_link">Heu accedit al vostre propi enllaç. Feu servir el del contacte que voleu afegir!</string>
<string name="missing_link">Escriviu un enllaç</string>
<!--This is a numeral indicating the first step in a series of screens-->
<string name="step_1">1</string>
<!--This is a numeral indicating the second step in a series of screens-->
<string name="step_2">2</string>
<plurals name="contact_added_notification_text">
<item quantity="one">S\'ha afegit el nou contacte </item>
<item quantity="other">S\'han afegit %d nous contactes.</item>
</plurals>
<string name="adding_contact_slow_warning">Afegir aquest contacte està trigant més que de costum…</string>
<string name="adding_contact_slow_title">No pot connectar-se al contacte</string>
<string name="adding_contact_slow_text">Afegir aquest contacte està trigant més que de costum.\n\nComproveu que el contacte ha rebut el vostre enllaç i també us ha afegit.</string>
<string name="offline_state">No hi ha connexió a Internet</string>
<string name="duplicate_link_dialog_title">Enllaç duplicat</string>
<string name="duplicate_link_dialog_text_1">Teniu una sol·licitud de contacte pendent amb l\'enllaç %s</string>
<!--This is a question asking whether two nicknames refer to the same person-->
<string name="duplicate_link_dialog_text_2">%s i %s són la mateixa persona?</string>
<!--This is a button for answering that two nicknames do indeed refer to the same person. This
string will be used in a dialog button, so if the translation of this string is longer than 20
characters, please use "Yes" instead, and use "No" for the "Different Person" button-->
<string name="same_person_button">Mateixa persona</string>
<!--This is a button for answering that two nicknames refer to different people. This string
will be used in a dialog button, so if the translation of this string longer than 20 characters,
please use "No" instead, and use "Yes" for the "Same Person" button-->
<string name="different_person_button">Persones diferents</string>
<string name="duplicate_link_dialog_text_3">%s i %s us han enviat el mateix enllaç.\n\nUn d\'ells pot estar provant de descobrir quins són els vostres contactes.\n\nNo els hi digueu que heu rebut el mateix enllaç d\'algú altre.</string>
<string name="pending_contact_updated_toast">S\'ha actualitzat la sol·licitud de contacte pendent</string>
<!--Introductions-->
<string name="introduction_onboarding_title">Presenteu als vostres contactes</string>
<string name="introduction_onboarding_text">Podeu presentar als vostres contactes entre si, de manera que no necessitin trobar-se en persona per a relacionar-se a través de Briar.</string>
@@ -352,12 +407,12 @@ Un cop s\'actualitzi ja li veureu una icona diferent </string>
<string name="bluetooth_setting_enabled">Sempre que hi hagi contactes propers</string>
<string name="bluetooth_setting_disabled">Només quan s\'afegeixen contactes</string>
<string name="tor_network_setting">Connecta a través d\'Internet (Tor)</string>
<string name="tor_network_setting_automatic">Automàtic basat en la ubicació</string>
<string name="tor_network_setting_automatic">Automàtic, basat en la posició</string>
<string name="tor_network_setting_without_bridges">Usa Tor sense ponts</string>
<string name="tor_network_setting_with_bridges">Usa Tor amb ponts</string>
<string name="tor_network_setting_never">No et connectis</string>
<!--How and when Tor will connect after Automatic: E.g. Don't connect (in China) or Use Tor with bridges (in Belarus)-->
<string name="tor_network_setting_summary">Automàtic: %1$s (en %2$s)</string>
<string name="tor_network_setting_summary">Automàtic: %1$s (a %2$s)</string>
<string name="tor_mobile_data_title">Usa dades mòbils</string>
<string name="tor_only_when_charging_title">Connecta a través d\'Internet (Tor) només quan s\'estigui carregant</string>
<string name="tor_only_when_charging_summary">Desactiva la connexió a Internet quan el dispositiu estigui funcionant amb la bateria</string>
@@ -366,7 +421,7 @@ Un cop s\'actualitzi ja li veureu una icona diferent </string>
<string name="pref_lock_title">Bloqueig de l\'aplicació</string>
<string name="pref_lock_summary">Usa el bloqueig de pantalla del dispositiu per protegir Briar mentre inicieu la sessió</string>
<string name="pref_lock_disabled_summary">Per utilitzar aquesta funció, configureu el bloqueig de pantalla en el vostre dispositiu</string>
<string name="pref_lock_timeout_title">Temps d\'inactivitat per al bloqueig</string>
<string name="pref_lock_timeout_title">Temps d\'inactivitat abans de bloquejar-se</string>
<!--The %s placeholder is replaced with the following time spans, e.g. 5 Minutes, 1 Hour-->
<string name="pref_lock_timeout_summary">Briar es bloquejarà automàticament després de %s de no usar-se.</string>
<!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"-->
@@ -430,7 +485,7 @@ Un cop s\'actualitzi ja li veureu una icona diferent </string>
<!--Link Warning-->
<string name="link_warning_title">Avís d\'enllaç</string>
<string name="link_warning_intro">L\'enllaç s\'obrirà amb una aplicació externa.</string>
<string name="link_warning_text">Això es podria utilitzar-se per identificar-vos. Penseu si confieu en la persona que us ha enviat aquest enllaç i considereu obrir-lo amb el navegador Tor.</string>
<string name="link_warning_text">Això podria usar-se per a identificar-vos. Penseu si us en refieu prou de la persona que us ha enviat l\'enllaç. Avalueu si us convindria obrir-lo amb un navegador que faciliti l\'anonimat com Tor Browser.</string>
<string name="link_warning_open_link">Obre l\'enllaç</string>
<!--Crash Reporter-->
<string name="crash_report_title">Informe de fallida de Briar</string>
@@ -457,10 +512,10 @@ Un cop s\'actualitzi ja li veureu una icona diferent </string>
<!--Permission Requests-->
<string name="permission_camera_title">Permís de la càmera</string>
<string name="permission_camera_request_body">Per escanejar el codi QR, Briar necessita accedir a la càmera.</string>
<string name="permission_location_title">Permís d\'accés a l\'ubicació</string>
<string name="permission_location_request_body">Per trobar dispositius Bluetooth, Briar necessita la vostra ubicació.\n\nBriar no guarda la vostra ubicació ni la comparteix amb ningú.</string>
<string name="permission_camera_location_title">Permís d\'accés a la càmera i l\'ubicació</string>
<string name="permission_camera_location_request_body">Per escanejar el codi QR, Briar necessita accedir a la càmera.\n\nPer trobar dispositius Bluetooth, Briar necessita la vostra ubicació.\n\nBriar no guarda la vostra ubicació ni la comparteix amb ningú.</string>
<string name="permission_location_title">Permís d\'accés a la posició</string>
<string name="permission_location_request_body">Per a descobrir dispositius Bluetooth, Briar necessita accedir a la vostra posició.\n\nBriar no guarda la vostra posició ni la comparteix amb ningú.</string>
<string name="permission_camera_location_title">Permís d\'accés a la càmera i a la posició</string>
<string name="permission_camera_location_request_body">Per escanejar el codi QR, Briar necessita accedir a la càmera.\n\nPer trobar dispositius Bluetooth, Briar necessita accedir a la vostra posició.\n\nBriar no guarda la vostra posició ni la comparteix amb ningú.</string>
<string name="permission_camera_denied_body">Heu denegat l\'accés a la càmera però per afegir contactes cal utilitzar la càmera.\n\nRecomanem que permeteu l\'accés a la càmera.</string>
<string name="qr_code">Codi QR</string>
<string name="show_qr_code_fullscreen">Mostra el codi QR a pantalla completa</string>
@@ -473,15 +528,15 @@ Un cop s\'actualitzi ja li veureu una icona diferent </string>
<string name="lock_tap_to_unlock">Toqueu per desbloquejar-lo</string>
<!--Screenshots-->
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_alice">Alice</string>
<string name="screenshot_alice">Alba</string>
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_bob">Bob</string>
<string name="screenshot_bob">Pere</string>
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_carol">Carol</string>
<string name="screenshot_carol">Queralt</string>
<!--This is a message to be used in screenshots. Please use the same translation for Bob!-->
<string name="screenshot_message_1">Hola Bob!</string>
<string name="screenshot_message_1">Hola Pere!</string>
<!--This is a message to be used in screenshots. Please use the same translation for Alice!-->
<string name="screenshot_message_2">Hola Alice! Gràcies per recomanar-me Briar!</string>
<string name="screenshot_message_2">Hola Alba! Gràcies per recomanar-me Briar!</string>
<!--This is a message to be used in screenshots.-->
<string name="screenshot_message_3">De res, espero que t\'agradi 😀</string>
</resources>

View File

@@ -18,7 +18,7 @@
<string name="passwords_do_not_match">Passwörter stimmen nicht überein</string>
<string name="create_account_button">Konto anlegen</string>
<string name="more_info">Weitere Informationen</string>
<string name="don_t_ask_again">Frage nicht noch einmal</string>
<string name="don_t_ask_again">Nicht erneut nachfragen</string>
<string name="setup_huawei_text">Bitte tippe auf die Schaltfläche unten und stelle sicher, dass Briar unter \"Geschützte Apps\" angezeigt wird.</string>
<string name="setup_huawei_button">Briar schützen</string>
<string name="setup_huawei_help">Wenn Briar nicht zur Liste der geschützten Apps hinzugefügt wird, kann es nicht im Hintergrund ausgeführt werden.</string>
@@ -44,11 +44,11 @@
<string name="expiry_update">Das Ablaufdatum des Tests wurde verlängert. Dein Konto läuft nun in %d Tagen ab.</string>
<string name="expiry_date_reached">Diese Software ist abgelaufen.\nDanke, dass du Briar getestet hast!</string>
<string name="download_briar">Lade bitte Version 1.0 herunter, um Briar weiterhin zu nutzen.</string>
<string name="create_new_account">Du wirst ein neues Konto erstellen müssen, wobei du jedoch wieder den selben Benutzernamen verwenden kannst.</string>
<string name="create_new_account">Du wirst ein neues Konto erstellen müssen, wobei du jedoch wieder denselben Benutzernamen verwenden kannst.</string>
<string name="download_briar_button">Lade Briar 1.0 herunter</string>
<string name="startup_open_database">Datenbank wird entschlüsselt...</string>
<string name="startup_migrate_database">Datenbank wird aktualisiert...</string>
<string name="startup_compact_database">Datenbank komprimieren...</string>
<string name="startup_compact_database">Datenbank wird komprimiert ...</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Navigationsleiste öffnen</string>
<string name="nav_drawer_close_description">Navigationsleiste schliessen</string>
@@ -118,10 +118,17 @@
<string name="message_hint">Nachricht eingeben</string>
<string name="image_caption_hint">Überschrift hinzufügen (optional)</string>
<string name="image_attach">Bild anhängen</string>
<string name="image_attach_error">Konnte kein Bild anhängen</string>
<string name="image_attach_error">Bild(er) konnte(n) nicht angehängt werden</string>
<string name="image_attach_error_too_big">Bild zu groß. Das Limit beträgt %d MB.</string>
<string name="image_attach_error_invalid_mime_type">Nicht unterstütztes Bildformat: %s</string>
<string name="set_contact_alias">Kontaktnamen ändern</string>
<string name="set_contact_alias_hint">Name des Kontakts</string>
<string name="set_alias_button">Ändern</string>
<string name="delete_all_messages">Alle Nachrichten löschen</string>
<string name="dialog_title_delete_all_messages">Löschen der Nachrichten bestätigen</string>
<string name="dialog_message_delete_all_messages">Bist Du sicher, dass Du alle Nachrichten löschen willst?</string>
<string name="dialog_title_not_all_messages_deleted">Konnte nicht alle Nachrichten löschen</string>
<string name="dialog_message_not_all_messages_deleted">Nachrichten mit Bezug auf offene Kontaktempfehlungen oder Einladungen können erst nach Abschluss dieser gelöscht werden.</string>
<string name="delete_contact">Kontakt löschen</string>
<string name="dialog_title_delete_contact">Löschen des Kontakts bestätigen</string>
<string name="dialog_message_delete_contact">Bist du sicher, dass du diesen Kontakt und alle dazugehörigen Nachrichten löschen möchtest?</string>
@@ -131,13 +138,15 @@
<string name="save_image">Bild speichern</string>
<string name="dialog_title_save_image">Bild speichern?</string>
<string name="dialog_message_save_image">Gespeicherte Bilder können von vielen anderen Apps eingesehen werden.\n\nBist du sicher, dass du das Bild speichern möchtest?</string>
<string name="save_image_success">Bild wurde gespeichert.</string>
<string name="save_image_error">Konnte Bild nicht speichern.</string>
<string name="save_image_success">Bild wurde gespeichert</string>
<string name="save_image_error">Bild konnte nicht gespeichert werden</string>
<string name="dialog_title_no_image_support">Bilder nicht verfügbar</string>
<string name="dialog_message_no_image_support">Der Briar deines Kontakts unterstützt noch keine Bildanhänge. Sobald die Aktualisierung abgeschlossen ist, siehst du ein anderes Symbol.</string>
<string name="dialog_title_image_support">Du kannst nun Bilder an diesen Kontakt senden</string>
<string name="dialog_message_image_support">Tippe auf das Symbol, um Bilder anzuhängen.</string>
<string name="messaging_too_many_attachments_toast">Nur die ersten %d Bilder werden übertragen werden</string>
<!--Adding Contacts-->
<string name="add_contact_title">Kontakt in der Nähe hinzufügen</string>
<string name="face_to_face">Um einen neuen Kontakt hinzuzufügen, ist es notwendig, dass sich beide Kontakte an einem Ort treffen.\n\nDadurch wird betrügerische Identitätsvortäuschung und unautorisierter Kommunikationszugriff verhindert.</string>
<string name="continue_button">Weiter</string>
<string name="try_again_button">Noch einmal versuchen</string>
@@ -147,23 +156,29 @@
<string name="contact_already_exists">Kontakt %s existiert bereits</string>
<string name="qr_code_invalid">Der QR-Code ist ungültig</string>
<string name="qr_code_too_old">Der QR-Code, den du eingescannt hast, kommt von einer älteren Version von %s.\n\nBitte deinen Kontakt, zur neuesten Version upzudaten, und probiere es erneut.</string>
<string name="qr_code_too_new">Der QR-Code, den du gescannt hast, kommt von einer neueren Version von .\n\nBitte update %s auf die neueste Version und versuche es erneut.</string>
<string name="qr_code_too_new">Der QR-Code, den du gescannt hast, kommt von einer neueren Version.\n\nBitte aktualisiere %s auf die neueste Version und versuche es erneut.</string>
<string name="camera_error">Kamerafehler</string>
<string name="connecting_to_device">Verbinde mit Gerät\u2026</string>
<string name="authenticating_with_device">Authentifiziere Gerät\u2026</string>
<string name="connection_error_title">Keine Verbindung zum Kontakt</string>
<string name="connection_error_explanation">Überprüfe ob ihr beide mit dem selben Wi-Fi Netzwerk verbunden seit.</string>
<string name="connection_error_explanation">Überprüfe, ob ihr beide mit dem selben WLAN-Netzwerk verbunden seid.</string>
<string name="connection_error_feedback">Wenn das Problem weiterbesteht, hilf uns die App zu verbessern und <a href="feedback">schicke Feedback</a>.</string>
<!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">Kontakt aus der Ferne hinzufügen</string>
<string name="add_contact_nearby_title">Kontakt in der Nähe hinzufügen</string>
<string name="add_contact_remotely_title">Kontakt aus der Ferne hinzufügen</string>
<string name="contact_name_hint">Gib dem Kontakt einen Spitznamen</string>
<string name="contact_link_intro">Hier den Link deines Kontakts einfügen</string>
<string name="contact_link_hint">Kontakt Link</string>
<string name="paste_button">Einfügen</string>
<string name="add_contact_button">Kontakt hinzufügen</string>
<string name="copy_button">Kopieren</string>
<string name="share_button">Teilen</string>
<string name="send_link_title">Links austauschen</string>
<string name="add_contact_choose_nickname">Wähle einen Spitznamen</string>
<string name="add_contact_choose_a_nickname">Gib einen Spitznamen ein</string>
<string name="nickname_intro">Gib deinem Kontakt einen Spitznamen. Nur du kannst es sehen.</string>
<string name="nickname_intro">Gib deinem Kontakt einen Spitznamen. Nur du kannst ihn sehen.</string>
<string name="your_link">Gebe diesen Link zu dem Kontakt den du hinzufügen möchtest</string>
<string name="link_clip_label">Briar Link</string>
<string name="link_copied_toast">Link kopiert</string>
<string name="adding_contact_error">Es gab einen Fehler beim Hinzufügen des Kontaktes.</string>
@@ -171,10 +186,18 @@
<string name="pending_contact_requests">Ausstehende Kontaktanfragen</string>
<string name="no_pending_contacts">Keine ausstehenden Kontakte</string>
<string name="add_contact_remote_connecting">Verbindung wird aufgebaut …</string>
<string name="waiting_for_contact_to_come_online">Warte auf Online-Aktivität des Kontakts ...</string>
<string name="connecting">Verbindung wird aufgebaut …</string>
<string name="adding_contact">Kontakt hinzufügen...</string>
<string name="adding_contact_failed">Hinzufügen von Kontakt ist fehlgeschlagen</string>
<string name="dialog_title_remove_pending_contact">Entfernung bestätigen</string>
<string name="dialog_message_remove_pending_contact">Dieser Kontakt befindet sich noch beim Hinzufügen. Wenn er jetzt entfernt wird, wird das Hinzufügen abgebrochen.</string>
<string name="own_link_error">Gebe den Link deines Kontakts ein, nicht deinen eigenen.</string>
<string name="nickname_missing">Bitte gib einen Spitznamen an</string>
<string name="invalid_link">Ungültiger Link</string>
<string name="unsupported_link">Dieser Link kommt von einer neueren Version von Briar. Bitte führe eine Aktualisierung auf die aktuelle Version durch und versuche es dann nochmal. </string>
<string name="intent_own_link">Du hast deinen eigenen Link aufgerufen. Nutze einen Link von deinen Kontakten, die du hinzufügen möchtest!</string>
<string name="missing_link">Bitte gebe einen Link ein</string>
<!--This is a numeral indicating the first step in a series of screens-->
<string name="step_1">1</string>
<!--This is a numeral indicating the second step in a series of screens-->
@@ -183,6 +206,25 @@
<item quantity="one">Neuer Kontakt hinzugefügt.</item>
<item quantity="other">%d neue Kontakte hinzugefügt.</item>
</plurals>
<string name="adding_contact_slow_warning">Das Hinzufügen des Kontakts dauert länger als normal</string>
<string name="adding_contact_slow_title">Kann nicht zu Kontakt verbinden</string>
<string name="adding_contact_slow_text">Das Hinzufügen des Kontakts dauert länger als normal.\n\nBitte kontrolliere, dass dein Kontakt deinen Link erhalten und eingegeben hat:</string>
<string name="offline_state">Keine Internetverbindung</string>
<string name="duplicate_link_dialog_title">Link duplizieren</string>
<string name="duplicate_link_dialog_text_1">Du hast bereits einen Kontakt mit diesem Link ausstehend: %s</string>
<string name="duplicate_link_dialog_text_1_contact">Du hast bereits einen Kontakt mit diesem Link: %s</string>
<!--This is a question asking whether two nicknames refer to the same person-->
<string name="duplicate_link_dialog_text_2">Sind %s und %s dieselbe Person?</string>
<!--This is a button for answering that two nicknames do indeed refer to the same person. This
string will be used in a dialog button, so if the translation of this string is longer than 20
characters, please use "Yes" instead, and use "No" for the "Different Person" button-->
<string name="same_person_button">Selbe Person</string>
<!--This is a button for answering that two nicknames refer to different people. This string
will be used in a dialog button, so if the translation of this string longer than 20 characters,
please use "No" instead, and use "Yes" for the "Same Person" button-->
<string name="different_person_button">Andere Person</string>
<string name="duplicate_link_dialog_text_3">%s und %s haben Dir denselben Link geschickt.\n\nMöglicherweise versucht einer der beiden mehr über deine Kontakte zu erfahren.\n\nDu solltest deswegen niemandem sagen, dass Du diesen Link auch von jemand anderem erhalten hast.</string>
<string name="pending_contact_updated_toast">Ausstehender Kontakt aktualisiert</string>
<!--Introductions-->
<string name="introduction_onboarding_title">Mache deine Kontakte untereinander bekannt</string>
<string name="introduction_onboarding_text">Du kannst deine Kontakte untereinander bekannt machen. So können sie sich über Briar verbinden, ohne sich persönlich treffen zu müssen.</string>
@@ -291,7 +333,7 @@
<string name="forum_invitation_received">%1$s hat das Forum \"%2$s\" mit dir geteilt.</string>
<string name="forum_invitation_sent">Du hast das Forum \"%1$s\" mit %2$s geteilt.</string>
<string name="forum_invitations_title">Foreneinladungen</string>
<string name="forum_invitation_exists">Du hast bereits eine Einladung zu diesem Forum angenommen.\n\nMehrere Einladungen anzunehmen, wird deine Verbindung zu diesem Forum schneller und zuverlässiger machen.</string>
<string name="forum_invitation_exists">Du hast bereits eine Einladung zu diesem Forum angenommen.\n\nMehrere Einladungen anzunehmen wird deine Verbindung zu diesem Forum schneller und zuverlässiger machen.</string>
<string name="forum_joined_toast">Dem Forum beigetreten</string>
<string name="forum_declined_toast">Einladung abgelehnt</string>
<string name="shared_by_format">Geteilt durch %s</string>
@@ -372,8 +414,8 @@
<string name="bluetooth_setting_disabled">Nur beim Hinzufügen von Kontakten</string>
<string name="tor_network_setting">Über Internet (Tor) verbinden</string>
<string name="tor_network_setting_automatic">Automatisch (standortbasiert)</string>
<string name="tor_network_setting_without_bridges">Tor ohne Bridges benutzen</string>
<string name="tor_network_setting_with_bridges">Tor mit Bridges benutzen</string>
<string name="tor_network_setting_without_bridges">Tor ohne Bridges nutzen</string>
<string name="tor_network_setting_with_bridges">Tor mit Bridges nutzen</string>
<string name="tor_network_setting_never">Nicht verbinden</string>
<!--How and when Tor will connect after Automatic: E.g. Don't connect (in China) or Use Tor with bridges (in Belarus)-->
<string name="tor_network_setting_summary">Automatisch: %1$s (in %2$s)</string>
@@ -383,11 +425,11 @@
<!--Settings Security and Panic-->
<string name="security_settings_title">Sicherheit</string>
<string name="pref_lock_title">App-Sperre</string>
<string name="pref_lock_summary">Systemsperrbildschirm benutzen um Briar zu schützen, während angemeldet</string>
<string name="pref_lock_summary">Systemsperrbildschirm benutzen um ein angemeldetes Briar zu schützen</string>
<string name="pref_lock_disabled_summary">Um dieses Feature zu benutzen, aktiviere den Systemsperrbildschirm</string>
<string name="pref_lock_timeout_title">Zeitlimit der Inaktivität für die App-Sperr-Funktion</string>
<!--The %s placeholder is replaced with the following time spans, e.g. 5 Minutes, 1 Hour-->
<string name="pref_lock_timeout_summary">Briar automatisch nach %s sperren, wenn unbenutzt.</string>
<string name="pref_lock_timeout_summary">Bei Nichtnutzung Briar automatisch nach %s sperren</string>
<!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"-->
<string name="pref_lock_timeout_1">1 Minute</string>
<!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"-->
@@ -479,7 +521,7 @@
<string name="permission_location_title">Berechtigung Standort</string>
<string name="permission_location_request_body">Um Bluetooth-Geräte zu finden, braucht Briar Zugriff auf deinen Standort.\n\nBriar speichert weder deinen Standort noch gibt es ihn an andere weiter.</string>
<string name="permission_camera_location_title">Kamera und Standort</string>
<string name="permission_camera_location_request_body">Um den QR-Code zu scannen, brauch Briar Zugriff auf die Kamera.\n\nUm Bluetooth-Geräte zu finden, braucht Briar Zugriff auf deinen Standort.\n\nBriar speichert weder deinen Standort noch gibt es ihn an andere weiter.</string>
<string name="permission_camera_location_request_body">Um den QR-Code zu scannen, braucht Briar Zugriff auf die Kamera.\n\nUm Bluetooth-Geräte zu finden, braucht Briar Zugriff auf deinen Standort.\n\nBriar speichert weder deinen Standort noch gibt es ihn an andere weiter.</string>
<string name="permission_camera_denied_body">Du hast den Zugriff auf die Kamera verweigert, aber das Hinzufügen von Kontakten erfordert die Verwendung der Kamera.\n\nBitte gewähre den Zugriff.</string>
<string name="qr_code">QR-Code</string>
<string name="show_qr_code_fullscreen">QR-Code im Vollbildmodus anzeigen</string>

View File

@@ -118,10 +118,17 @@
<string name="message_hint">Escribe un mensaje</string>
<string name="image_caption_hint">Añade una breve descripción (opcional)</string>
<string name="image_attach">Adjuntar imagen</string>
<string name="image_attach_error">No se pudo adjuntar imagen</string>
<string name="image_attach_error">No se pudieron adjuntar las imágenes</string>
<string name="image_attach_error_too_big">Imagen demasiado grande. El límite es de %d MB.</string>
<string name="image_attach_error_invalid_mime_type">Formato de imagen no admitido: %s</string>
<string name="set_contact_alias">Cambiar nombre del contacto</string>
<string name="set_contact_alias_hint">Nombre del contacto</string>
<string name="set_alias_button">Cambiar</string>
<string name="delete_all_messages">Eliminar todos los mensajes</string>
<string name="dialog_title_delete_all_messages">Confirmar la eliminación del mensaje</string>
<string name="dialog_message_delete_all_messages">¿Estás seguro de que deseas eliminar todos los mensajes?</string>
<string name="dialog_title_not_all_messages_deleted">No se pudieron eliminar todos los mensajes.</string>
<string name="dialog_message_not_all_messages_deleted">Los mensajes relacionados con presentaciones o invitaciones en curso no se pueden eliminar hasta que finalicen.</string>
<string name="delete_contact">Eliminar contacto</string>
<string name="dialog_title_delete_contact">Confirmar eliminación de contacto</string>
<string name="dialog_message_delete_contact">¿Seguro que quieres eliminar este contacto y todos los mensajes intercambiados entre vosotros?</string>
@@ -137,7 +144,9 @@
<string name="dialog_message_no_image_support">El Briar de tu contacto todavía no soporta imágenes. Una vez que lo actualice verás un ícono diferente.</string>
<string name="dialog_title_image_support">Ahora puedes enviar imágenes a este contacto.</string>
<string name="dialog_message_image_support">Pulsa este ícono para adjuntar imágenes.</string>
<string name="messaging_too_many_attachments_toast">Solo se enviarán las primeras %d imágenes</string>
<!--Adding Contacts-->
<string name="add_contact_title">Agregar Contacto Cercano</string>
<string name="face_to_face">Debes reunirte con la persona a la que quieras añadir como contacto.\n\nHaciéndolo así prevendrás que nadie te suplante o pueda leer tus mensajes en el futuro.</string>
<string name="continue_button">Continuar</string>
<string name="try_again_button">Prueba de nuevo</string>
@@ -155,15 +164,67 @@
<string name="connection_error_explanation">Por favor comprobar que ambos estén conectados a la misma red Wi-Fi.</string>
<string name="connection_error_feedback">Si este problema persiste, por favor <a href="feedback">envía tus comentarios</a> para ayudarnos a mejorar la aplicación.</string>
<!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">Añadir un Contacto a Distancia</string>
<string name="add_contact_nearby_title">Agregar un contacto cercano</string>
<string name="add_contact_remotely_title">Añadir un contacto a distancia</string>
<string name="contact_name_hint">Ponerle un sobrenombre al contacto</string>
<string name="contact_link_intro">Introduzca el enlace de su contacto aquí</string>
<string name="contact_link_hint">Enlace de contacto</string>
<string name="paste_button">Pegar</string>
<string name="add_contact_button">Agregar contacto</string>
<string name="copy_button">Copiar</string>
<string name="share_button">Compartir</string>
<string name="send_link_title">Intercambiar enlaces</string>
<string name="add_contact_choose_nickname">Elija un sobrenombre</string>
<string name="add_contact_choose_a_nickname">Ingrese un sobrenombre</string>
<string name="nickname_intro">Dele a su contacto un sobrenombre. Sólo usted puede verlo.</string>
<string name="your_link">Déle este enlace al contacto que desea añadir</string>
<string name="link_clip_label">Enlace Briar</string>
<string name="link_copied_toast">Enlace copiado</string>
<string name="adding_contact_error">Se ha producido un error al añadir el contacto.</string>
<string name="pending_contact_requests_snackbar">Hay solicitudes de contacto pendientes</string>
<string name="pending_contact_requests">Solicitudes de contacto pendientes</string>
<string name="no_pending_contacts">No hay contactos pendientes</string>
<string name="add_contact_remote_connecting">Conectando...</string>
<string name="waiting_for_contact_to_come_online">Esperando que el contacto se ponga en línea....</string>
<string name="connecting">Conectando...</string>
<string name="adding_contact">Añadir contacto....</string>
<string name="adding_contact_failed">La adición de un contacto ha fallado</string>
<string name="dialog_title_remove_pending_contact">Confirmar la eliminación</string>
<string name="dialog_message_remove_pending_contact">Este contacto todavía está siendo añadido. Si lo quita ahora, no se añadirá.</string>
<string name="own_link_error">Introduzca el enlace de su contacto, no el suyo propio</string>
<string name="nickname_missing">Por favor, introduzca un sobrenombre</string>
<string name="invalid_link">Enlace inválido</string>
<string name="unsupported_link">Este enlace proviene de una nueva versión de Briar. Por favor, actualice a la última versión e inténtelo de nuevo.</string>
<string name="intent_own_link">Abrió su propio enlace. ¡Utilice el del contacto que quiere añadir!</string>
<string name="missing_link">Por favor, introduzca un enlace</string>
<!--This is a numeral indicating the first step in a series of screens-->
<string name="step_1">1</string>
<!--This is a numeral indicating the second step in a series of screens-->
<string name="step_2">2</string>
<plurals name="contact_added_notification_text">
<item quantity="one">Nuevo contacto añadido.</item>
<item quantity="other">%d nuevos contactos añadidos.</item>
</plurals>
<string name="adding_contact_slow_warning">Añadir este contacto tarda más de lo habitual</string>
<string name="adding_contact_slow_title">No se puede conectar con el contacto</string>
<string name="adding_contact_slow_text">Agregar este contacto está tomando más tiempo de lo normal.\n\nPor favor, compruebe que su contacto ha recibido su enlace y lo ha agregado:</string>
<string name="offline_state">No hay conexión a Internet</string>
<string name="duplicate_link_dialog_title">Duplicar enlace</string>
<string name="duplicate_link_dialog_text_1">Ya tiene un contacto pendiente con este enlace: %s</string>
<string name="duplicate_link_dialog_text_1_contact">Ya tienes un contacto con este enlace: %s</string>
<!--This is a question asking whether two nicknames refer to the same person-->
<string name="duplicate_link_dialog_text_2">¿Son %s y %s la misma persona?</string>
<!--This is a button for answering that two nicknames do indeed refer to the same person. This
string will be used in a dialog button, so if the translation of this string is longer than 20
characters, please use "Yes" instead, and use "No" for the "Different Person" button-->
<string name="same_person_button">Misma persona</string>
<!--This is a button for answering that two nicknames refer to different people. This string
will be used in a dialog button, so if the translation of this string longer than 20 characters,
please use "No" instead, and use "Yes" for the "Same Person" button-->
<string name="different_person_button">Diferente Persona</string>
<string name="duplicate_link_dialog_text_3">%s y %s le envió el mismo enlace.\n\nUno de ellos puede estar tratando de descubrir quiénes son sus contactos.\n\nNo les diga que recibió el mismo enlace de otra persona.</string>
<string name="pending_contact_updated_toast">Contacto pendiente actualizado</string>
<!--Introductions-->
<string name="introduction_onboarding_title">Presenta a tus contactos</string>
<string name="introduction_onboarding_text">Presenta a tus contactos entre sí para ahorrarles encontrarse en persona para poder conectar mediante Briar.</string>

Some files were not shown because too many files have changed in this diff Show More