Compare commits

..

132 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
c955466bda Load missing attachments when they arrive. 2019-06-19 12:47:18 +01:00
akwizgran
593a0c4632 Improve handling of missing and invalid attachments. 2019-06-19 11:23:57 +01:00
akwizgran
ed20b2d8d6 Use attachment header to retrieve attachment. 2019-06-19 10:57:13 +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
173 changed files with 9024 additions and 1782 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,15 +47,17 @@ 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 {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(smallKitten);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(msgId, item.getMessageId());
assertEquals(160, item.getWidth());
assertEquals(240, item.getHeight());
@@ -70,8 +72,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testBigJpegImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(originalKitten);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(msgId, item.getMessageId());
assertEquals(1728, item.getWidth());
assertEquals(2592, item.getHeight());
@@ -86,8 +88,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testSmallPngImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/png");
InputStream is = getUrlInputStream(pngKitten);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(msgId, item.getMessageId());
assertEquals(737, item.getWidth());
assertEquals(510, item.getHeight());
@@ -102,8 +104,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testUberGif() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(uberGif);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1, item.getWidth());
assertEquals(1, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
@@ -117,8 +119,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testLottaPixels() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(lottaPixel);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(64250, item.getWidth());
assertEquals(64250, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -132,8 +134,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testImageIoCrash() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(imageIoCrash);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1184, item.getWidth());
assertEquals(448, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -147,8 +149,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testGimpCrash() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(gimpCrash);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1, item.getWidth());
assertEquals(1, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
@@ -162,8 +164,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testOptiPngAfl() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(optiPngAfl);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(32, item.getWidth());
assertEquals(32, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
@@ -177,8 +179,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testLibrawError() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(librawError);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertTrue(item.hasError());
}
@@ -186,8 +188,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testSmallAnimatedGifMaxDimensions() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("animated.gif");
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(65535, item.getWidth());
assertEquals(65535, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -201,8 +203,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testSmallAnimatedGifHugeDimensions() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("animated2.gif");
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(10000, item.getWidth());
assertEquals(10000, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -216,8 +218,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testSmallGifLargeDimensions() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("error_large.gif");
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(16384, item.getWidth());
assertEquals(16384, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -231,8 +233,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testHighError() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("error_high.jpg");
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1, item.getWidth());
assertEquals(10000, item.getHeight());
assertEquals(dimensions.minWidth, item.getThumbnailWidth());
@@ -246,8 +248,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testWideError() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("error_wide.jpg");
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1920, item.getWidth());
assertEquals(1, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());

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

@@ -239,11 +239,11 @@ public class AppModule {
@Override
public boolean shouldEnableImageAttachments() {
return false;
return IS_DEBUG_BUILD;
}
@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(h, 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

@@ -68,7 +68,7 @@ public class AttachmentItem implements Parcelable {
header = new AttachmentHeader(messageId, mimeType);
}
AttachmentHeader getHeader() {
public AttachmentHeader getHeader() {
return header;
}

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,276 +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.Pair;
import org.briarproject.bramble.api.db.DatabaseExecutor;
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.ArrayList;
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.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
@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);
@DatabaseExecutor
public List<Pair<AttachmentHeader, Attachment>> getMessageAttachments(
List<AttachmentHeader> headers) throws DbException {
long start = now();
List<Pair<AttachmentHeader, Attachment>> attachments =
new ArrayList<>(headers.size());
for (AttachmentHeader h : headers) {
Attachment a = messagingManager.getAttachment(h.getMessageId());
attachments.add(new Pair<>(h, a));
}
logDuration(LOG, "Loading attachments", start);
return attachments;
}
@DatabaseExecutor
Attachment getMessageAttachment(AttachmentHeader h) throws DbException {
return messagingManager.getAttachment(h.getMessageId());
}
/**
* Creates {@link AttachmentItem}s from the passed headers and Attachments.
* <p>
* Note: This closes the {@link Attachment}'s {@link InputStream}.
*/
public List<AttachmentItem> getAttachmentItems(
List<Pair<AttachmentHeader, Attachment>> attachments) {
boolean needsSize = attachments.size() == 1;
List<AttachmentItem> items = new ArrayList<>(attachments.size());
for (Pair<AttachmentHeader, Attachment> a : attachments) {
AttachmentItem item =
getAttachmentItem(a.getFirst(), a.getSecond(), needsSize);
items.add(item);
}
return items;
}
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.
*/
AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a,
boolean needsSize) {
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

@@ -37,6 +37,7 @@ import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.db.NoSuchMessageException;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
@@ -81,6 +82,7 @@ import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
import java.util.ArrayList;
@@ -107,10 +109,12 @@ import static android.support.v7.util.SortedList.INVALID_POSITION;
import static android.view.Gravity.RIGHT;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Collections.sort;
import static java.util.Objects.requireNonNull;
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.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
@@ -138,7 +142,7 @@ public class ConversationActivity extends BriarActivity
public static final String CONTACT_ID = "briar.CONTACT_ID";
private static final Logger LOG =
Logger.getLogger(ConversationActivity.class.getName());
getLogger(ConversationActivity.class.getName());
private static final int TRANSITION_DURATION_MS = 500;
private static final int ONBOARDING_DELAY_MS = 250;
@@ -171,6 +175,8 @@ public class ConversationActivity extends BriarActivity
volatile GroupInvitationManager groupInvitationManager;
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
private final Map<MessageId, PrivateMessageHeader> missingAttachments =
new ConcurrentHashMap<>();
private final Observer<String> contactNameObserver = name -> {
requireNonNull(name);
loadMessages();
@@ -270,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() {
@@ -350,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) {
@@ -383,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;
@@ -434,29 +448,40 @@ public class ConversationActivity extends BriarActivity
});
}
private void eagerlyLoadMessageSize(PrivateMessageHeader h)
throws DbException {
MessageId id = h.getId();
// If the message has text, load it
if (h.hasText()) {
String text = textCache.get(id);
if (text == null) {
LOG.info("Eagerly loading text for latest message");
text = messagingManager.getMessageText(id);
textCache.put(id, requireNonNull(text));
private void eagerlyLoadMessageSize(PrivateMessageHeader h) {
try {
MessageId id = h.getId();
// If the message has text, load it
if (h.hasText()) {
String text = textCache.get(id);
if (text == null) {
LOG.info("Eagerly loading text for latest message");
text = messagingManager.getMessageText(id);
textCache.put(id, requireNonNull(text));
}
}
}
// If the message has a single image, load its size - for multiple
// images we use a grid so the size is fixed
if (h.getAttachmentHeaders().size() == 1) {
List<AttachmentItem> items = attachmentRetriever.cacheGet(id);
if (items == null) {
LOG.info("Eagerly loading image size for latest message");
items = attachmentRetriever.getAttachmentItems(
attachmentRetriever.getMessageAttachments(
h.getAttachmentHeaders()));
attachmentRetriever.cachePut(id, items);
// If the message has a single image, load its size - for multiple
// images we use a grid so the size is fixed
List<AttachmentHeader> headers = h.getAttachmentHeaders();
if (headers.size() == 1) {
List<AttachmentItem> items = attachmentRetriever.cacheGet(id);
if (items == null) {
LOG.info("Eagerly loading image size for latest message");
AttachmentHeader header = headers.get(0);
try {
Attachment a = attachmentRetriever
.getMessageAttachment(header);
AttachmentItem item =
attachmentRetriever.getAttachmentItem(a, true);
attachmentRetriever.cachePut(id, singletonList(item));
} catch (NoSuchMessageException e) {
LOG.info("Attachment not received yet");
missingAttachments.put(header.getMessageId(), h);
}
}
}
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
@@ -534,16 +559,30 @@ public class ConversationActivity extends BriarActivity
&& adapter.isScrolledToBottom(layoutManager);
}
private void loadMessageAttachments(MessageId messageId,
List<AttachmentHeader> headers) {
private void loadMessageAttachments(PrivateMessageHeader h) {
// TODO: Use placeholders for missing/invalid attachments
runOnDbThread(() -> {
try {
List<Pair<AttachmentHeader, Attachment>> attachments =
attachmentRetriever.getMessageAttachments(headers);
// TODO move getting the items off to IoExecutor, if size == 1
List<AttachmentItem> items =
attachmentRetriever.getAttachmentItems(attachments);
displayMessageAttachments(messageId, items);
List<AttachmentHeader> headers = h.getAttachmentHeaders();
boolean needsSize = headers.size() == 1;
List<AttachmentItem> items = new ArrayList<>(headers.size());
for (AttachmentHeader header : headers) {
try {
Attachment a = attachmentRetriever
.getMessageAttachment(header);
AttachmentItem item = attachmentRetriever
.getAttachmentItem(a, needsSize);
items.add(item);
} catch (NoSuchMessageException e) {
LOG.info("Attachment not received yet");
missingAttachments.put(header.getMessageId(), h);
return;
}
}
// Don't cache items unless all are present and valid
attachmentRetriever.cachePut(h.getId(), items);
displayMessageAttachments(h.getId(), items);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
@@ -553,7 +592,6 @@ public class ConversationActivity extends BriarActivity
private void displayMessageAttachments(MessageId m,
List<AttachmentItem> items) {
runOnUiThreadUnlessDestroyed(() -> {
attachmentRetriever.cachePut(m, items);
Pair<Integer, ConversationMessageItem> pair =
adapter.getMessageItem(m);
if (pair != null) {
@@ -567,6 +605,13 @@ public class ConversationActivity extends BriarActivity
@Override
public void eventOccurred(Event e) {
if (e instanceof AttachmentReceivedEvent) {
AttachmentReceivedEvent a = (AttachmentReceivedEvent) e;
if (a.getContactId().equals(contactId)) {
LOG.info("Attachment received");
onAttachmentReceived(a.getMessageId());
}
}
if (e instanceof ContactRemovedEvent) {
ContactRemovedEvent c = (ContactRemovedEvent) e;
if (c.getContactId().equals(contactId)) {
@@ -617,6 +662,15 @@ public class ConversationActivity extends BriarActivity
scrollToBottom();
}
@UiThread
private void onAttachmentReceived(MessageId attachmentId) {
PrivateMessageHeader h = missingAttachments.remove(attachmentId);
if (h != null) {
LOG.info("Missing attachment received");
loadMessageAttachments(h);
}
}
@UiThread
private void onNewConversationMessage(ConversationMessageHeader h) {
if (h instanceof ConversationRequest ||
@@ -681,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();
@@ -903,11 +1003,11 @@ public class ConversationActivity extends BriarActivity
}
@Override
public List<AttachmentItem> getAttachmentItems(MessageId m,
List<AttachmentHeader> headers) {
List<AttachmentItem> attachments = attachmentRetriever.cacheGet(m);
public List<AttachmentItem> getAttachmentItems(PrivateMessageHeader h) {
List<AttachmentItem> attachments =
attachmentRetriever.cacheGet(h.getId());
if (attachments == null) {
loadMessageAttachments(m, headers);
loadMessageAttachments(h);
return emptyList();
}
return attachments;

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

@@ -15,7 +15,6 @@ import org.briarproject.briar.api.forum.ForumInvitationRequest;
import org.briarproject.briar.api.forum.ForumInvitationResponse;
import org.briarproject.briar.api.introduction.IntroductionRequest;
import org.briarproject.briar.api.introduction.IntroductionResponse;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse;
@@ -56,8 +55,7 @@ class ConversationVisitor implements
if (h.getAttachmentHeaders().isEmpty()) {
attachments = emptyList();
} else {
attachments = attachmentCache
.getAttachmentItems(h.getId(), h.getAttachmentHeaders());
attachments = attachmentCache.getAttachmentItems(h);
}
if (h.isLocal()) {
item = new ConversationMessageItem(
@@ -295,7 +293,6 @@ class ConversationVisitor implements
}
interface AttachmentCache {
List<AttachmentItem> getAttachmentItems(MessageId m,
List<AttachmentHeader> headers);
List<AttachmentItem> getAttachmentItems(PrivateMessageHeader h);
}
}

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

@@ -12,7 +12,6 @@ import org.briarproject.bramble.api.db.DatabaseExecutor;
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.MessageId;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
@@ -135,10 +134,10 @@ public class ImageViewModel extends AndroidViewModel {
private void saveImage(AttachmentItem attachment, OutputStreamProvider osp,
@Nullable Runnable afterCopy) {
MessageId messageId = attachment.getMessageId();
dbExecutor.execute(() -> {
try {
Attachment a = messagingManager.getAttachment(messageId);
Attachment a =
messagingManager.getAttachment(attachment.getHeader());
copyImageFromDb(a, osp, afterCopy);
} catch (DbException e) {
logException(LOG, WARNING, e);

View File

@@ -9,8 +9,8 @@ import com.bumptech.glide.load.data.DataFetcher;
import org.briarproject.bramble.api.db.DatabaseExecutor;
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.AttachmentItem;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.MessagingManager;
import java.io.InputStream;
@@ -50,11 +50,12 @@ class BriarDataFetcher implements DataFetcher<InputStream> {
@Override
public void loadData(Priority priority,
DataCallback<? super InputStream> callback) {
MessageId id = attachment.getMessageId();
dbExecutor.execute(() -> {
if (cancel) return;
try {
inputStream = messagingManager.getAttachment(id).getStream();
Attachment a =
messagingManager.getAttachment(attachment.getHeader());
inputStream = a.getStream();
callback.onDataReady(inputStream);
} catch (DbException e) {
callback.onLoadFailed(e);

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>

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