Compare commits

..

110 Commits

Author SHA1 Message Date
akwizgran
b4d0a221a3 Add executor with high and low priority queues. 2019-02-14 14:51:31 +00:00
Torsten Grote
b342759e06 Merge branch '978-tor-only-on-battery' into 'master'
Add a setting to disable Tor when running on battery

Closes #978

See merge request briar/briar!1032
2019-02-06 14:46:33 +00:00
akwizgran
93d99b0111 Tweak wording of Tor battery setting. 2019-02-06 14:23:15 +00:00
akwizgran
61e8d576d2 Update mobile data log message, simplify logic. 2019-02-06 14:20:04 +00:00
Torsten Grote
75c37a258e Add a setting to disable Tor when running on battery 2019-02-05 13:46:26 -02:00
akwizgran
e964dae64b Merge branch '1468-image-size-tests' into 'master'
Add tests for parsing image sizes

See merge request briar/briar!1026
2019-01-15 17:26:55 +00:00
akwizgran
986d884b40 Refactor ImageManager to ImageHelper. 2019-01-15 17:14:57 +00:00
akwizgran
9557afabc6 Change MIME types to "image/jpeg", unsuppress warning. 2019-01-15 16:49:18 +00:00
Torsten Grote
ebe6b0d4c0 [android] Split up AttachmentController tests into integration and unit 2019-01-15 16:33:03 +00:00
Torsten Grote
6e83fb7aef [android] add tests for getting attachment items from AttachmentController 2019-01-15 16:33:00 +00:00
Torsten Grote
7a5ec2af12 [android] Add test for MarkEnforcingInputStream 2019-01-15 16:32:23 +00:00
akwizgran
ce1fde496c Merge branch '1477-check-attachment-support' into 'master'
Find out if contacts support image attachments and enable them

Closes #1477

See merge request briar/briar!1019
2019-01-15 15:35:48 +00:00
akwizgran
4b62c51fbf Revert to using a fixed delay for the onboarding. 2019-01-15 15:23:30 +00:00
akwizgran
226ed3dd73 Wrap long line, remove redundant variable. 2019-01-14 14:31:31 +00:00
akwizgran
ab07dfb32c Use expression lambda. 2019-01-14 14:26:09 +00:00
akwizgran
20c51c1aa4 Group together fields with the same access restrictions. 2019-01-14 14:25:32 +00:00
Torsten Grote
232c2129a7 [android] use a LiveData in ConversationActivity to get notified when transition ended 2019-01-14 14:22:31 +00:00
Torsten Grote
3620edbfc9 [android] set a transition animation duration for ConversationActivity
so we know better for how long to delay the onboarding dialogs
2019-01-14 14:21:34 +00:00
Torsten Grote
ad71d69149 Create and use method in MessagingManager for checking for image support 2019-01-14 14:21:33 +00:00
Torsten Grote
f73f8ca7e7 [android] do not show two private conversation onboardings at the same time
Checking for introduction onboarding is now done in the ViewModel
together with the image onboarding. The latter has preference. If both
could be shown, the introduction onboarding will be delayed to the next
time the user enters the conversation.
2019-01-14 14:21:33 +00:00
Torsten Grote
16c701a71a [android] only enable image feature if contact supports it
Also show an onboarding the first time, the feature gets activiated
2019-01-14 14:21:19 +00:00
akwizgran
8183b7b26a Merge branch '1469-hide-ui-without-flashing' into 'master'
Hide UI without flashing

Closes #1469

See merge request briar/briar!1030
2019-01-11 17:22:04 +00:00
akwizgran
bd48c97eab Merge branch 'upgrade-jackson-2.9.8' into 'master'
Upgrade Jackson to 2.9.8

See merge request briar/briar!1031
2019-01-11 17:07:07 +00:00
akwizgran
925dc29a1f Merge branch 'hide-ui-api-15' into 'master'
Improve UI hiding behaviour

See merge request briar/briar!1029
2019-01-11 17:03:24 +00:00
akwizgran
91777fd942 Hide UI without flashing. 2019-01-11 16:59:53 +00:00
akwizgran
fbce8f81c7 Merge branch '1475-transition-name' into 'master'
Use a unique transition name for each AttachmentItem

See merge request briar/briar!1028
2019-01-11 16:54:24 +00:00
akwizgran
d7c72c4d68 Use a unique transition name for each AttachmentItem. 2019-01-11 16:45:20 +00:00
akwizgran
4faf535801 Reduce visibility. 2019-01-11 16:45:20 +00:00
akwizgran
526ef7c6d8 Add array entries for new translations. 2019-01-11 15:13:06 +00:00
akwizgran
798dff1a03 Update translations, add Macedonian and Ukrainian. 2019-01-11 12:18:15 +00:00
akwizgran
a4336776c9 Merge branch '1475-image-transitions' into 'master'
Resolve main issues with image transition animation

See merge request briar/briar!1016
2019-01-09 15:01:29 +00:00
akwizgran
418451cbd9 Use consistent conditions to decide whether to scroll. 2019-01-09 14:30:57 +00:00
akwizgran
045fcfc5fa Remove translucent window effect. 2019-01-09 14:30:57 +00:00
Torsten Grote
ef998577db [android] add nullability annotations to ImageActivity 2019-01-09 14:30:57 +00:00
Torsten Grote
a53345a3c9 [android] scroll down when new messages arrive while conversation is visible
Also shows new message notification when ConversationActivity is paused
2019-01-09 14:30:56 +00:00
Torsten Grote
ed8c09282d [android] enable image shared element transition for API 21+22
There's an Android framework bug (#224270) on these APIs that causes a NPE
when the shared element is not visible anymore when returning.
Since we know restore the list position, the shared element should be
visible and thus not produce NPEs anymore.
2019-01-09 14:30:56 +00:00
Torsten Grote
42197b5b5c [android] Fix enter transition to fullscreen ImageActivity 2019-01-09 14:30:56 +00:00
Torsten Grote
374fc7035b [android] Save and restore list position of conversation across restarts 2019-01-09 14:30:55 +00:00
akwizgran
9b796c7cc3 Merge branch '1438-send-image-attachments-multiple' into 'master'
UX for sending multiple image attachments

See merge request briar/briar!1015
2019-01-04 17:04:43 +00:00
akwizgran
532edff642 Minor code cleanups. 2019-01-04 16:55:29 +00:00
akwizgran
6857252471 Merge branch '1480-window-background' into 'master'
[android] Change light theme background color closer to white

See merge request briar/briar!1020
2018-12-21 16:48:18 +00:00
Torsten Grote
c229e19452 [android] remove images from preview that could not be loaded
We will not even attempt to attach them
2018-12-21 11:05:34 -02:00
Torsten Grote
42bca09d16 [android] Add gap between attached image previews 2018-12-21 11:05:34 -02:00
Torsten Grote
9eacbfa659 [android] Remove palette library
we are not extracting photo colors anymore
2018-12-21 11:05:34 -02:00
Torsten Grote
f14e546dc6 [android] allow to attach multiple images 2018-12-21 11:05:34 -02:00
akwizgran
684c64a1d9 Merge branch '1310-disable-enter-transition-for-samsung7' into 'master'
[android] Disable Conversation Enter Transition for Samsung 7 devices

Closes #1310

See merge request briar/briar!1023
2018-12-19 11:32:51 +00:00
akwizgran
6fdab959b1 Merge branch '631-inject-fragments-early' into 'master'
Inject fragments earlier in their lifecycle

Closes #631

See merge request briar/briar!1024
2018-12-19 11:24:31 +00:00
Torsten Grote
c8487483ff [android] Also consider Android 7.1 (API 25) to be Samsung7
which is used for disabling certain features due to crashes there.
2018-12-18 18:17:27 -02:00
Torsten Grote
a159b23dc0 [android] Disable Conversation Enter Transition for Samsung 7 devices 2018-12-18 18:16:32 -02:00
Torsten Grote
5070a27a83 [android] also fix some activity nullability issues 2018-12-18 18:12:05 -02:00
Torsten Grote
9ce73a6840 [android] inject fragments already in onAttach()
This also removes the need to override the inject method even when
there's nothing to inject.

While passing over all fragments, some nullability issues also have been
addressed.
2018-12-18 18:01:04 -02:00
akwizgran
6e9928f20f Merge branch '1484-wait-for-component-to-be-created' into 'master'
[android] AliasFragment: Wait for activity component to be created

Closes #1484

See merge request briar/briar!1022
2018-12-18 17:46:19 +00:00
Torsten Grote
b31d61afc5 [android] AliasFragment: Wait for activity component to be created
before injecting the ViewModel
2018-12-18 15:32:26 -02:00
akwizgran
5a99cb93cc Merge branch '1482-check-earlier-for-sign-in' into 'master'
[android] don't crash when re-opening conversation after briar exited

Closes #1482

See merge request briar/briar!1021
2018-12-18 12:58:39 +00:00
Torsten Grote
d0bbebd25e [android] don't crash when re-opening conversation after briar exited 2018-12-17 18:42:06 -02:00
Torsten Grote
4307d26606 [android] Change light theme background color closer to white 2018-12-17 17:25:12 -02:00
akwizgran
0089c1ac6d Merge branch '1468-restrict-image-size' into 'master'
Fix first issues related to image size

See merge request briar/briar!1018
2018-12-17 12:48:15 +00:00
akwizgran
2a7aac4930 Upgrade Jackson to 2.9.8. 2018-12-17 12:09:36 +00:00
akwizgran
a37b6d81ed Merge branch '1242-save-snackbar-fix' into 'master'
[android] Clarify the meaning of image save state

See merge request briar/briar!1017
2018-12-17 11:17:12 +00:00
Torsten Grote
1d09a6708a [android] don't ever load an entire image into memory
This happens on API 27+28 if loading TIFF or WebP files.
Using an InputStream with a read limit prevents this.
2018-12-14 20:11:43 -02:00
Torsten Grote
d3b6f484c8 [android] allow image transformations in full-screen view
to prevent crashes from huge images
2018-12-14 20:11:43 -02:00
Torsten Grote
039c6edb66 [android] increase scale levels of PhotoView 2018-12-14 20:11:43 -02:00
Torsten Grote
8b9f89eab2 [android] Clarify the meaning of image save state 2018-12-14 12:27:47 -02:00
akwizgran
1e2c17b170 Merge branch '1242-display-image-attachments-multiple' into 'master'
Swipe left/right in image screen for images from same message

See merge request briar/briar!1012
2018-12-13 16:33:24 +00:00
Torsten Grote
a994966095 [android] address review comments for image fullscreen swiping 2018-12-13 12:00:51 -02:00
Torsten Grote
2bea581654 [android] Swipe left/right in image screen to see other images from the same message 2018-12-13 11:59:41 -02:00
Torsten Grote
87377666aa Merge branch '1473-display-multiple-images' into 'master'
UX for displaying multiple image attachments

Closes #1473

See merge request briar/briar!1010
2018-12-13 13:07:24 +00:00
akwizgran
9d07b2e141 Resolve merge conflicts.
# Conflicts:
#   briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java
#   briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java
2018-12-13 11:41:04 +00:00
akwizgran
5c312b49e2 Merge branch '1438-send-image-attachments' into 'master'
Store attachments and actually attach them to sent messages

Closes #1438

See merge request briar/briar!1006
2018-12-13 10:27:09 +00:00
Torsten Grote
f56efe45cd Merge branch '1477-get-client-minor-version' into 'master'
Add method for querying client minor version supported by contact

See merge request briar/briar!1014
2018-12-12 19:34:55 +00:00
Torsten Grote
2332a58681 [android] address review comments for displaying multiple images 2018-12-12 17:00:44 -02:00
Torsten Grote
8c6dfaa196 [android] Use @UiThread instead of @MainThread 2018-12-12 16:18:43 -02:00
Torsten Grote
3cfb04b60d Establish some rules for handling InputStreams
* Methods shouldn't place any special requirements on the streams
  passed into them
* This implies that if a stream's going to be marked and reset,
  that should all happen within one method
* This also implies that if a method needs to mark and reset a stream,
  it should wrap the stream in a BufferedInputStream before doing so,
  rather than requiring a markable stream to be passed in
2018-12-12 16:17:50 -02:00
Torsten Grote
e85fbfb952 [android] close InputStream with new IoUtils method 2018-12-12 16:17:50 -02:00
Torsten Grote
80ee35d926 [core] Return fake mini PNG as Attachment instead of throwing exception 2018-12-12 16:17:50 -02:00
Torsten Grote
4796902b9c [android] store attachments and actually attach them to sent messages 2018-12-12 16:17:50 -02:00
akwizgran
149e67c0f7 Reduce code duplication in tests. 2018-12-12 11:57:35 +00:00
akwizgran
1d5214117f Add tests for getClientMinorVersion(). 2018-12-11 17:55:39 +00:00
akwizgran
b8f248ca9c Add tests for getClientVisibility(). 2018-12-11 17:51:42 +00:00
Torsten Grote
dfb71a03a5 [android] Only retrieve image sizes for single images in messages
We need to do this to know the height of messages when binding the view.
The size of single images can be different (e.g. due to orientation).
For multiple images, we use a fixed size, so no retrieval is required.
2018-12-11 15:38:05 -02:00
Torsten Grote
961fdc8e72 [android] Show multiple images in message bubble 2018-12-11 15:28:21 -02:00
Torsten Grote
c3d44663cd [android] Use a nested RecyclerView with a single items to show image attachments
This is preparation for showing multiple image attachments in one
message bubble.
2018-12-11 15:28:21 -02:00
akwizgran
0081472489 Add method for querying contact's client minor version. 2018-12-11 17:25:29 +00:00
akwizgran
cdf4f3a24b Merge branch '1232-add-contacts-remotely-api' into 'master'
[api] Add interface for adding contacts remotely

See merge request briar/briar!1007
2018-12-10 10:53:37 +00:00
Torsten Grote
fb1d8e860f [api] Add interface for adding contacts remotely 2018-12-10 08:30:50 -02:00
akwizgran
a3c526ec9a Merge branch '1298-scrub-wifi-address-in-crash-report' into 'master'
Scrub wifi IP address in crash reports.

Closes #1298

See merge request briar/briar!1013
2018-12-10 10:12:42 +00:00
Jordi Salvat
dee488d06d Scrub wifi IP address in crash reports. 2018-12-10 01:07:37 +01:00
Torsten Grote
b29c7d8022 Merge branch '1385-make-link-cover-entire-word' into 'master'
[android] fix start of link in error message for adding contacts

Closes #1385

See merge request briar/briar!1011
2018-12-07 19:13:54 +00:00
akwizgran
0725d207ec Merge branch '1432-headless-integration-tests' into 'master'
[headless] Add first integration test for ContactController

See merge request briar/briar!1008
2018-12-07 17:37:22 +00:00
akwizgran
5a7599a88d Merge branch '1242-display-image-attachments-save' into 'master'
Allow the user to save image attachment outside of Briar

See merge request briar/briar!1005
2018-12-07 17:31:42 +00:00
Torsten Grote
59cd98db81 [android] Get image extension from MimeTypeMap and store it in AttachmentItem 2018-12-07 15:11:09 -02:00
Torsten Grote
768488eb04 [android] Show (tinted) security icon when warning about saving attachments 2018-12-07 14:39:43 -02:00
Torsten Grote
a6b1ad48c3 [android] Add support for saving image attachments on API < 19
This is done by using the WRITE_EXTERNAL_STORAGE permission
to write the file directly without using the system activity.
2018-12-07 13:01:44 -02:00
Torsten Grote
77299a68ed [android] Allow the user to save image attachment outside of Briar 2018-12-07 13:01:42 -02:00
akwizgran
5e5705c73b Merge branch '1438-send-image-attachments-ui' into 'master'
Implement UX for sending image attachments

See merge request briar/briar!1004
2018-12-07 14:58:23 +00:00
Torsten Grote
e6229a3a13 [android] Factor out image preview into its own view class 2018-12-06 17:56:02 -02:00
Torsten Grote
5fbacb4ee4 [android] Split out an EmojiTextInputView from TextInputViews
This also removes the TextInputController whose job is now done by the view.
2018-12-06 17:56:02 -02:00
Torsten Grote
c7f4e976ed [android] Require users of TextInputView to set its controller 2018-12-06 17:56:02 -02:00
Torsten Grote
419f2d966a [android] Show a toast when an image could not be attached 2018-12-06 17:56:02 -02:00
Torsten Grote
d6c18db9e9 [android] set image preview size to 1/4 of screen height 2018-12-06 17:56:02 -02:00
Torsten Grote
8fe49d9961 [android] Re-factor TextInputViews 2018-12-06 17:56:02 -02:00
Torsten Grote
f536cfdab8 [android] first round of review comments for attaching images 2018-12-06 17:56:02 -02:00
Torsten Grote
4d594acad5 [android] Save attached (but not sent) image on screen rotation 2018-12-06 17:56:02 -02:00
Torsten Grote
800dfed5c1 [android] support adding image attachments to private messages 2018-12-06 17:55:59 -02:00
Jordi Salvat
54b823e401 [android] fix start of link in error message for adding contacts 2018-12-06 20:44:36 +01:00
Torsten Grote
52ec56d690 Merge branch 'invalid-slide-direction' into 'master'
Revert change to slide direction

Closes #1478

See merge request briar/briar!1009
2018-12-06 15:32:07 +00:00
akwizgran
7b3afcca99 Revert change to slide direction. 2018-12-06 15:18:16 +00:00
Torsten Grote
a22d03d028 [headless] wait for lifecycle manager to finish starting
before starting web server
2018-12-05 16:08:03 -02:00
Torsten Grote
d857338ad0 [headless] Add first integration test for ContactController 2018-12-05 16:04:14 -02:00
akwizgran
dcd5e34c6b Improve UI hiding behaviour. 2018-11-30 12:40:45 +00:00
206 changed files with 6437 additions and 2358 deletions

View File

@@ -19,9 +19,7 @@ import javax.inject.Inject;
import static android.content.Intent.ACTION_BATTERY_CHANGED; import static android.content.Intent.ACTION_BATTERY_CHANGED;
import static android.content.Intent.ACTION_POWER_CONNECTED; import static android.content.Intent.ACTION_POWER_CONNECTED;
import static android.content.Intent.ACTION_POWER_DISCONNECTED; import static android.content.Intent.ACTION_POWER_DISCONNECTED;
import static android.os.BatteryManager.BATTERY_STATUS_CHARGING; import static android.os.BatteryManager.EXTRA_PLUGGED;
import static android.os.BatteryManager.BATTERY_STATUS_FULL;
import static android.os.BatteryManager.EXTRA_STATUS;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
@@ -48,9 +46,8 @@ class AndroidBatteryManager implements BatteryManager, Service {
IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED); IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED);
Intent i = appContext.registerReceiver(null, filter); Intent i = appContext.registerReceiver(null, filter);
if (i == null) return false; if (i == null) return false;
int status = i.getIntExtra(EXTRA_STATUS, -1); int status = i.getIntExtra(EXTRA_PLUGGED, 0);
return status == BATTERY_STATUS_CHARGING || return status != 0;
status == BATTERY_STATUS_FULL;
} }
@Override @Override

View File

@@ -13,6 +13,8 @@ import java.util.Collection;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import static org.briarproject.bramble.api.contact.PendingContact.PendingContactState.FAILED;
@NotNullByDefault @NotNullByDefault
public interface ContactManager { public interface ContactManager {
@@ -52,6 +54,35 @@ public interface ContactManager {
long timestamp, boolean alice, boolean verified, boolean active) long timestamp, boolean alice, boolean verified, boolean active)
throws DbException; throws DbException;
/**
* Returns the static link that needs to be sent to the contact to be added.
*/
String getRemoteContactLink();
/**
* Returns true if the given link is syntactically valid.
*/
boolean isValidRemoteContactLink(String link);
/**
* Requests a new contact to be added via the given {@code link}.
*
* @param link The link received from the contact we want to add.
* @param alias The alias the user has given this contact.
* @return A PendingContact representing the contact to be added.
*/
PendingContact addRemoteContactRequest(String link, String alias);
/**
* Returns a list of {@link PendingContact}s.
*/
Collection<PendingContact> getPendingContacts();
/**
* Removes a {@link PendingContact} that is in state {@link FAILED}.
*/
void removePendingContact(PendingContact pendingContact);
/** /**
* Returns the contact with the given ID. * Returns the contact with the given ID.
*/ */

View File

@@ -0,0 +1,54 @@
package org.briarproject.bramble.api.contact;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class PendingContact {
public enum PendingContactState {
WAITING_FOR_CONNECTION,
CONNECTED,
ADDING_CONTACT,
FAILED
}
private final PendingContactId id;
private final String alias;
private final PendingContactState state;
private final long timestamp;
public PendingContact(PendingContactId id, String alias,
PendingContactState state, long timestamp) {
this.id = id;
this.alias = alias;
this.state = state;
this.timestamp = timestamp;
}
public String getAlias() {
return alias;
}
public PendingContactState getState() {
return state;
}
public long getTimestamp() {
return timestamp;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object o) {
return o instanceof PendingContact &&
id.equals(((PendingContact) o).id);
}
}

View File

@@ -0,0 +1,11 @@
package org.briarproject.bramble.api.contact;
import org.briarproject.bramble.api.UniqueId;
public class PendingContactId extends UniqueId {
public PendingContactId(byte[] id) {
super(id);
}
}

View File

@@ -0,0 +1,34 @@
package org.briarproject.bramble.api.contact.event;
import org.briarproject.bramble.api.contact.PendingContact.PendingContactState;
import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.Immutable;
/**
* An event that is broadcast when a pending contact's state is changed.
*/
@Immutable
@NotNullByDefault
public class PendingContactStateChangedEvent extends Event {
private final PendingContactId id;
private final PendingContactState state;
public PendingContactStateChangedEvent(PendingContactId id,
PendingContactState state) {
this.id = id;
this.state = state;
}
public PendingContactId getId() {
return id;
}
public PendingContactState getPendingContactState() {
return state;
}
}

View File

@@ -1,20 +0,0 @@
package org.briarproject.bramble.api.io;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.sync.tree.TreeHash;
import java.util.List;
public interface BlockSink {
/**
* Stores a block of the message with the given temporary ID.
*/
void putBlock(HashingId h, int blockNumber, byte[] data) throws DbException;
/**
* Sets the hash tree path of a previously stored block.
*/
void setPath(HashingId h, int blockNumber, List<TreeHash> path)
throws DbException;
}

View File

@@ -1,27 +0,0 @@
package org.briarproject.bramble.api.io;
import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import javax.annotation.concurrent.ThreadSafe;
/**
* Type-safe wrapper for a byte array that uniquely identifies a
* {@link Message} while it's being hashed and the {@link MessageId} is not
* yet known.
*/
@ThreadSafe
@NotNullByDefault
public class HashingId extends UniqueId {
public HashingId(byte[] id) {
super(id);
}
@Override
public boolean equals(Object o) {
return o instanceof HashingId && super.equals(o);
}
}

View File

@@ -16,6 +16,7 @@ public interface TorConstants {
String PREF_TOR_NETWORK = "network2"; String PREF_TOR_NETWORK = "network2";
String PREF_TOR_PORT = "port"; String PREF_TOR_PORT = "port";
String PREF_TOR_MOBILE = "useMobileData"; String PREF_TOR_MOBILE = "useMobileData";
String PREF_TOR_ONLY_WHEN_CHARGING = "onlyWhenCharging";
int PREF_TOR_NETWORK_AUTOMATIC = 0; int PREF_TOR_NETWORK_AUTOMATIC = 0;
int PREF_TOR_NETWORK_WITHOUT_BRIDGES = 1; int PREF_TOR_NETWORK_WITHOUT_BRIDGES = 1;

View File

@@ -1,7 +1,6 @@
package org.briarproject.bramble.api.sync; package org.briarproject.bramble.api.sync;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.tree.TreeHash;
@NotNullByDefault @NotNullByDefault
public interface MessageFactory { public interface MessageFactory {
@@ -11,6 +10,4 @@ public interface MessageFactory {
Message createMessage(byte[] raw); Message createMessage(byte[] raw);
byte[] getRawMessage(Message m); byte[] getRawMessage(Message m);
MessageId getMessageId(GroupId g, long timestamp, TreeHash rootHash);
} }

View File

@@ -24,12 +24,6 @@ public class MessageId extends UniqueId {
public static final String BLOCK_LABEL = public static final String BLOCK_LABEL =
"org.briarproject.bramble/MESSAGE_BLOCK"; "org.briarproject.bramble/MESSAGE_BLOCK";
/**
* Label for hashing two tree hashes to produce a parent.
*/
public static final String TREE_LABEL =
"org.briarproject.bramble/MESSAGE_TREE";
public MessageId(byte[] id) { public MessageId(byte[] id) {
super(id); super(id);
} }

View File

@@ -35,9 +35,4 @@ public interface SyncConstants {
* The maximum number of message IDs in an ack, offer or request record. * The maximum number of message IDs in an ack, offer or request record.
*/ */
int MAX_MESSAGE_IDS = MAX_RECORD_PAYLOAD_BYTES / UniqueId.LENGTH; int MAX_MESSAGE_IDS = MAX_RECORD_PAYLOAD_BYTES / UniqueId.LENGTH;
/**
* The maximum length of a message block in bytes.
*/
int MAX_BLOCK_LENGTH = 32 * 2014; // 32 KiB
} }

View File

@@ -1,21 +0,0 @@
package org.briarproject.bramble.api.sync.tree;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
public class LeafNode extends TreeNode {
public LeafNode(TreeHash hash, int blockNumber) {
super(hash, 0, blockNumber, blockNumber);
}
@Override
public TreeNode getLeftChild() {
throw new UnsupportedOperationException();
}
@Override
public TreeNode getRightChild() {
throw new UnsupportedOperationException();
}
}

View File

@@ -1,26 +0,0 @@
package org.briarproject.bramble.api.sync.tree;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
public class ParentNode extends TreeNode {
private final TreeNode left, right;
public ParentNode(TreeHash hash, TreeNode left, TreeNode right) {
super(hash, Math.max(left.getHeight(), right.getHeight()) + 1,
left.getFirstBlockNumber(), right.getLastBlockNumber());
this.left = left;
this.right = right;
}
@Override
public TreeNode getLeftChild() {
return left;
}
@Override
public TreeNode getRightChild() {
return right;
}
}

View File

@@ -1,21 +0,0 @@
package org.briarproject.bramble.api.sync.tree;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.io.BlockSink;
import org.briarproject.bramble.api.io.HashingId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.IOException;
import java.io.InputStream;
@NotNullByDefault
public interface StreamHasher {
/**
* Reads the given input stream, divides the data into blocks, stores
* the blocks and the resulting hash tree using the given block sink and
* temporary ID, and returns the hash tree.
*/
TreeNode hash(InputStream in, BlockSink sink, HashingId h)
throws IOException, DbException;
}

View File

@@ -1,24 +0,0 @@
package org.briarproject.bramble.api.sync.tree;
import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.ThreadSafe;
/**
* Type-safe wrapper for a byte array that uniquely identifies a sequence of
* one or more message blocks.
*/
@ThreadSafe
@NotNullByDefault
public class TreeHash extends UniqueId {
public TreeHash(byte[] id) {
super(id);
}
@Override
public boolean equals(Object o) {
return o instanceof TreeHash && super.equals(o);
}
}

View File

@@ -1,11 +0,0 @@
package org.briarproject.bramble.api.sync.tree;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
public interface TreeHasher {
LeafNode hashBlock(int blockNumber, byte[] data);
ParentNode mergeTrees(TreeNode left, TreeNode right);
}

View File

@@ -1,38 +0,0 @@
package org.briarproject.bramble.api.sync.tree;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
public abstract class TreeNode {
private final TreeHash hash;
private final int height, firstBlockNumber, lastBlockNumber;
TreeNode(TreeHash hash, int height, int firstBlockNumber,
int lastBlockNumber) {
this.hash = hash;
this.height = height;
this.firstBlockNumber = firstBlockNumber;
this.lastBlockNumber = lastBlockNumber;
}
public TreeHash getHash() {
return hash;
}
public int getHeight() {
return height;
}
public int getFirstBlockNumber() {
return firstBlockNumber;
}
public int getLastBlockNumber() {
return lastBlockNumber;
}
public abstract TreeNode getLeftChild();
public abstract TreeNode getRightChild();
}

View File

@@ -38,6 +38,13 @@ public interface ClientVersioningManager {
Visibility getClientVisibility(Transaction txn, ContactId contactId, Visibility getClientVisibility(Transaction txn, ContactId contactId,
ClientId clientId, int majorVersion) throws DbException; ClientId clientId, int majorVersion) throws DbException;
/**
* Returns the minor version of the given client that is supported by the
* given contact, or -1 if the contact does not support the client.
*/
int getClientMinorVersion(Transaction txn, ContactId contactId,
ClientId clientId, int majorVersion) throws DbException;
interface ClientVersioningHook { interface ClientVersioningHook {
void onClientVisibilityChanging(Transaction txn, Contact c, void onClientVisibilityChanging(Transaction txn, Contact c,

View File

@@ -0,0 +1,67 @@
package org.briarproject.bramble;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.Executor;
import javax.annotation.concurrent.GuardedBy;
@NotNullByDefault
public class PriorityExecutor {
private final Object lock = new Object();
private final Executor delegate, high, low;
@GuardedBy("lock")
private final Queue<Runnable> highQueue = new LinkedList<>();
@GuardedBy("lock")
private final Queue<Runnable> lowQueue = new LinkedList<>();
@GuardedBy("lock")
private boolean isTaskRunning = false;
public PriorityExecutor(Executor delegate) {
this.delegate = delegate;
high = r -> submit(r, true);
low = r -> submit(r, false);
}
public Executor getHighPriorityExecutor() {
return high;
}
public Executor getLowPriorityExecutor() {
return low;
}
private void submit(Runnable r, boolean isHighPriority) {
Runnable wrapped = () -> {
try {
r.run();
} finally {
scheduleNext();
}
};
synchronized (lock) {
if (!isTaskRunning && highQueue.isEmpty() &&
(isHighPriority || lowQueue.isEmpty())) {
isTaskRunning = true;
delegate.execute(wrapped);
} else if (isHighPriority) {
highQueue.add(wrapped);
} else {
lowQueue.add(wrapped);
}
}
}
private void scheduleNext() {
synchronized (lock) {
Runnable next = highQueue.poll();
if (next == null) next = lowQueue.poll();
if (next == null) isTaskRunning = false;
else delegate.execute(next);
}
}
}

View File

@@ -3,6 +3,8 @@ package org.briarproject.bramble.contact;
import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.contact.PendingContact;
import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
@@ -19,12 +21,16 @@ import org.briarproject.bramble.api.transport.KeyManager;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Pattern;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe; import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject; import javax.inject.Inject;
import static java.util.Collections.emptyList;
import static org.briarproject.bramble.api.contact.PendingContact.PendingContactState.WAITING_FOR_CONNECTION;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorInfo.Status.OURSELVES; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.OURSELVES;
import static org.briarproject.bramble.api.identity.AuthorInfo.Status.UNKNOWN; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.UNKNOWN;
@@ -36,6 +42,12 @@ import static org.briarproject.bramble.util.StringUtils.toUtf8;
@NotNullByDefault @NotNullByDefault
class ContactManagerImpl implements ContactManager { class ContactManagerImpl implements ContactManager {
private static final int LINK_LENGTH = 64;
private static final String REMOTE_CONTACT_LINK =
"briar://" + getRandomBase32String(LINK_LENGTH);
private static final Pattern LINK_REGEX =
Pattern.compile("(briar://)?([a-z2-7]{" + LINK_LENGTH + "})");
private final DatabaseComponent db; private final DatabaseComponent db;
private final KeyManager keyManager; private final KeyManager keyManager;
private final IdentityManager identityManager; private final IdentityManager identityManager;
@@ -84,6 +96,48 @@ class ContactManagerImpl implements ContactManager {
verified, active)); verified, active));
} }
@Override
public String getRemoteContactLink() {
// TODO replace with real implementation
return REMOTE_CONTACT_LINK;
}
@SuppressWarnings("SameParameterValue")
private static String getRandomBase32String(int length) {
Random random = new Random();
char[] c = new char[length];
for (int i = 0; i < length; i++) {
int character = random.nextInt(32);
if (character < 26) c[i] = (char) ('a' + character);
else c[i] = (char) ('2' + (character - 26));
}
return new String(c);
}
@Override
public boolean isValidRemoteContactLink(String link) {
return LINK_REGEX.matcher(link).matches();
}
@Override
public PendingContact addRemoteContactRequest(String link, String alias) {
// TODO replace with real implementation
PendingContactId id = new PendingContactId(link.getBytes());
return new PendingContact(id, alias, WAITING_FOR_CONNECTION,
System.currentTimeMillis());
}
@Override
public Collection<PendingContact> getPendingContacts() {
// TODO replace with real implementation
return emptyList();
}
@Override
public void removePendingContact(PendingContact pendingContact) {
// TODO replace with real implementation
}
@Override @Override
public Contact getContact(ContactId c) throws DbException { public Contact getContact(ContactId c) throws DbException {
return db.transactionWithResult(true, txn -> db.getContact(txn, c)); return db.transactionWithResult(true, txn -> db.getContact(txn, c));

View File

@@ -69,6 +69,7 @@ import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_AUTOMATIC; import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_AUTOMATIC;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_NEVER; import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_NEVER;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_WITH_BRIDGES; import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_WITH_BRIDGES;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_PORT; import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_PORT;
import static org.briarproject.bramble.api.plugin.TorConstants.PROP_ONION_V2; import static org.briarproject.bramble.api.plugin.TorConstants.PROP_ONION_V2;
import static org.briarproject.bramble.api.plugin.TorConstants.PROP_ONION_V3; import static org.briarproject.bramble.api.plugin.TorConstants.PROP_ONION_V3;
@@ -648,8 +649,10 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
if (s.getNamespace().equals(ID.getString())) { if (s.getNamespace().equals(ID.getString())) {
LOG.info("Tor settings updated"); LOG.info("Tor settings updated");
settings = s.getSettings(); settings = s.getSettings();
// Works around a bug introduced in Tor 0.3.4.8. Could be // Works around a bug introduced in Tor 0.3.4.8.
// replaced with callback.transportDisabled() when fixed. // https://trac.torproject.org/projects/tor/ticket/28027
// Could be replaced with callback.transportDisabled()
// when fixed.
disableNetwork(); disableNetwork();
updateConnectionStatus(networkManager.getNetworkStatus(), updateConnectionStatus(networkManager.getNetworkStatus(),
batteryManager.isCharging()); batteryManager.isCharging());
@@ -685,6 +688,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
int network = settings.getInt(PREF_TOR_NETWORK, int network = settings.getInt(PREF_TOR_NETWORK,
PREF_TOR_NETWORK_AUTOMATIC); PREF_TOR_NETWORK_AUTOMATIC);
boolean useMobile = settings.getBoolean(PREF_TOR_MOBILE, true); boolean useMobile = settings.getBoolean(PREF_TOR_MOBILE, true);
boolean onlyWhenCharging =
settings.getBoolean(PREF_TOR_ONLY_WHEN_CHARGING, false);
boolean bridgesWork = circumventionProvider.doBridgesWork(country); boolean bridgesWork = circumventionProvider.doBridgesWork(country);
boolean automatic = network == PREF_TOR_NETWORK_AUTOMATIC; boolean automatic = network == PREF_TOR_NETWORK_AUTOMATIC;
@@ -699,9 +704,12 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
if (!online) { if (!online) {
LOG.info("Disabling network, device is offline"); LOG.info("Disabling network, device is offline");
enableNetwork(false); enableNetwork(false);
} else if (!charging && onlyWhenCharging) {
LOG.info("Disabling network, device is on battery");
enableNetwork(false);
} else if (network == PREF_TOR_NETWORK_NEVER || } else if (network == PREF_TOR_NETWORK_NEVER ||
(!useMobile && !wifi)) { (!useMobile && !wifi)) {
LOG.info("Disabling network due to setting"); LOG.info("Disabling network, device is using mobile data");
enableNetwork(false); enableNetwork(false);
} else if (automatic && blocked && !bridgesWork) { } else if (automatic && blocked && !bridgesWork) {
LOG.info("Disabling network, country is blocked"); LOG.info("Disabling network, country is blocked");

View File

@@ -7,7 +7,6 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageFactory; import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.tree.TreeHash;
import org.briarproject.bramble.util.ByteUtils; import org.briarproject.bramble.util.ByteUtils;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
@@ -40,19 +39,13 @@ class MessageFactoryImpl implements MessageFactory {
if (body.length == 0) throw new IllegalArgumentException(); if (body.length == 0) throw new IllegalArgumentException();
if (body.length > MAX_MESSAGE_BODY_LENGTH) if (body.length > MAX_MESSAGE_BODY_LENGTH)
throw new IllegalArgumentException(); throw new IllegalArgumentException();
MessageId id = getMessageIdFromBody(g, timestamp, body); MessageId id = getMessageId(g, timestamp, body);
return new Message(id, g, timestamp, body); return new Message(id, g, timestamp, body);
} }
private MessageId getMessageIdFromBody(GroupId g, long timestamp, private MessageId getMessageId(GroupId g, long timestamp, byte[] body) {
byte[] body) {
// There's only one block, so the root hash is the hash of the block // There's only one block, so the root hash is the hash of the block
byte[] rootHash = crypto.hash(BLOCK_LABEL, FORMAT_VERSION_BYTES, body); byte[] rootHash = crypto.hash(BLOCK_LABEL, FORMAT_VERSION_BYTES, body);
return getMessageIdFromRootHash(g, timestamp, rootHash);
}
private MessageId getMessageIdFromRootHash(GroupId g, long timestamp,
byte[] rootHash) {
byte[] timeBytes = new byte[INT_64_BYTES]; byte[] timeBytes = new byte[INT_64_BYTES];
ByteUtils.writeUint64(timestamp, timeBytes, 0); ByteUtils.writeUint64(timestamp, timeBytes, 0);
byte[] idHash = crypto.hash(ID_LABEL, FORMAT_VERSION_BYTES, byte[] idHash = crypto.hash(ID_LABEL, FORMAT_VERSION_BYTES,
@@ -72,7 +65,7 @@ class MessageFactoryImpl implements MessageFactory {
long timestamp = ByteUtils.readUint64(raw, UniqueId.LENGTH); long timestamp = ByteUtils.readUint64(raw, UniqueId.LENGTH);
byte[] body = new byte[raw.length - MESSAGE_HEADER_LENGTH]; byte[] body = new byte[raw.length - MESSAGE_HEADER_LENGTH];
System.arraycopy(raw, MESSAGE_HEADER_LENGTH, body, 0, body.length); System.arraycopy(raw, MESSAGE_HEADER_LENGTH, body, 0, body.length);
MessageId id = getMessageIdFromBody(g, timestamp, body); MessageId id = getMessageId(g, timestamp, body);
return new Message(id, g, timestamp, body); return new Message(id, g, timestamp, body);
} }
@@ -85,10 +78,4 @@ class MessageFactoryImpl implements MessageFactory {
System.arraycopy(body, 0, raw, MESSAGE_HEADER_LENGTH, body.length); System.arraycopy(body, 0, raw, MESSAGE_HEADER_LENGTH, body.length);
return raw; return raw;
} }
@Override
public MessageId getMessageId(GroupId g, long timestamp,
TreeHash rootHash) {
return getMessageIdFromRootHash(g, timestamp, rootHash.getBytes());
}
} }

View File

@@ -1,16 +0,0 @@
package org.briarproject.bramble.sync.tree;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.tree.LeafNode;
import org.briarproject.bramble.api.sync.tree.TreeNode;
import javax.annotation.concurrent.NotThreadSafe;
@NotThreadSafe
@NotNullByDefault
interface HashTree {
void addLeaf(LeafNode leaf);
TreeNode getRoot();
}

View File

@@ -1,46 +0,0 @@
package org.briarproject.bramble.sync.tree;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.tree.LeafNode;
import org.briarproject.bramble.api.sync.tree.TreeHasher;
import org.briarproject.bramble.api.sync.tree.TreeNode;
import java.util.Deque;
import java.util.LinkedList;
import javax.inject.Inject;
@NotNullByDefault
class HashTreeImpl implements HashTree {
private final TreeHasher treeHasher;
private final Deque<TreeNode> nodes = new LinkedList<>();
@Inject
HashTreeImpl(TreeHasher treeHasher) {
this.treeHasher = treeHasher;
}
@Override
public void addLeaf(LeafNode leaf) {
TreeNode add = leaf;
int height = leaf.getHeight();
TreeNode last = nodes.peekLast();
while (last != null && last.getHeight() == height) {
add = treeHasher.mergeTrees(last, add);
height = add.getHeight();
nodes.removeLast();
last = nodes.peekLast();
}
nodes.addLast(add);
}
@Override
public TreeNode getRoot() {
TreeNode root = nodes.removeLast();
while (!nodes.isEmpty()) {
root = treeHasher.mergeTrees(nodes.removeLast(), root);
}
return root;
}
}

View File

@@ -1,85 +0,0 @@
package org.briarproject.bramble.sync.tree;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.io.BlockSink;
import org.briarproject.bramble.api.io.HashingId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.tree.StreamHasher;
import org.briarproject.bramble.api.sync.tree.TreeHash;
import org.briarproject.bramble.api.sync.tree.TreeHasher;
import org.briarproject.bramble.api.sync.tree.TreeNode;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import javax.inject.Provider;
import static java.util.Arrays.copyOfRange;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_BLOCK_LENGTH;
@Immutable
@NotNullByDefault
class StreamHasherImpl implements StreamHasher {
private final TreeHasher treeHasher;
private final Provider<HashTree> hashTreeProvider;
@Inject
StreamHasherImpl(TreeHasher treeHasher,
Provider<HashTree> hashTreeProvider) {
this.treeHasher = treeHasher;
this.hashTreeProvider = hashTreeProvider;
}
@Override
public TreeNode hash(InputStream in, BlockSink sink, HashingId h)
throws IOException, DbException {
HashTree tree = hashTreeProvider.get();
byte[] block = new byte[MAX_BLOCK_LENGTH];
int read;
for (int blockNumber = 0; (read = read(in, block)) > 0; blockNumber++) {
byte[] data;
if (read == block.length) data = block;
else data = copyOfRange(block, 0, read);
sink.putBlock(h, blockNumber, data);
tree.addLeaf(treeHasher.hashBlock(blockNumber, data));
}
TreeNode root = tree.getRoot();
setPaths(sink, h, root, new LinkedList<>());
return root;
}
/**
* Reads a block from the given input stream and returns the number of
* bytes read, or 0 if no bytes were read before reaching the end of the
* stream.
*/
private int read(InputStream in, byte[] block) throws IOException {
int offset = 0;
while (offset < block.length) {
int read = in.read(block, offset, block.length - offset);
if (read == -1) return offset;
offset += read;
}
return offset;
}
private void setPaths(BlockSink sink, HashingId h, TreeNode node,
LinkedList<TreeHash> path) throws DbException {
if (node.getHeight() == 0) {
// We've reached a leaf - store the path
sink.setPath(h, node.getFirstBlockNumber(), path);
} else {
// Add the right child's hash to the path and traverse the left
path.addFirst(node.getRightChild().getHash());
setPaths(sink, h, node.getLeftChild(), path);
// Add the left child's hash to the path and traverse the right
path.removeFirst();
path.addFirst(node.getLeftChild().getHash());
setPaths(sink, h, node.getRightChild(), path);
}
}
}

View File

@@ -1,44 +0,0 @@
package org.briarproject.bramble.sync.tree;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.tree.LeafNode;
import org.briarproject.bramble.api.sync.tree.ParentNode;
import org.briarproject.bramble.api.sync.tree.TreeHash;
import org.briarproject.bramble.api.sync.tree.TreeHasher;
import org.briarproject.bramble.api.sync.tree.TreeNode;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static org.briarproject.bramble.api.sync.Message.FORMAT_VERSION;
import static org.briarproject.bramble.api.sync.MessageId.BLOCK_LABEL;
import static org.briarproject.bramble.api.sync.MessageId.TREE_LABEL;
@Immutable
@NotNullByDefault
class TreeHasherImpl implements TreeHasher {
private static final byte[] FORMAT_VERSION_BYTES =
new byte[] {FORMAT_VERSION};
private final CryptoComponent crypto;
@Inject
TreeHasherImpl(CryptoComponent crypto) {
this.crypto = crypto;
}
@Override
public LeafNode hashBlock(int blockNumber, byte[] data) {
byte[] hash = crypto.hash(BLOCK_LABEL, FORMAT_VERSION_BYTES, data);
return new LeafNode(new TreeHash(hash), blockNumber);
}
@Override
public ParentNode mergeTrees(TreeNode left, TreeNode right) {
byte[] hash = crypto.hash(TREE_LABEL, FORMAT_VERSION_BYTES,
left.getHash().getBytes(), right.getHash().getBytes());
return new ParentNode(new TreeHash(hash), left, right);
}
}

View File

@@ -89,14 +89,9 @@ class ClientVersioningManagerImpl implements ClientVersioningManager, Client,
public Visibility getClientVisibility(Transaction txn, ContactId contactId, public Visibility getClientVisibility(Transaction txn, ContactId contactId,
ClientId clientId, int majorVersion) throws DbException { ClientId clientId, int majorVersion) throws DbException {
try { try {
Contact contact = db.getContact(txn, contactId); LatestUpdates latest = findLatestUpdates(txn, contactId);
Group g = getContactGroup(contact); if (latest == null || latest.remote == null) return INVISIBLE;
// Contact may be in the process of being added or removed, so
// contact group may not exist
if (!db.containsGroup(txn, g.getId())) return INVISIBLE;
LatestUpdates latest = findLatestUpdates(txn, g.getId());
if (latest.local == null) throw new DbException(); if (latest.local == null) throw new DbException();
if (latest.remote == null) return INVISIBLE;
Update localUpdate = loadUpdate(txn, latest.local.messageId); Update localUpdate = loadUpdate(txn, latest.local.messageId);
Update remoteUpdate = loadUpdate(txn, latest.remote.messageId); Update remoteUpdate = loadUpdate(txn, latest.remote.messageId);
Map<ClientMajorVersion, Visibility> visibilities = Map<ClientMajorVersion, Visibility> visibilities =
@@ -110,6 +105,24 @@ class ClientVersioningManagerImpl implements ClientVersioningManager, Client,
} }
} }
@Override
public int getClientMinorVersion(Transaction txn, ContactId contactId,
ClientId clientId, int majorVersion) throws DbException {
try {
LatestUpdates latest = findLatestUpdates(txn, contactId);
if (latest == null || latest.remote == null) return -1;
Update remoteUpdate = loadUpdate(txn, latest.remote.messageId);
ClientMajorVersion cv =
new ClientMajorVersion(clientId, majorVersion);
for (ClientState remote : remoteUpdate.states) {
if (remote.majorVersion.equals(cv)) return remote.minorVersion;
}
return -1;
} catch (FormatException e) {
throw new DbException(e);
}
}
@Override @Override
public void createLocalState(Transaction txn) throws DbException { public void createLocalState(Transaction txn) throws DbException {
if (db.containsGroup(txn, localGroup.getId())) return; if (db.containsGroup(txn, localGroup.getId())) return;
@@ -336,6 +349,17 @@ class ClientVersioningManagerImpl implements ClientVersioningManager, Client,
MAJOR_VERSION, c); MAJOR_VERSION, c);
} }
@Nullable
private LatestUpdates findLatestUpdates(Transaction txn, ContactId c)
throws DbException, FormatException {
Contact contact = db.getContact(txn, c);
Group g = getContactGroup(contact);
// Contact may be in the process of being added or removed, so
// contact group may not exist
if (!db.containsGroup(txn, g.getId())) return null;
return findLatestUpdates(txn, g.getId());
}
private LatestUpdates findLatestUpdates(Transaction txn, GroupId g) private LatestUpdates findLatestUpdates(Transaction txn, GroupId g)
throws DbException, FormatException { throws DbException, FormatException {
Map<MessageId, BdfDictionary> metadata = Map<MessageId, BdfDictionary> metadata =

View File

@@ -0,0 +1,86 @@
package org.briarproject.bramble;
import org.briarproject.bramble.test.BrambleTestCase;
import org.junit.Test;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import static java.util.Arrays.asList;
import static java.util.concurrent.Executors.newSingleThreadExecutor;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public class PriorityExecutorTest extends BrambleTestCase {
@Test
public void testHighPriorityTasksAreDelegatedInOrderOfSubmission()
throws Exception {
Executor delegate = newSingleThreadExecutor();
PriorityExecutor priority = new PriorityExecutor(delegate);
Executor high = priority.getHighPriorityExecutor();
testTasksAreDelegatedInOrderOfSubmission(high);
}
@Test
public void testLowPriorityTasksAreDelegatedInOrderOfSubmission()
throws Exception {
Executor delegate = newSingleThreadExecutor();
PriorityExecutor priority = new PriorityExecutor(delegate);
Executor low = priority.getLowPriorityExecutor();
testTasksAreDelegatedInOrderOfSubmission(low);
}
@Test
public void testHighPriorityTasksAreRunFirst() throws Exception {
Executor delegate = newSingleThreadExecutor();
PriorityExecutor priority = new PriorityExecutor(delegate);
Executor high = priority.getHighPriorityExecutor();
Executor low = priority.getLowPriorityExecutor();
// Submit a task that will block, causing other tasks to be queued
CountDownLatch cork = new CountDownLatch(1);
low.execute(() -> {
try {
cork.await();
} catch (InterruptedException e) {
fail();
}
});
// Submit alternating tasks to the high and low priority executors
List<Integer> results = new Vector<>();
CountDownLatch tasksFinished = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
int result = i;
Runnable task = () -> {
results.add(result);
tasksFinished.countDown();
};
if (i % 2 == 0) high.execute(task);
else low.execute(task);
}
// Release the cork and wait for all tasks to finish
cork.countDown();
tasksFinished.await();
// The high-priority tasks should have run before the low-priority tasks
assertEquals(asList(0, 2, 4, 6, 8, 1, 3, 5, 7, 9), results);
}
private void testTasksAreDelegatedInOrderOfSubmission(Executor e)
throws Exception {
List<Integer> results = new Vector<>();
CountDownLatch tasksFinished = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
int result = i;
e.execute(() -> {
results.add(result);
tasksFinished.countDown();
});
}
// Wait for all the tasks to finish
tasksFinished.await();
// The tasks should have run in the order they were submitted
assertEquals(asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), results);
}
}

View File

@@ -4,8 +4,6 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageFactory; import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.tree.TreeHash;
import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
@@ -29,10 +27,4 @@ public class TestMessageFactory implements MessageFactory {
System.arraycopy(body, 0, raw, MESSAGE_HEADER_LENGTH, body.length); System.arraycopy(body, 0, raw, MESSAGE_HEADER_LENGTH, body.length);
return raw; return raw;
} }
@Override
public MessageId getMessageId(GroupId g, long timestamp,
TreeHash rootHash) {
throw new UnsupportedOperationException();
}
} }

View File

@@ -8,6 +8,7 @@ import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfEntry; import org.briarproject.bramble.api.data.BdfEntry;
import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Metadata; import org.briarproject.bramble.api.db.Metadata;
import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.sync.ClientId; import org.briarproject.bramble.api.sync.ClientId;
@@ -43,6 +44,7 @@ import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.briarproject.bramble.versioning.ClientVersioningConstants.GROUP_KEY_CONTACT_ID; import static org.briarproject.bramble.versioning.ClientVersioningConstants.GROUP_KEY_CONTACT_ID;
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL; import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION; import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
public class ClientVersioningManagerImplTest extends BrambleMockTestCase { public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
@@ -657,4 +659,327 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
c.registerClient(clientId, 123, 234, hook); c.registerClient(clientId, 123, 234, hook);
assertFalse(c.incomingMessage(txn, newRemoteUpdate, new Metadata())); assertFalse(c.incomingMessage(txn, newRemoteUpdate, new Metadata()));
} }
@Test
public void testReturnsInvisibleIfContactGroupDoesNotExist()
throws Exception {
expectGetContactGroup(false);
ClientVersioningManagerImpl c = createInstance();
assertEquals(INVISIBLE, c.getClientVisibility(txn, contact.getId(),
clientId, 123));
}
@Test
public void testReturnsInvisibleIfNoRemoteUpdateExists() throws Exception {
MessageId localUpdateId = new MessageId(getRandomId());
BdfDictionary localUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, true));
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(singletonMap(localUpdateId, localUpdateMeta)));
}});
ClientVersioningManagerImpl c = createInstance();
assertEquals(INVISIBLE, c.getClientVisibility(txn, contact.getId(),
clientId, 123));
}
@Test(expected = DbException.class)
public void testThrowsExceptionIfNoLocalUpdateExists() throws Exception {
MessageId remoteUpdateId = new MessageId(getRandomId());
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, false));
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(singletonMap(remoteUpdateId, remoteUpdateMeta)));
}});
ClientVersioningManagerImpl c = createInstance();
c.getClientVisibility(txn, contact.getId(), clientId, 123);
}
@Test
public void testReturnsInvisibleIfClientNotSupportedLocally()
throws Exception {
MessageId localUpdateId = new MessageId(getRandomId());
BdfDictionary localUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, true));
MessageId remoteUpdateId = new MessageId(getRandomId());
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, false));
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
messageMetadata.put(localUpdateId, localUpdateMeta);
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
// The client is supported remotely but not locally
BdfList localUpdateBody = BdfList.of(new BdfList(), 1L);
BdfList remoteUpdateBody = BdfList.of(BdfList.of(
BdfList.of(clientId.getString(), 123, 234, false)), 1L);
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, localUpdateId);
will(returnValue(localUpdateBody));
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody));
}});
ClientVersioningManagerImpl c = createInstance();
assertEquals(INVISIBLE, c.getClientVisibility(txn, contact.getId(),
clientId, 123));
}
@Test
public void testReturnsInvisibleIfClientNotSupportedRemotely()
throws Exception {
MessageId localUpdateId = new MessageId(getRandomId());
BdfDictionary localUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, true));
MessageId remoteUpdateId = new MessageId(getRandomId());
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, false));
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
messageMetadata.put(localUpdateId, localUpdateMeta);
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
// The client is supported locally but not remotely
BdfList localUpdateBody = BdfList.of(BdfList.of(
BdfList.of(clientId.getString(), 123, 234, false)), 1L);
BdfList remoteUpdateBody = BdfList.of(new BdfList(), 1L);
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, localUpdateId);
will(returnValue(localUpdateBody));
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody));
}});
ClientVersioningManagerImpl c = createInstance();
assertEquals(INVISIBLE, c.getClientVisibility(txn, contact.getId(),
clientId, 123));
}
@Test
public void testReturnsVisibleIfClientNotActiveRemotely() throws Exception {
MessageId localUpdateId = new MessageId(getRandomId());
BdfDictionary localUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, true));
MessageId remoteUpdateId = new MessageId(getRandomId());
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, false));
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
messageMetadata.put(localUpdateId, localUpdateMeta);
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
// The client is supported locally and remotely but not active
BdfList localUpdateBody = BdfList.of(BdfList.of(
BdfList.of(clientId.getString(), 123, 234, false)), 1L);
BdfList remoteUpdateBody = BdfList.of(BdfList.of(
BdfList.of(clientId.getString(), 123, 234, false)), 1L);
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, localUpdateId);
will(returnValue(localUpdateBody));
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody));
}});
ClientVersioningManagerImpl c = createInstance();
assertEquals(VISIBLE, c.getClientVisibility(txn, contact.getId(),
clientId, 123));
}
@Test
public void testReturnsSharedIfClientActiveRemotely() throws Exception {
MessageId localUpdateId = new MessageId(getRandomId());
BdfDictionary localUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, true));
MessageId remoteUpdateId = new MessageId(getRandomId());
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, false));
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
messageMetadata.put(localUpdateId, localUpdateMeta);
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
// The client is supported locally and remotely and active
BdfList localUpdateBody = BdfList.of(BdfList.of(
BdfList.of(clientId.getString(), 123, 234, true)), 1L);
BdfList remoteUpdateBody = BdfList.of(BdfList.of(
BdfList.of(clientId.getString(), 123, 234, true)), 1L);
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, localUpdateId);
will(returnValue(localUpdateBody));
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody));
}});
ClientVersioningManagerImpl c = createInstance();
assertEquals(SHARED, c.getClientVisibility(txn, contact.getId(),
clientId, 123));
}
@Test
public void testReturnsNegativeIfContactGroupDoesNotExist()
throws Exception {
expectGetContactGroup(false);
ClientVersioningManagerImpl c = createInstance();
assertEquals(-1, c.getClientMinorVersion(txn, contact.getId(),
clientId, 123));
}
@Test
public void testReturnsNegativeIfNoRemoteUpdateExists() throws Exception {
MessageId localUpdateId = new MessageId(getRandomId());
BdfDictionary localUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, true));
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(singletonMap(localUpdateId, localUpdateMeta)));
}});
ClientVersioningManagerImpl c = createInstance();
assertEquals(-1, c.getClientMinorVersion(txn, contact.getId(),
clientId, 123));
}
@Test
public void testReturnsNegativeIfClientNotSupportedRemotely()
throws Exception {
MessageId localUpdateId = new MessageId(getRandomId());
BdfDictionary localUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, true));
MessageId remoteUpdateId = new MessageId(getRandomId());
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, false));
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
messageMetadata.put(localUpdateId, localUpdateMeta);
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
// The client is not supported remotely
BdfList remoteUpdateBody = BdfList.of(new BdfList(), 1L);
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody));
}});
ClientVersioningManagerImpl c = createInstance();
assertEquals(-1, c.getClientMinorVersion(txn, contact.getId(),
clientId, 123));
}
@Test
public void testReturnsMinorVersionIfClientNotActiveRemotely()
throws Exception {
MessageId localUpdateId = new MessageId(getRandomId());
BdfDictionary localUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, true));
MessageId remoteUpdateId = new MessageId(getRandomId());
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, false));
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
messageMetadata.put(localUpdateId, localUpdateMeta);
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
// The client is supported remotely but not active
BdfList remoteUpdateBody = BdfList.of(BdfList.of(
BdfList.of(clientId.getString(), 123, 234, false)), 1L);
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody));
}});
ClientVersioningManagerImpl c = createInstance();
assertEquals(234, c.getClientMinorVersion(txn, contact.getId(),
clientId, 123));
}
@Test
public void testReturnsMinorVersionIfClientActiveRemotely()
throws Exception {
MessageId localUpdateId = new MessageId(getRandomId());
BdfDictionary localUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, true));
MessageId remoteUpdateId = new MessageId(getRandomId());
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, false));
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
messageMetadata.put(localUpdateId, localUpdateMeta);
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
// The client is supported remotely and active
BdfList remoteUpdateBody = BdfList.of(BdfList.of(
BdfList.of(clientId.getString(), 123, 234, true)), 1L);
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody));
}});
ClientVersioningManagerImpl c = createInstance();
assertEquals(234, c.getClientMinorVersion(txn, contact.getId(),
clientId, 123));
}
private void expectGetContactGroup(boolean exists) throws Exception {
context.checking(new Expectations() {{
oneOf(db).getContact(txn, contact.getId());
will(returnValue(contact));
oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
MAJOR_VERSION, contact);
will(returnValue(contactGroup));
oneOf(db).containsGroup(txn, contactGroup.getId());
will(returnValue(exists));
}});
}
} }

View File

@@ -116,7 +116,7 @@ dependencies {
implementation 'info.guardianproject.trustedintents:trustedintents:0.2' implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
implementation 'de.hdodenhof:circleimageview:2.2.0' implementation 'de.hdodenhof:circleimageview:2.2.0'
implementation 'com.google.zxing:core:3.3.3' implementation 'com.google.zxing:core:3.3.3'
implementation 'uk.co.samuelwall:material-tap-target-prompt:2.12.4' 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.5.1'
def glideVersion = '4.8.0' def glideVersion = '4.8.0'
implementation("com.github.bumptech.glide:glide:$glideVersion") { implementation("com.github.bumptech.glide:glide:$glideVersion") {
@@ -134,7 +134,7 @@ dependencies {
testImplementation project(path: ':bramble-core', configuration: 'testOutput') testImplementation project(path: ':bramble-core', configuration: 'testOutput')
testImplementation 'org.robolectric:robolectric:4.0.1' testImplementation 'org.robolectric:robolectric:4.0.1'
testImplementation 'org.robolectric:shadows-support-v4:3.3.2' testImplementation 'org.robolectric:shadows-support-v4:3.3.2'
testImplementation 'org.mockito:mockito-core:2.13.0' testImplementation 'org.mockito:mockito-core:2.19.0'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
testImplementation "org.jmock:jmock:2.8.2" testImplementation "org.jmock:jmock:2.8.2"
testImplementation "org.jmock:jmock-junit4:2.8.2" testImplementation "org.jmock:jmock-junit4:2.8.2"

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,279 @@
package org.briarproject.briar.android.conversation;
import android.content.res.AssetManager;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Random;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
public class AttachmentControllerIntegrationTest {
private static final String smallKitten =
"https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/Kitten_in_Rizal_Park%2C_Manila.jpg/160px-Kitten_in_Rizal_Park%2C_Manila.jpg";
private static final String originalKitten =
"https://upload.wikimedia.org/wikipedia/commons/0/06/Kitten_in_Rizal_Park%2C_Manila.jpg";
private static final String pngKitten =
"https://upload.wikimedia.org/wikipedia/commons/c/c8/Young_cat.png";
private static final String uberGif =
"https://raw.githubusercontent.com/fuzzdb-project/fuzzdb/master/attack/file-upload/malicious-images/uber.gif";
private static final String lottaPixel =
"https://raw.githubusercontent.com/fuzzdb-project/fuzzdb/master/attack/file-upload/malicious-images/lottapixel.jpg";
private static final String imageIoCrash =
"https://www.landaire.net/img/crasher.png";
private static final String gimpCrash =
"https://gitlab.gnome.org/GNOME/gimp/uploads/75f5b7ed3b09b3f1c13f1f65bffe784f/31153c919d3aa634e8e6cff82219fe7352dd8a37.png";
private static final String optiPngAfl =
"https://sourceforge.net/p/optipng/bugs/64/attachment/test.gif";
private static final String librawError =
"https://www.libraw.org/sites/libraw.org/files/P1010671.JPG";
private final AttachmentDimensions dimensions = new AttachmentDimensions(
100, 50, 200, 75, 300
);
private final MessageId msgId = new MessageId(getRandomId());
private final AttachmentController controller =
new AttachmentController(null, dimensions);
@Test
public void testSmallJpegImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(smallKitten);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(msgId, item.getMessageId());
assertEquals(160, item.getWidth());
assertEquals(240, item.getHeight());
assertEquals(160, item.getThumbnailWidth());
assertEquals(240, item.getThumbnailHeight());
assertEquals("image/jpeg", item.getMimeType());
assertEquals("jpg", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testBigJpegImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(originalKitten);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(msgId, item.getMessageId());
assertEquals(1728, item.getWidth());
assertEquals(2592, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.maxHeight, item.getThumbnailHeight());
assertEquals("image/jpeg", item.getMimeType());
assertEquals("jpg", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testSmallPngImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/png");
InputStream is = getUrlInputStream(pngKitten);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(msgId, item.getMessageId());
assertEquals(737, item.getWidth());
assertEquals(510, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(138, item.getThumbnailHeight());
assertEquals("image/png", item.getMimeType());
assertEquals("png", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testUberGif() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(uberGif);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(1, item.getWidth());
assertEquals(1, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
assertEquals(dimensions.minHeight, item.getThumbnailHeight());
assertEquals("image/gif", item.getMimeType());
assertEquals("gif", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testLottaPixels() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(lottaPixel);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(64250, item.getWidth());
assertEquals(64250, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.maxWidth, item.getThumbnailHeight());
assertEquals("image/jpeg", item.getMimeType());
assertEquals("jpg", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testImageIoCrash() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(imageIoCrash);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(1184, item.getWidth());
assertEquals(448, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.minHeight, item.getThumbnailHeight());
assertEquals("image/png", item.getMimeType());
assertEquals("png", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testGimpCrash() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(gimpCrash);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(1, item.getWidth());
assertEquals(1, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
assertEquals(dimensions.minHeight, item.getThumbnailHeight());
assertEquals("image/gif", item.getMimeType());
assertEquals("gif", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testOptiPngAfl() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(optiPngAfl);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(32, item.getWidth());
assertEquals(32, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
assertEquals(dimensions.minHeight, item.getThumbnailHeight());
assertEquals("image/gif", item.getMimeType());
assertEquals("gif", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testLibrawError() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(librawError);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertTrue(item.hasError());
}
@Test
public void testSmallAnimatedGifMaxDimensions() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("animated.gif");
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(65535, item.getWidth());
assertEquals(65535, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.maxWidth, item.getThumbnailHeight());
assertEquals("image/gif", item.getMimeType());
assertEquals("gif", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testSmallAnimatedGifHugeDimensions() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("animated2.gif");
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(10000, item.getWidth());
assertEquals(10000, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.maxWidth, item.getThumbnailHeight());
assertEquals("image/gif", item.getMimeType());
assertEquals("gif", item.getExtension());
assertFalse(item.hasError());
}
@Test
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 = controller.getAttachmentItem(h, a, true);
assertEquals(16384, item.getWidth());
assertEquals(16384, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.maxWidth, item.getThumbnailHeight());
assertEquals("image/gif", item.getMimeType());
assertEquals("gif", item.getExtension());
assertFalse(item.hasError());
}
@Test
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 = controller.getAttachmentItem(h, a, true);
assertEquals(1, item.getWidth());
assertEquals(10000, item.getHeight());
assertEquals(dimensions.minWidth, item.getThumbnailWidth());
assertEquals(dimensions.maxHeight, item.getThumbnailHeight());
assertEquals("image/jpeg", item.getMimeType());
assertEquals("jpg", item.getExtension());
assertFalse(item.hasError());
}
@Test
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 = controller.getAttachmentItem(h, a, true);
assertEquals(1920, item.getWidth());
assertEquals(1, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.minHeight, item.getThumbnailHeight());
assertEquals("image/jpeg", item.getMimeType());
assertEquals("jpg", item.getExtension());
assertFalse(item.hasError());
}
private InputStream getUrlInputStream(String url) throws IOException {
return new URL(url).openStream();
}
private InputStream getAssetInputStream(String name) throws IOException {
AssetManager assets = InstrumentationRegistry.getContext().getAssets();
return assets.open(name);
}
public static byte[] getRandomBytes(int length) {
byte[] b = new byte[length];
new Random().nextBytes(b);
return b;
}
public static byte[] getRandomId() {
return getRandomBytes(UniqueId.LENGTH);
}
}

View File

@@ -0,0 +1,38 @@
package org.briarproject.briar.android.conversation;
import android.content.Context;
import android.content.Intent;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import org.briarproject.briar.R;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static android.support.test.InstrumentationRegistry.getInstrumentation;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.briarproject.briar.android.ViewActions.waitUntilMatches;
import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
@RunWith(AndroidJUnit4.class)
public class ConversationActivityNotSignedInTest {
@Rule
public ActivityTestRule<ConversationActivity> testRule =
new ActivityTestRule<>(ConversationActivity.class, false, false);
@Test
public void openWithoutSignedIn() {
Context targetContext = getInstrumentation().getTargetContext();
Intent intent = new Intent(targetContext, ConversationActivity.class);
intent.putExtra(CONTACT_ID, 1);
testRule.launchActivity(intent);
onView(withText(R.string.sign_in_button))
.perform(waitUntilMatches(isDisplayed()));
}
}

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest <manifest
package="org.briarproject.briar" package="org.briarproject.briar"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.bluetooth" android:required="false"/> <uses-feature android:name="android.hardware.bluetooth" android:required="false"/>
@@ -17,6 +18,7 @@
<uses-permission android:name="android.permission.USE_FINGERPRINT"/> <uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/> <uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission-sdk-23 android:name="android.permission.USE_BIOMETRIC" /> <uses-permission-sdk-23 android:name="android.permission.USE_BIOMETRIC" />
@@ -28,7 +30,8 @@
android:label="@string/app_name" android:label="@string/app_name"
android:logo="@mipmap/ic_launcher_round" android:logo="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/BriarTheme"> android:theme="@style/BriarTheme"
tools:ignore="GoogleAppIndexingWarning">
<receiver <receiver
android:name="org.briarproject.briar.android.login.SignInReminderReceiver" android:name="org.briarproject.briar.android.login.SignInReminderReceiver"
@@ -116,7 +119,7 @@
<activity <activity
android:name=".android.conversation.ImageActivity" android:name=".android.conversation.ImageActivity"
android:parentActivityName="org.briarproject.briar.android.conversation.ConversationActivity" android:parentActivityName="org.briarproject.briar.android.conversation.ConversationActivity"
android:theme="@style/BriarTheme.Transparent.NoActionBar"> android:theme="@style/BriarTheme.ActionBarOverlay">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.conversation.ConversationActivity"/> android:value="org.briarproject.briar.android.conversation.ConversationActivity"/>
@@ -154,7 +157,7 @@
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/> android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
</activity> </activity>
<activity <activity
android:name="org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity" android:name="org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity"
android:label="@string/groups_member_list" android:label="@string/groups_member_list"
android:parentActivityName="org.briarproject.briar.android.privategroup.conversation.GroupActivity" android:parentActivityName="org.briarproject.briar.android.privategroup.conversation.GroupActivity"
@@ -397,7 +400,7 @@
<activity <activity
android:name="org.briarproject.briar.android.panic.PanicResponderActivity" android:name="org.briarproject.briar.android.panic.PanicResponderActivity"
android:noHistory="true" android:noHistory="true"
android:theme="@style/Theme.AppCompat.NoActionBar"> android:theme="@style/TranslucentTheme">
<!-- this can never have launchMode singleTask or singleInstance! --> <!-- this can never have launchMode singleTask or singleInstance! -->
<intent-filter> <intent-filter>
<action android:name="info.guardianproject.panic.action.TRIGGER"/> <action android:name="info.guardianproject.panic.action.TRIGGER"/>
@@ -407,12 +410,12 @@
<activity <activity
android:name="org.briarproject.briar.android.logout.ExitActivity" android:name="org.briarproject.briar.android.logout.ExitActivity"
android:theme="@style/Theme.AppCompat.NoActionBar"> android:theme="@android:style/Theme.NoDisplay">
</activity> </activity>
<activity <activity
android:name=".android.logout.HideUiActivity" android:name=".android.logout.HideUiActivity"
android:theme="@style/Theme.AppCompat.NoActionBar"> android:theme="@android:style/Theme.NoDisplay">
</activity> </activity>
<activity <activity

View File

@@ -32,7 +32,7 @@ import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.conversation.glide.BriarModelLoader; import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
import org.briarproject.briar.android.login.SignInReminderReceiver; import org.briarproject.briar.android.login.SignInReminderReceiver;
import org.briarproject.briar.android.reporting.BriarReportSender; import org.briarproject.briar.android.reporting.BriarReportSender;
import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.EmojiTextInputView;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.android.DozeWatchdog; import org.briarproject.briar.api.android.DozeWatchdog;
import org.briarproject.briar.api.android.LockManager; import org.briarproject.briar.api.android.LockManager;
@@ -169,7 +169,7 @@ public interface AndroidComponent
void inject(NotificationCleanupService notificationCleanupService); void inject(NotificationCleanupService notificationCleanupService);
void inject(TextInputView textInputView); void inject(EmojiTextInputView textInputView);
void inject(BriarModelLoader briarModelLoader); void inject(BriarModelLoader briarModelLoader);

View File

@@ -0,0 +1,54 @@
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

@@ -16,4 +16,6 @@ public interface BriarApplication {
AndroidComponent getApplicationComponent(); AndroidComponent getApplicationComponent();
SharedPreferences getDefaultSharedPreferences(); SharedPreferences getDefaultSharedPreferences();
boolean isRunningInBackground();
} }

View File

@@ -1,5 +1,7 @@
package org.briarproject.briar.android; package org.briarproject.briar.android;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.app.Application; import android.app.Application;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@@ -30,6 +32,8 @@ import java.util.logging.Handler;
import java.util.logging.LogRecord; import java.util.logging.LogRecord;
import java.util.logging.Logger; 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.FINE;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static org.acra.ReportField.ANDROID_VERSION; import static org.acra.ReportField.ANDROID_VERSION;
@@ -79,6 +83,7 @@ public class BriarApplicationImpl extends Application
Logger.getLogger(BriarApplicationImpl.class.getName()); Logger.getLogger(BriarApplicationImpl.class.getName());
private final CachingLogHandler logHandler = new CachingLogHandler(); private final CachingLogHandler logHandler = new CachingLogHandler();
private final BackgroundMonitor backgroundMonitor = new BackgroundMonitor();
private AndroidComponent applicationComponent; private AndroidComponent applicationComponent;
private volatile SharedPreferences prefs; private volatile SharedPreferences prefs;
@@ -115,6 +120,9 @@ public class BriarApplicationImpl extends Application
applicationComponent = createApplicationComponent(); applicationComponent = createApplicationComponent();
EmojiManager.install(new GoogleEmojiProvider()); EmojiManager.install(new GoogleEmojiProvider());
if (SDK_INT < 16)
registerActivityLifecycleCallbacks(backgroundMonitor);
} }
protected AndroidComponent createApplicationComponent() { protected AndroidComponent createApplicationComponent() {
@@ -173,4 +181,15 @@ public class BriarApplicationImpl extends Application
public SharedPreferences getDefaultSharedPreferences() { public SharedPreferences getDefaultSharedPreferences() {
return prefs; return prefs;
} }
@Override
public boolean isRunningInBackground() {
if (SDK_INT >= 16) {
RunningAppProcessInfo info = new RunningAppProcessInfo();
ActivityManager.getMyMemoryState(info);
return (info.importance != IMPORTANCE_FOREGROUND);
} else {
return backgroundMonitor.isRunningInBackground();
}
}
} }

View File

@@ -1,7 +1,5 @@
package org.briarproject.briar.android; package org.briarproject.briar.android;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
@@ -35,7 +33,6 @@ import java.util.logging.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_NONE; import static android.app.NotificationManager.IMPORTANCE_NONE;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
@@ -74,6 +71,7 @@ public class BriarService extends Service {
@Nullable @Nullable
private BroadcastReceiver receiver = null; private BroadcastReceiver receiver = null;
private BriarApplication app;
@Inject @Inject
AndroidNotificationManager notificationManager; AndroidNotificationManager notificationManager;
@@ -93,8 +91,8 @@ public class BriarService extends Service {
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
BriarApplication application = (BriarApplication) getApplication(); app = (BriarApplication) getApplication();
application.getApplicationComponent().inject(this); app.getApplicationComponent().inject(this);
LOG.info("Created"); LOG.info("Created");
if (created.getAndSet(true)) { if (created.getAndSet(true)) {
@@ -220,8 +218,8 @@ public class BriarService extends Service {
public void onLowMemory() { public void onLowMemory() {
super.onLowMemory(); super.onLowMemory();
LOG.warning("Memory is low"); LOG.warning("Memory is low");
// Clear the UI - this is done in onTrimMemory() if SDK_INT >= 16 // If we're not in the foreground, clear the UI to save memory
if (SDK_INT < 16) hideUi(); if (app.isRunningInBackground()) hideUi();
} }
@Override @Override
@@ -235,20 +233,16 @@ public class BriarService extends Service {
LOG.info("Trim memory: near middle of LRU list"); LOG.info("Trim memory: near middle of LRU list");
} else if (level == TRIM_MEMORY_COMPLETE) { } else if (level == TRIM_MEMORY_COMPLETE) {
LOG.info("Trim memory: near end of LRU list"); LOG.info("Trim memory: near end of LRU list");
} else if (SDK_INT >= 16) { } else if (level == TRIM_MEMORY_RUNNING_MODERATE) {
if (level == TRIM_MEMORY_RUNNING_MODERATE) { LOG.info("Trim memory: running moderately low");
LOG.info("Trim memory: running moderately low"); } else if (level == TRIM_MEMORY_RUNNING_LOW) {
} else if (level == TRIM_MEMORY_RUNNING_LOW) { LOG.info("Trim memory: running low");
LOG.info("Trim memory: running low"); } else if (level == TRIM_MEMORY_RUNNING_CRITICAL) {
} else if (level == TRIM_MEMORY_RUNNING_CRITICAL) { // This level may be received if SDK_INT < 16, although the
LOG.info("Trim memory: running critically low"); // constant isn't declared until API level 16
// If we're not in the foreground, clear the UI to save memory LOG.warning("Trim memory: running critically low");
RunningAppProcessInfo info = new RunningAppProcessInfo(); // If we're not in the foreground, clear the UI to save memory
ActivityManager.getMyMemoryState(info); if (app.isRunningInBackground()) hideUi();
if (info.importance != IMPORTANCE_FOREGROUND) hideUi();
} else if (LOG.isLoggable(INFO)) {
LOG.info("Trim memory: unknown level " + level);
}
} else if (LOG.isLoggable(INFO)) { } else if (LOG.isLoggable(INFO)) {
LOG.info("Trim memory: unknown level " + level); LOG.info("Trim memory: unknown level " + level);
} }

View File

@@ -3,22 +3,29 @@ package org.briarproject.briar.android;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BaseActivity; import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener; import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import org.briarproject.briar.android.fragment.ErrorFragment; import org.briarproject.briar.android.fragment.ErrorFragment;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult;
import static org.briarproject.briar.android.BriarService.EXTRA_NOTIFICATION_ID; import static org.briarproject.briar.android.BriarService.EXTRA_NOTIFICATION_ID;
import static org.briarproject.briar.android.BriarService.EXTRA_START_RESULT; import static org.briarproject.briar.android.BriarService.EXTRA_START_RESULT;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class StartupFailureActivity extends BaseActivity implements public class StartupFailureActivity extends BaseActivity implements
BaseFragmentListener { BaseFragmentListener {
@Override @Override
public void onCreate(Bundle state) { public void onCreate(@Nullable Bundle state) {
super.onCreate(state); super.onCreate(state);
setContentView(R.layout.activity_fragment_container); setContentView(R.layout.activity_fragment_container);
@@ -38,7 +45,7 @@ public class StartupFailureActivity extends BaseActivity implements
// cancel notification // cancel notification
if (notificationId > -1) { if (notificationId > -1) {
Object o = getSystemService(NOTIFICATION_SERVICE); Object o = getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o; NotificationManager nm = (NotificationManager) requireNonNull(o);
nm.cancel(notificationId); nm.cancel(notificationId);
} }
@@ -66,7 +73,7 @@ public class StartupFailureActivity extends BaseActivity implements
} }
@Override @Override
public void runOnDbThread(Runnable runnable) { public void runOnDbThread(@NonNull Runnable runnable) {
throw new AssertionError("Deprecated and should not be used"); throw new AssertionError("Deprecated and should not be used");
} }

View File

@@ -30,4 +30,10 @@ public interface TestingConstants {
long EXPIRY_DATE = IS_DEBUG_BUILD || IS_BETA_BUILD ? long EXPIRY_DATE = IS_DEBUG_BUILD || IS_BETA_BUILD ?
BuildConfig.BuildTimestamp + 90 * 24 * 60 * 60 * 1000L : BuildConfig.BuildTimestamp + 90 * 24 * 60 * 60 * 1000L :
Long.MAX_VALUE; Long.MAX_VALUE;
/**
* Feature flag for enabling image attachments.
*/
boolean FEATURE_FLAG_IMAGE_ATTACHMENTS = IS_DEBUG_BUILD;
} }

View File

@@ -20,6 +20,7 @@ import org.briarproject.briar.android.contact.ContactModule;
import org.briarproject.briar.android.conversation.AliasDialogFragment; import org.briarproject.briar.android.conversation.AliasDialogFragment;
import org.briarproject.briar.android.conversation.ConversationActivity; import org.briarproject.briar.android.conversation.ConversationActivity;
import org.briarproject.briar.android.conversation.ImageActivity; import org.briarproject.briar.android.conversation.ImageActivity;
import org.briarproject.briar.android.conversation.ImageFragment;
import org.briarproject.briar.android.forum.CreateForumActivity; import org.briarproject.briar.android.forum.CreateForumActivity;
import org.briarproject.briar.android.forum.ForumActivity; import org.briarproject.briar.android.forum.ForumActivity;
import org.briarproject.briar.android.forum.ForumListFragment; import org.briarproject.briar.android.forum.ForumListFragment;
@@ -30,7 +31,6 @@ import org.briarproject.briar.android.introduction.IntroductionActivity;
import org.briarproject.briar.android.introduction.IntroductionMessageFragment; import org.briarproject.briar.android.introduction.IntroductionMessageFragment;
import org.briarproject.briar.android.keyagreement.ContactExchangeActivity; import org.briarproject.briar.android.keyagreement.ContactExchangeActivity;
import org.briarproject.briar.android.keyagreement.ContactExchangeErrorFragment; import org.briarproject.briar.android.keyagreement.ContactExchangeErrorFragment;
import org.briarproject.briar.android.keyagreement.IntroFragment;
import org.briarproject.briar.android.keyagreement.KeyAgreementActivity; import org.briarproject.briar.android.keyagreement.KeyAgreementActivity;
import org.briarproject.briar.android.keyagreement.KeyAgreementFragment; import org.briarproject.briar.android.keyagreement.KeyAgreementFragment;
import org.briarproject.briar.android.login.AuthorNameFragment; import org.briarproject.briar.android.login.AuthorNameFragment;
@@ -48,7 +48,6 @@ import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
import org.briarproject.briar.android.privategroup.conversation.GroupConversationModule; import org.briarproject.briar.android.privategroup.conversation.GroupConversationModule;
import org.briarproject.briar.android.privategroup.creation.CreateGroupActivity; import org.briarproject.briar.android.privategroup.creation.CreateGroupActivity;
import org.briarproject.briar.android.privategroup.creation.CreateGroupFragment; import org.briarproject.briar.android.privategroup.creation.CreateGroupFragment;
import org.briarproject.briar.android.privategroup.creation.CreateGroupMessageFragment;
import org.briarproject.briar.android.privategroup.creation.CreateGroupModule; import org.briarproject.briar.android.privategroup.creation.CreateGroupModule;
import org.briarproject.briar.android.privategroup.creation.GroupInviteActivity; import org.briarproject.briar.android.privategroup.creation.GroupInviteActivity;
import org.briarproject.briar.android.privategroup.creation.GroupInviteFragment; import org.briarproject.briar.android.privategroup.creation.GroupInviteFragment;
@@ -69,10 +68,8 @@ import org.briarproject.briar.android.sharing.ForumInvitationActivity;
import org.briarproject.briar.android.sharing.ForumSharingStatusActivity; import org.briarproject.briar.android.sharing.ForumSharingStatusActivity;
import org.briarproject.briar.android.sharing.ShareBlogActivity; import org.briarproject.briar.android.sharing.ShareBlogActivity;
import org.briarproject.briar.android.sharing.ShareBlogFragment; import org.briarproject.briar.android.sharing.ShareBlogFragment;
import org.briarproject.briar.android.sharing.ShareBlogMessageFragment;
import org.briarproject.briar.android.sharing.ShareForumActivity; import org.briarproject.briar.android.sharing.ShareForumActivity;
import org.briarproject.briar.android.sharing.ShareForumFragment; import org.briarproject.briar.android.sharing.ShareForumFragment;
import org.briarproject.briar.android.sharing.ShareForumMessageFragment;
import org.briarproject.briar.android.sharing.SharingModule; import org.briarproject.briar.android.sharing.SharingModule;
import org.briarproject.briar.android.splash.SplashScreenActivity; import org.briarproject.briar.android.splash.SplashScreenActivity;
import org.briarproject.briar.android.test.TestDataActivity; import org.briarproject.briar.android.test.TestDataActivity;
@@ -182,8 +179,6 @@ public interface ActivityComponent {
void inject(CreateGroupFragment fragment); void inject(CreateGroupFragment fragment);
void inject(CreateGroupMessageFragment fragment);
void inject(GroupListFragment fragment); void inject(GroupListFragment fragment);
void inject(GroupInviteFragment fragment); void inject(GroupInviteFragment fragment);
@@ -194,20 +189,14 @@ public interface ActivityComponent {
void inject(FeedFragment fragment); void inject(FeedFragment fragment);
void inject(IntroFragment fragment);
void inject(KeyAgreementFragment fragment); void inject(KeyAgreementFragment fragment);
void inject(ContactChooserFragment fragment); void inject(ContactChooserFragment fragment);
void inject(ShareForumFragment fragment); void inject(ShareForumFragment fragment);
void inject(ShareForumMessageFragment fragment);
void inject(ShareBlogFragment fragment); void inject(ShareBlogFragment fragment);
void inject(ShareBlogMessageFragment fragment);
void inject(IntroductionMessageFragment fragment); void inject(IntroductionMessageFragment fragment);
void inject(SettingsFragment fragment); void inject(SettingsFragment fragment);
@@ -218,4 +207,6 @@ public interface ActivityComponent {
void inject(AliasDialogFragment aliasDialogFragment); void inject(AliasDialogFragment aliasDialogFragment);
void inject(ImageFragment imageFragment);
} }

View File

@@ -15,6 +15,8 @@ import android.view.ViewGroup.LayoutParams;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.AndroidComponent; import org.briarproject.briar.android.AndroidComponent;
import org.briarproject.briar.android.BriarApplication; import org.briarproject.briar.android.BriarApplication;
@@ -51,6 +53,8 @@ import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOT
* Warning: Some activities don't extend {@link BaseActivity}. * Warning: Some activities don't extend {@link BaseActivity}.
* E.g. {@link DevReportActivity} * E.g. {@link DevReportActivity}
*/ */
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public abstract class BaseActivity extends AppCompatActivity public abstract class BaseActivity extends AppCompatActivity
implements DestroyableContext, OnTapFilteredListener { implements DestroyableContext, OnTapFilteredListener {
@@ -77,6 +81,17 @@ public abstract class BaseActivity extends AppCompatActivity
@Override @Override
public void onCreate(@Nullable Bundle state) { public void onCreate(@Nullable Bundle state) {
// create the ActivityComponent *before* calling super.onCreate()
// because it already attaches fragments which need access
// to the component for their own injection
AndroidComponent applicationComponent =
((BriarApplication) getApplication()).getApplicationComponent();
activityComponent = DaggerActivityComponent.builder()
.androidComponent(applicationComponent)
.activityModule(getActivityModule())
.forumModule(getForumModule())
.build();
injectActivity(activityComponent);
super.onCreate(state); super.onCreate(state);
// WARNING: When removing this or making it possible to turn it off, // WARNING: When removing this or making it possible to turn it off,
@@ -86,17 +101,6 @@ public abstract class BaseActivity extends AppCompatActivity
// unlock screen is shown. // unlock screen is shown.
if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE); if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE);
AndroidComponent applicationComponent =
((BriarApplication) getApplication()).getApplicationComponent();
activityComponent = DaggerActivityComponent.builder()
.androidComponent(applicationComponent)
.activityModule(getActivityModule())
.forumModule(getForumModule())
.build();
injectActivity(activityComponent);
for (ActivityLifecycleController alc : lifecycleControllers) { for (ActivityLifecycleController alc : lifecycleControllers) {
alc.onActivityCreate(this); alc.onActivityCreate(this);
} }

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.android.activity; package org.briarproject.briar.android.activity;
import android.annotation.SuppressLint;
import android.content.Intent; import android.content.Intent;
import android.support.annotation.RequiresApi; import android.support.annotation.RequiresApi;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
@@ -10,6 +9,8 @@ import android.transition.Transition;
import android.view.Window; import android.view.Window;
import android.widget.CheckBox; import android.widget.CheckBox;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.controller.BriarController; import org.briarproject.briar.android.controller.BriarController;
import org.briarproject.briar.android.controller.DbController; import org.briarproject.briar.android.controller.DbController;
@@ -36,7 +37,8 @@ import static org.briarproject.briar.android.util.UiUtils.excludeSystemUi;
import static org.briarproject.briar.android.util.UiUtils.getDozeWhitelistingIntent; import static org.briarproject.briar.android.util.UiUtils.getDozeWhitelistingIntent;
import static org.briarproject.briar.android.util.UiUtils.isSamsung7; import static org.briarproject.briar.android.util.UiUtils.isSamsung7;
@SuppressLint("Registered") @MethodsNotNullByDefault
@ParametersNotNullByDefault
public abstract class BriarActivity extends BaseActivity { public abstract class BriarActivity extends BaseActivity {
public static final String GROUP_ID = "briar.GROUP_ID"; public static final String GROUP_ID = "briar.GROUP_ID";
@@ -60,7 +62,8 @@ public abstract class BriarActivity extends BaseActivity {
} }
@Override @Override
protected void onActivityResult(int request, int result, Intent data) { protected void onActivityResult(int request, int result,
@Nullable Intent data) {
super.onActivityResult(request, result, data); super.onActivityResult(request, result, data);
if (request == REQUEST_PASSWORD) { if (request == REQUEST_PASSWORD) {
// The result can be RESULT_CANCELED if there's no account // The result can be RESULT_CANCELED if there's no account
@@ -89,7 +92,7 @@ public abstract class BriarActivity extends BaseActivity {
} else if (lockManager.isLocked() && !isFinishing()) { } else if (lockManager.isLocked() && !isFinishing()) {
// Also check that the activity isn't finishing already. // Also check that the activity isn't finishing already.
// This is possible if we finished in onActivityResult(). // This is possible if we finished in onActivityResult().
// Lauching another UnlockActivity would cause a loop. // Launching another UnlockActivity would cause a loop.
Intent i = new Intent(this, UnlockActivity.class); Intent i = new Intent(this, UnlockActivity.class);
startActivityForResult(i, REQUEST_UNLOCK); startActivityForResult(i, REQUEST_UNLOCK);
} else if (SDK_INT >= 23) { } else if (SDK_INT >= 23) {
@@ -111,6 +114,10 @@ public abstract class BriarActivity extends BaseActivity {
lockManager.onActivityStop(); lockManager.onActivityStop();
} }
protected boolean signedIn() {
return briarController.accountSignedIn();
}
/** /**
* Sets the transition animations. * Sets the transition animations.
* @param enterTransition used to move views into initial positions * @param enterTransition used to move views into initial positions

View File

@@ -14,5 +14,7 @@ public interface RequestCodes {
int REQUEST_BLUETOOTH_DISCOVERABLE = 10; int REQUEST_BLUETOOTH_DISCOVERABLE = 10;
int REQUEST_UNLOCK = 11; int REQUEST_UNLOCK = 11;
int REQUEST_KEYGUARD_UNLOCK = 12; int REQUEST_KEYGUARD_UNLOCK = 12;
int REQUEST_ATTACH_IMAGE = 13;
int REQUEST_SAVE_ATTACHMENT = 14;
} }

View File

@@ -24,6 +24,8 @@ import javax.annotation.Nullable;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.view.View.INVISIBLE; import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.util.UiUtils.MIN_DATE_RESOLUTION; import static org.briarproject.briar.android.util.UiUtils.MIN_DATE_RESOLUTION;
@@ -35,7 +37,7 @@ abstract class BasePostFragment extends BaseFragment {
static final String POST_ID = "briar.POST_ID"; static final String POST_ID = "briar.POST_ID";
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(BasePostFragment.class.getName()); getLogger(BasePostFragment.class.getName());
private final Handler handler = new Handler(Looper.getMainLooper()); private final Handler handler = new Handler(Looper.getMainLooper());
@@ -52,7 +54,7 @@ abstract class BasePostFragment extends BaseFragment {
@Nullable ViewGroup container, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) { @Nullable Bundle savedInstanceState) {
// retrieve MessageId of blog post from arguments // retrieve MessageId of blog post from arguments
byte[] p = getArguments().getByteArray(POST_ID); byte[] p = requireNonNull(getArguments()).getByteArray(POST_ID);
if (p == null) throw new IllegalStateException("No post ID in args"); if (p == null) throw new IllegalStateException("No post ID in args");
postId = new MessageId(p); postId = new MessageId(p);
@@ -68,6 +70,7 @@ abstract class BasePostFragment extends BaseFragment {
@Override @Override
public void onAuthorClick(BlogPostItem post) { public void onAuthorClick(BlogPostItem post) {
if (getContext() == null) return;
Intent i = new Intent(getContext(), BlogActivity.class); Intent i = new Intent(getContext(), BlogActivity.class);
i.putExtra(GROUP_ID, post.getGroupId().getBytes()); i.putExtra(GROUP_ID, post.getGroupId().getBytes());
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);

View File

@@ -43,6 +43,7 @@ import javax.inject.Inject;
import static android.app.Activity.RESULT_OK; import static android.app.Activity.RESULT_OK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.widget.Toast.LENGTH_SHORT; import static android.widget.Toast.LENGTH_SHORT;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_BLOG; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_BLOG;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST;
@@ -69,7 +70,6 @@ public class BlogFragment extends BaseFragment
private boolean isMyBlog = false, canDeleteBlog = false; private boolean isMyBlog = false, canDeleteBlog = false;
static BlogFragment newInstance(GroupId groupId) { static BlogFragment newInstance(GroupId groupId) {
BlogFragment f = new BlogFragment(); BlogFragment f = new BlogFragment();
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
@@ -79,20 +79,27 @@ public class BlogFragment extends BaseFragment
return f; return f;
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
blogController.setBlogSharingListener(this);
sharingController.setSharingListener(this);
}
@Nullable @Nullable
@Override @Override
public View onCreateView(LayoutInflater inflater, public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) { @Nullable Bundle savedInstanceState) {
Bundle args = getArguments(); Bundle args = requireNonNull(getArguments());
byte[] b = args.getByteArray(GROUP_ID); byte[] b = args.getByteArray(GROUP_ID);
if (b == null) throw new IllegalStateException("No group ID in args"); if (b == null) throw new IllegalStateException("No group ID in args");
groupId = new GroupId(b); groupId = new GroupId(b);
View v = inflater.inflate(R.layout.fragment_blog, container, false); View v = inflater.inflate(R.layout.fragment_blog, container, false);
adapter = adapter = new BlogPostAdapter(requireNonNull(getActivity()), this,
new BlogPostAdapter(getActivity(), this, getFragmentManager()); getFragmentManager());
list = v.findViewById(R.id.postList); list = v.findViewById(R.id.postList);
list.setLayoutManager(new LinearLayoutManager(getActivity())); list.setLayoutManager(new LinearLayoutManager(getActivity()));
list.setAdapter(adapter); list.setAdapter(adapter);
@@ -102,13 +109,6 @@ public class BlogFragment extends BaseFragment
return v; return v;
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
blogController.setBlogSharingListener(this);
sharingController.setSharingListener(this);
}
@Override @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
@@ -218,7 +218,10 @@ public class BlogFragment extends BaseFragment
@Override @Override
public void onAuthorClick(BlogPostItem post) { public void onAuthorClick(BlogPostItem post) {
if (post.getGroupId().equals(groupId)) return; // We're already there if (post.getGroupId().equals(groupId) || getContext() == null) {
// We're already there
return;
}
Intent i = new Intent(getContext(), BlogActivity.class); Intent i = new Intent(getContext(), BlogActivity.class);
i.putExtra(GROUP_ID, post.getGroupId().getBytes()); i.putExtra(GROUP_ID, post.getGroupId().getBytes());
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);

View File

@@ -35,6 +35,7 @@ import javax.inject.Inject;
import static android.app.Activity.RESULT_OK; import static android.app.Activity.RESULT_OK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.support.design.widget.Snackbar.LENGTH_LONG; import static android.support.design.widget.Snackbar.LENGTH_LONG;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST;
@@ -64,13 +65,18 @@ public class FeedFragment extends BaseFragment implements
return f; return f;
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
feedController.setFeedListener(this);
}
@Nullable @Nullable
@Override @Override
public View onCreateView(LayoutInflater inflater, public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) { @Nullable Bundle savedInstanceState) {
requireNonNull(getActivity()).setTitle(R.string.blogs_button);
getActivity().setTitle(R.string.blogs_button);
View v = inflater.inflate(R.layout.fragment_blog, container, false); View v = inflater.inflate(R.layout.fragment_blog, container, false);
@@ -88,12 +94,6 @@ public class FeedFragment extends BaseFragment implements
return v; return v;
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
feedController.setFeedListener(this);
}
@Override @Override
public void onActivityResult(int requestCode, int resultCode, Intent data) { public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);

View File

@@ -17,6 +17,7 @@ import org.briarproject.briar.android.controller.handler.UiResultExceptionHandle
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
@UiThread @UiThread
@@ -42,13 +43,17 @@ public class FeedPostFragment extends BasePostFragment {
return f; return f;
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Nullable @Nullable
@Override @Override
public View onCreateView(LayoutInflater inflater, public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) { @Nullable Bundle savedInstanceState) {
Bundle args = requireNonNull(getArguments());
Bundle args = getArguments();
byte[] b = args.getByteArray(GROUP_ID); byte[] b = args.getByteArray(GROUP_ID);
if (b == null) throw new IllegalStateException("No group ID in args"); if (b == null) throw new IllegalStateException("No group ID in args");
blogId = new GroupId(b); blogId = new GroupId(b);
@@ -61,11 +66,6 @@ public class FeedPostFragment extends BasePostFragment {
return TAG; return TAG;
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();

View File

@@ -1,5 +1,6 @@
package org.briarproject.briar.android.blog; package org.briarproject.briar.android.blog;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -18,7 +19,10 @@ import org.briarproject.briar.android.controller.handler.UiExceptionHandler;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler; import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextInputView.TextInputListener; import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import java.util.List;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
@@ -27,18 +31,18 @@ import static android.view.View.FOCUS_DOWN;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.view.View.INVISIBLE; import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID; import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID;
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class ReblogFragment extends BaseFragment implements TextInputListener { public class ReblogFragment extends BaseFragment implements SendListener {
public static final String TAG = ReblogFragment.class.getName(); public static final String TAG = ReblogFragment.class.getName();
private ViewHolder ui; private ViewHolder ui;
private GroupId blogId;
private MessageId postId;
private BlogPostItem item; private BlogPostItem item;
@Inject @Inject
@@ -70,24 +74,22 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
@Nullable ViewGroup container, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) { @Nullable Bundle savedInstanceState) {
Bundle args = getArguments(); Bundle args = requireNonNull(getArguments());
blogId = new GroupId(args.getByteArray(GROUP_ID)); GroupId blogId =
postId = new MessageId(args.getByteArray(POST_ID)); new GroupId(requireNonNull(args.getByteArray(GROUP_ID)));
MessageId postId =
new MessageId(requireNonNull(args.getByteArray(POST_ID)));
View v = inflater.inflate(R.layout.fragment_reblog, container, false); View v = inflater.inflate(R.layout.fragment_reblog, container, false);
ui = new ViewHolder(v); ui = new ViewHolder(v);
ui.post.setTransitionName(postId); ui.post.setTransitionName(postId);
ui.input.setSendButtonEnabled(false); TextSendController sendController =
new TextSendController(ui.input, this, true);
ui.input.setSendController(sendController);
ui.input.setEnabled(false);
ui.input.setMaxTextLength(MAX_BLOG_POST_TEXT_LENGTH);
showProgressBar(); showProgressBar();
return v;
}
@Override
public void onStart() {
super.onStart();
// TODO: Load blog post when fragment is created. #631
feedController.loadBlogPost(blogId, postId, feedController.loadBlogPost(blogId, postId,
new UiResultExceptionHandler<BlogPostItem, DbException>( new UiResultExceptionHandler<BlogPostItem, DbException>(
this) { this) {
@@ -102,6 +104,8 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
handleDbException(exception); handleDbException(exception);
} }
}); });
return v;
} }
private void bindViewHolder() { private void bindViewHolder() {
@@ -112,16 +116,14 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
ui.post.bindItem(item); ui.post.bindItem(item);
ui.post.hideReblogButton(); ui.post.hideReblogButton();
ui.input.setListener(this); ui.input.setEnabled(true);
ui.input.setSendButtonEnabled(true);
ui.scrollView.post(() -> ui.scrollView.fullScroll(FOCUS_DOWN)); ui.scrollView.post(() -> ui.scrollView.fullScroll(FOCUS_DOWN));
} }
@Override @Override
public void onSendClick(String text) { public void onSendClick(@Nullable String text, List<Uri> imageUris) {
ui.input.hideSoftKeyboard(); ui.input.hideSoftKeyboard();
String comment = getComment(); feedController.repeatPost(item, text,
feedController.repeatPost(item, comment,
new UiExceptionHandler<DbException>(this) { new UiExceptionHandler<DbException>(this) {
@Override @Override
public void onExceptionUi(DbException exception) { public void onExceptionUi(DbException exception) {
@@ -131,12 +133,6 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
finish(); finish();
} }
@Nullable
private String getComment() {
if (ui.input.getText().length() == 0) return null;
return ui.input.getText().toString();
}
private void showProgressBar() { private void showProgressBar() {
ui.progressBar.setVisibility(VISIBLE); ui.progressBar.setVisibility(VISIBLE);
ui.input.setVisibility(GONE); ui.input.setVisibility(GONE);

View File

@@ -1,9 +1,9 @@
package org.briarproject.briar.android.blog; package org.briarproject.briar.android.blog;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable; import android.support.annotation.Nullable;
import android.text.TextWatcher;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MenuItem; import android.view.MenuItem;
import android.widget.ProgressBar; import android.widget.ProgressBar;
@@ -14,19 +14,22 @@ import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.identity.IdentityManager; import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextInputView.TextInputListener; import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.BlogManager; import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPost; import org.briarproject.briar.api.blog.BlogPost;
import org.briarproject.briar.api.blog.BlogPostFactory; import org.briarproject.briar.api.blog.BlogPostFactory;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.util.List;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
@@ -35,10 +38,13 @@ import static android.view.View.GONE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH; import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class WriteBlogPostActivity extends BriarActivity public class WriteBlogPostActivity extends BriarActivity
implements OnEditorActionListener, TextInputListener { implements OnEditorActionListener, SendListener {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(WriteBlogPostActivity.class.getName()); Logger.getLogger(WriteBlogPostActivity.class.getName());
@@ -58,9 +64,8 @@ public class WriteBlogPostActivity extends BriarActivity
@Inject @Inject
volatile BlogManager blogManager; volatile BlogManager blogManager;
@SuppressWarnings("ConstantConditions")
@Override @Override
public void onCreate(Bundle state) { public void onCreate(@Nullable Bundle state) {
super.onCreate(state); super.onCreate(state);
Intent i = getIntent(); Intent i = getIntent();
@@ -71,24 +76,10 @@ public class WriteBlogPostActivity extends BriarActivity
setContentView(R.layout.activity_write_blog_post); setContentView(R.layout.activity_write_blog_post);
input = findViewById(R.id.textInput); input = findViewById(R.id.textInput);
input.setSendButtonEnabled(false); TextSendController sendController =
input.addTextChangedListener(new TextWatcher() { new TextSendController(input, this, false);
@Override input.setSendController(sendController);
public void beforeTextChanged(CharSequence s, int start, int count, input.setMaxTextLength(MAX_BLOG_POST_TEXT_LENGTH);
int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
}
@Override
public void afterTextChanged(Editable s) {
enableOrDisablePublishButton();
}
});
input.setListener(this);
progressBar = findViewById(R.id.progressBar); progressBar = findViewById(R.id.progressBar);
} }
@@ -127,18 +118,15 @@ public class WriteBlogPostActivity extends BriarActivity
return true; return true;
} }
private void enableOrDisablePublishButton() {
input.setSendButtonEnabled(input.getText().length() > 0);
}
@Override @Override
public void onSendClick(String text) { public void onSendClick(@Nullable String text, List<Uri> imageUris) {
if (isNullOrEmpty(text)) throw new AssertionError();
// hide publish button, show progress bar // hide publish button, show progress bar
input.hideSoftKeyboard(); input.hideSoftKeyboard();
input.setVisibility(GONE); input.setVisibility(GONE);
progressBar.setVisibility(VISIBLE); progressBar.setVisibility(VISIBLE);
text = StringUtils.truncateUtf8(text, MAX_BLOG_POST_TEXT_LENGTH);
storePost(text); storePost(text);
} }

View File

@@ -51,11 +51,13 @@ import javax.inject.Inject;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAnimation; import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAnimation;
import static android.support.v4.view.ViewCompat.getTransitionName; import static android.support.v4.view.ViewCompat.getTransitionName;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID; import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
import static org.briarproject.briar.android.util.UiUtils.isSamsung7;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
@@ -102,8 +104,7 @@ public class ContactListFragment extends BaseFragment implements EventListener {
public View onCreateView(LayoutInflater inflater, public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) { @Nullable Bundle savedInstanceState) {
requireNonNull(getActivity()).setTitle(R.string.contact_list_button);
getActivity().setTitle(R.string.contact_list_button);
View contentView = inflater.inflate(R.layout.list, container, false); View contentView = inflater.inflate(R.layout.list, container, false);
@@ -114,7 +115,7 @@ public class ContactListFragment extends BaseFragment implements EventListener {
ContactId contactId = item.getContact().getId(); ContactId contactId = item.getContact().getId();
i.putExtra(CONTACT_ID, contactId.getInt()); i.putExtra(CONTACT_ID, contactId.getInt());
if (SDK_INT >= 23) { if (SDK_INT >= 23 && !isSamsung7()) {
ContactListItemViewHolder holder = ContactListItemViewHolder holder =
(ContactListItemViewHolder) list (ContactListItemViewHolder) list
.getRecyclerView() .getRecyclerView()

View File

@@ -25,6 +25,7 @@ import java.util.Collection;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.contactselection.ContactSelectorActivity.CONTACTS; import static org.briarproject.briar.android.contactselection.ContactSelectorActivity.CONTACTS;
import static org.briarproject.briar.android.contactselection.ContactSelectorActivity.getContactsFromIds; import static org.briarproject.briar.android.contactselection.ContactSelectorActivity.getContactsFromIds;
@@ -50,10 +51,10 @@ public abstract class BaseContactSelectorFragment<I extends SelectableContactIte
} }
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Bundle args = getArguments(); Bundle args = requireNonNull(getArguments());
byte[] b = args.getByteArray(GROUP_ID); byte[] b = args.getByteArray(GROUP_ID);
if (b == null) throw new IllegalStateException("No GroupId"); if (b == null) throw new IllegalStateException("No GroupId");
groupId = new GroupId(b); groupId = new GroupId(b);
@@ -72,7 +73,7 @@ public abstract class BaseContactSelectorFragment<I extends SelectableContactIte
list.setEmptyImage(R.drawable.ic_empty_state_contact_list); list.setEmptyImage(R.drawable.ic_empty_state_contact_list);
list.setEmptyText(getString(R.string.no_contacts_selector)); list.setEmptyText(getString(R.string.no_contacts_selector));
list.setEmptyAction(getString(R.string.no_contacts_selector_action)); list.setEmptyAction(getString(R.string.no_contacts_selector_action));
adapter = getAdapter(getContext(), this); adapter = getAdapter(requireNonNull(getContext()), this);
list.setAdapter(adapter); list.setAdapter(adapter);
// restore selected contacts if available // restore selected contacts if available

View File

@@ -39,8 +39,8 @@ public class AliasDialogFragment extends AppCompatDialogFragment {
setStyle(STYLE_NO_TITLE, R.style.BriarDialogTheme); setStyle(STYLE_NO_TITLE, R.style.BriarDialogTheme);
if (getActivity() == null) return; BriarActivity a = (BriarActivity) requireNonNull(getActivity());
((BriarActivity) getActivity()).getActivityComponent().inject(this); a.getActivityComponent().inject(this);
viewModel = ViewModelProviders.of(getActivity(), viewModelFactory) viewModel = ViewModelProviders.of(getActivity(), viewModelFactory)
.get(ConversationViewModel.class); .get(ConversationViewModel.class);
} }
@@ -48,7 +48,6 @@ public class AliasDialogFragment extends AppCompatDialogFragment {
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) { ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_alias_dialog, container, View v = inflater.inflate(R.layout.fragment_alias_dialog, container,
false); false);
@@ -69,4 +68,5 @@ public class AliasDialogFragment extends AppCompatDialogFragment {
return v; return v;
} }
} }

View File

@@ -1,20 +1,24 @@
package org.briarproject.briar.android.conversation; package org.briarproject.briar.android.conversation;
import android.content.res.Resources;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.media.ExifInterface; 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.Pair;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R; import org.briarproject.briar.android.conversation.ImageHelper.DecodeResult;
import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import java.io.BufferedInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
@@ -42,8 +46,10 @@ class AttachmentController {
private static final Logger LOG = private static final Logger LOG =
getLogger(AttachmentController.class.getName()); getLogger(AttachmentController.class.getName());
private static final int READ_LIMIT = 1024 * 8192;
private final MessagingManager messagingManager; private final MessagingManager messagingManager;
private final ImageHelper imageHelper;
private final int defaultSize; private final int defaultSize;
private final int minWidth, maxWidth; private final int minWidth, maxWidth;
private final int minHeight, maxHeight; private final int minHeight, maxHeight;
@@ -51,18 +57,38 @@ class AttachmentController {
private final Map<MessageId, List<AttachmentItem>> attachmentCache = private final Map<MessageId, List<AttachmentItem>> attachmentCache =
new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
AttachmentController(MessagingManager messagingManager, Resources res) { AttachmentController(MessagingManager messagingManager,
AttachmentDimensions dimensions, ImageHelper imageHelper) {
this.messagingManager = messagingManager; this.messagingManager = messagingManager;
defaultSize = this.imageHelper = imageHelper;
res.getDimensionPixelSize(R.dimen.message_bubble_image_default); defaultSize = dimensions.defaultSize;
minWidth = res.getDimensionPixelSize( minWidth = dimensions.minWidth;
R.dimen.message_bubble_image_min_width); maxWidth = dimensions.maxWidth;
maxWidth = res.getDimensionPixelSize( minHeight = dimensions.minHeight;
R.dimen.message_bubble_image_max_width); maxHeight = dimensions.maxHeight;
minHeight = res.getDimensionPixelSize( }
R.dimen.message_bubble_image_min_height);
maxHeight = res.getDimensionPixelSize( AttachmentController(MessagingManager messagingManager,
R.dimen.message_bubble_image_max_height); 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);
}
});
} }
void put(MessageId messageId, List<AttachmentItem> attachments) { void put(MessageId messageId, List<AttachmentItem> attachments) {
@@ -81,31 +107,53 @@ class AttachmentController {
List<Pair<AttachmentHeader, Attachment>> attachments = List<Pair<AttachmentHeader, Attachment>> attachments =
new ArrayList<>(headers.size()); new ArrayList<>(headers.size());
for (AttachmentHeader h : headers) { for (AttachmentHeader h : headers) {
Attachment a = Attachment a = messagingManager.getAttachment(h.getMessageId());
messagingManager.getAttachment(h.getMessageId());
attachments.add(new Pair<>(h, a)); attachments.add(new Pair<>(h, a));
} }
logDuration(LOG, "Loading attachment", start); logDuration(LOG, "Loading attachment", start);
return attachments; return attachments;
} }
/**
* Creates {@link AttachmentItem}s from the passed headers and Attachments.
* <p>
* Note: This closes the {@link Attachment}'s {@link InputStream}.
*/
List<AttachmentItem> getAttachmentItems( List<AttachmentItem> getAttachmentItems(
List<Pair<AttachmentHeader, Attachment>> attachments) { List<Pair<AttachmentHeader, Attachment>> attachments) {
boolean needsSize = attachments.size() == 1;
List<AttachmentItem> items = new ArrayList<>(attachments.size()); List<AttachmentItem> items = new ArrayList<>(attachments.size());
for (Pair<AttachmentHeader, Attachment> a : attachments) { for (Pair<AttachmentHeader, Attachment> a : attachments) {
AttachmentItem item = AttachmentItem item =
getAttachmentItem(a.getFirst(), a.getSecond()); getAttachmentItem(a.getFirst(), a.getSecond(), needsSize);
items.add(item); items.add(item);
} }
return items; return items;
} }
private AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a) { /**
* 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) {
MessageId messageId = h.getMessageId(); MessageId messageId = h.getMessageId();
Size size = new Size(); if (!needsSize) {
String mimeType = h.getContentType();
String extension = imageHelper.getExtensionFromMimeType(mimeType);
boolean hasError = false;
if (extension == null) {
extension = "";
hasError = true;
}
return new AttachmentItem(messageId, 0, 0, mimeType, extension, 0,
0, hasError);
}
InputStream is = a.getStream(); Size size = new Size();
is.mark(Integer.MAX_VALUE); InputStream is = new MarkEnforcingInputStream(
new BufferedInputStream(a.getStream()));
is.mark(READ_LIMIT);
try { try {
// use exif to get size // use exif to get size
if (h.getContentType().equals("image/jpeg")) { if (h.getContentType().equals("image/jpeg")) {
@@ -118,6 +166,8 @@ class AttachmentController {
// use BitmapFactory to get size // use BitmapFactory to get size
if (size.error) { if (size.error) {
is.reset(); is.reset();
// need to mark again to re-add read limit
is.mark(READ_LIMIT);
size = getSizeFromBitmap(is); size = getSizeFromBitmap(is);
} }
} catch (IOException e) { } catch (IOException e) {
@@ -127,19 +177,24 @@ class AttachmentController {
} }
// calculate thumbnail size // calculate thumbnail size
Size thumbnailSize = new Size(defaultSize, defaultSize); Size thumbnailSize = new Size(defaultSize, defaultSize, size.mimeType);
if (!size.error) { if (!size.error) {
thumbnailSize = getThumbnailSize(size.width, size.height); thumbnailSize =
getThumbnailSize(size.width, size.height, size.mimeType);
} }
// get file extension
String extension = imageHelper.getExtensionFromMimeType(size.mimeType);
boolean hasError = extension == null || size.error;
if (extension == null) extension = "";
return new AttachmentItem(messageId, size.width, size.height, return new AttachmentItem(messageId, size.width, size.height,
thumbnailSize.width, thumbnailSize.height, size.error); size.mimeType, extension, thumbnailSize.width,
thumbnailSize.height, hasError);
} }
/** /**
* Gets the size of a JPEG {@link InputStream} if EXIF info is available. * Gets the size of a JPEG {@link InputStream} if EXIF info is available.
*/ */
private static Size getSizeFromExif(InputStream is) private Size getSizeFromExif(InputStream is) throws IOException {
throws IOException {
ExifInterface exif = new ExifInterface(is); ExifInterface exif = new ExifInterface(is);
// these can return 0 independent of default value // these can return 0 independent of default value
int width = exif.getAttributeInt(TAG_IMAGE_WIDTH, 0); int width = exif.getAttributeInt(TAG_IMAGE_WIDTH, 0);
@@ -151,24 +206,21 @@ class AttachmentController {
orientation == ORIENTATION_TRANSVERSE || orientation == ORIENTATION_TRANSVERSE ||
orientation == ORIENTATION_TRANSPOSE) { orientation == ORIENTATION_TRANSPOSE) {
//noinspection SuspiciousNameCombination //noinspection SuspiciousNameCombination
return new Size(height, width); return new Size(height, width, "image/jpeg");
} }
return new Size(width, height); return new Size(width, height, "image/jpeg");
} }
/** /**
* Gets the size of any image {@link InputStream}. * Gets the size of any image {@link InputStream}.
*/ */
private static Size getSizeFromBitmap(InputStream is) { private Size getSizeFromBitmap(InputStream is) {
BitmapFactory.Options options = new BitmapFactory.Options(); DecodeResult result = imageHelper.decodeStream(is);
options.inJustDecodeBounds = true; if (result.width < 1 || result.height < 1) return new Size();
BitmapFactory.decodeStream(is, null, options); return new Size(result.width, result.height, result.mimeType);
if (options.outWidth < 1 || options.outHeight < 1)
return new Size();
return new Size(options.outWidth, options.outHeight);
} }
private Size getThumbnailSize(int width, int height) { private Size getThumbnailSize(int width, int height, String mimeType) {
float widthPercentage = maxWidth / (float) width; float widthPercentage = maxWidth / (float) width;
float heightPercentage = maxHeight / (float) height; float heightPercentage = maxHeight / (float) height;
float scaleFactor = Math.min(widthPercentage, heightPercentage); float scaleFactor = Math.min(widthPercentage, heightPercentage);
@@ -184,24 +236,27 @@ class AttachmentController {
if (thumbnailWidth > maxWidth) thumbnailWidth = maxWidth; if (thumbnailWidth > maxWidth) thumbnailWidth = maxWidth;
if (thumbnailHeight > maxHeight) thumbnailHeight = maxHeight; if (thumbnailHeight > maxHeight) thumbnailHeight = maxHeight;
} }
return new Size(thumbnailWidth, thumbnailHeight); return new Size(thumbnailWidth, thumbnailHeight, mimeType);
} }
private static class Size { private static class Size {
private final int width; private final int width;
private final int height; private final int height;
private final String mimeType;
private final boolean error; private final boolean error;
private Size(int width, int height) { private Size(int width, int height, String mimeType) {
this.width = width; this.width = width;
this.height = height; this.height = height;
this.mimeType = mimeType;
this.error = false; this.error = false;
} }
private Size() { private Size() {
this.width = 0; this.width = 0;
this.height = 0; this.height = 0;
this.mimeType = "";
this.error = true; this.error = true;
} }
} }

View File

@@ -0,0 +1,39 @@
package org.briarproject.briar.android.conversation;
import android.content.res.Resources;
import android.support.annotation.VisibleForTesting;
import org.briarproject.briar.R;
class AttachmentDimensions {
final int defaultSize;
final int minWidth, maxWidth;
final int minHeight, maxHeight;
@VisibleForTesting
AttachmentDimensions(int defaultSize, int minWidth, int maxWidth,
int minHeight, int maxHeight) {
this.defaultSize = defaultSize;
this.minWidth = minWidth;
this.maxWidth = maxWidth;
this.minHeight = minHeight;
this.maxHeight = maxHeight;
}
static AttachmentDimensions getAttachmentDimensions(Resources res) {
int defaultSize =
res.getDimensionPixelSize(R.dimen.message_bubble_image_default);
int minWidth = res.getDimensionPixelSize(
R.dimen.message_bubble_image_min_width);
int maxWidth = res.getDimensionPixelSize(
R.dimen.message_bubble_image_max_width);
int minHeight = res.getDimensionPixelSize(
R.dimen.message_bubble_image_min_height);
int maxHeight = res.getDimensionPixelSize(
R.dimen.message_bubble_image_max_height);
return new AttachmentDimensions(defaultSize, minWidth, maxWidth,
minHeight, minHeight);
}
}

View File

@@ -2,10 +2,13 @@ package org.briarproject.briar.android.conversation;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
@Immutable @Immutable
@@ -14,8 +17,10 @@ public class AttachmentItem implements Parcelable {
private final MessageId messageId; private final MessageId messageId;
private final int width, height; private final int width, height;
private final String mimeType, extension;
private final int thumbnailWidth, thumbnailHeight; private final int thumbnailWidth, thumbnailHeight;
private final boolean hasError; private final boolean hasError;
private final long instanceId;
public static final Creator<AttachmentItem> CREATOR = public static final Creator<AttachmentItem> CREATOR =
new Creator<AttachmentItem>() { new Creator<AttachmentItem>() {
@@ -30,14 +35,20 @@ public class AttachmentItem implements Parcelable {
} }
}; };
AttachmentItem(MessageId messageId, int width, int height, private static final AtomicLong NEXT_INSTANCE_ID = new AtomicLong(0);
int thumbnailWidth, int thumbnailHeight, boolean hasError) {
AttachmentItem(MessageId messageId, int width, int height, String mimeType,
String extension, int thumbnailWidth, int thumbnailHeight,
boolean hasError) {
this.messageId = messageId; this.messageId = messageId;
this.width = width; this.width = width;
this.height = height; this.height = height;
this.mimeType = mimeType;
this.extension = extension;
this.thumbnailWidth = thumbnailWidth; this.thumbnailWidth = thumbnailWidth;
this.thumbnailHeight = thumbnailHeight; this.thumbnailHeight = thumbnailHeight;
this.hasError = hasError; this.hasError = hasError;
instanceId = NEXT_INSTANCE_ID.getAndIncrement();
} }
protected AttachmentItem(Parcel in) { protected AttachmentItem(Parcel in) {
@@ -46,9 +57,12 @@ public class AttachmentItem implements Parcelable {
messageId = new MessageId(messageIdByte); messageId = new MessageId(messageIdByte);
width = in.readInt(); width = in.readInt();
height = in.readInt(); height = in.readInt();
mimeType = in.readString();
extension = in.readString();
thumbnailWidth = in.readInt(); thumbnailWidth = in.readInt();
thumbnailHeight = in.readInt(); thumbnailHeight = in.readInt();
hasError = in.readByte() != 0; hasError = in.readByte() != 0;
instanceId = in.readLong();
} }
public MessageId getMessageId() { public MessageId getMessageId() {
@@ -63,6 +77,14 @@ public class AttachmentItem implements Parcelable {
return height; return height;
} }
String getMimeType() {
return mimeType;
}
String getExtension() {
return extension;
}
int getThumbnailWidth() { int getThumbnailWidth() {
return thumbnailWidth; return thumbnailWidth;
} }
@@ -75,9 +97,8 @@ public class AttachmentItem implements Parcelable {
return hasError; return hasError;
} }
// TODO use counter instead, because in theory one attachment can appear in more than one messages
String getTransitionName() { String getTransitionName() {
return String.valueOf(messageId.hashCode()); return String.valueOf(instanceId);
} }
@Override @Override
@@ -90,9 +111,18 @@ public class AttachmentItem implements Parcelable {
dest.writeByteArray(messageId.getBytes()); dest.writeByteArray(messageId.getBytes());
dest.writeInt(width); dest.writeInt(width);
dest.writeInt(height); dest.writeInt(height);
dest.writeString(mimeType);
dest.writeString(extension);
dest.writeInt(thumbnailWidth); dest.writeInt(thumbnailWidth);
dest.writeInt(thumbnailHeight); dest.writeInt(thumbnailHeight);
dest.writeByte((byte) (hasError ? 1 : 0)); dest.writeByte((byte) (hasError ? 1 : 0));
dest.writeLong(instanceId);
}
@Override
public boolean equals(@Nullable Object o) {
return o instanceof AttachmentItem &&
instanceId == ((AttachmentItem) o).instanceId;
} }
} }

View File

@@ -1,11 +1,14 @@
package org.briarproject.briar.android.conversation; package org.briarproject.briar.android.conversation;
import android.annotation.SuppressLint;
import android.arch.lifecycle.Observer; import android.arch.lifecycle.Observer;
import android.arch.lifecycle.ViewModelProvider; import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders; import android.arch.lifecycle.ViewModelProviders;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.support.design.widget.Snackbar; import android.support.design.widget.Snackbar;
@@ -27,7 +30,6 @@ import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.Pair; import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
@@ -44,14 +46,11 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionRegistry; import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent; import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent; import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager; import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent; import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
import org.briarproject.bramble.api.sync.event.MessagesSentEvent; import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.activity.BriarActivity;
@@ -62,8 +61,12 @@ import org.briarproject.briar.android.forum.ForumActivity;
import org.briarproject.briar.android.introduction.IntroductionActivity; import org.briarproject.briar.android.introduction.IntroductionActivity;
import org.briarproject.briar.android.privategroup.conversation.GroupActivity; import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.view.ImagePreview;
import org.briarproject.briar.android.view.TextAttachmentController;
import org.briarproject.briar.android.view.TextAttachmentController.AttachImageListener;
import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextInputView.TextInputListener; import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.BlogSharingManager; import org.briarproject.briar.api.blog.BlogSharingManager;
import org.briarproject.briar.api.client.ProtocolStateException; import org.briarproject.briar.api.client.ProtocolStateException;
@@ -78,7 +81,6 @@ import org.briarproject.briar.api.introduction.IntroductionManager;
import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessage;
import org.briarproject.briar.api.messaging.PrivateMessageFactory; import org.briarproject.briar.api.messaging.PrivateMessageFactory;
import org.briarproject.briar.api.messaging.PrivateMessageHeader; import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
@@ -100,11 +102,12 @@ import im.delight.android.identicons.IdenticonDrawable;
import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt; import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt;
import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.PromptStateChangeListener; import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.PromptStateChangeListener;
import static android.arch.lifecycle.Lifecycle.State.STARTED;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAnimation; import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAnimation;
import static android.support.v4.view.ViewCompat.setTransitionName; import static android.support.v4.view.ViewCompat.setTransitionName;
import static android.support.v7.util.SortedList.INVALID_POSITION; import static android.support.v7.util.SortedList.INVALID_POSITION;
import static android.view.Gravity.END; import static android.view.Gravity.RIGHT;
import static android.widget.Toast.LENGTH_SHORT; import static android.widget.Toast.LENGTH_SHORT;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.Collections.sort; import static java.util.Collections.sort;
@@ -114,11 +117,14 @@ import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.briar.android.TestingConstants.FEATURE_FLAG_IMAGE_ATTACHMENTS;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_ATTACH_IMAGE;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION;
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT; import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENTS;
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION;
import static org.briarproject.briar.android.conversation.ImageActivity.DATE; import static org.briarproject.briar.android.conversation.ImageActivity.DATE;
import static org.briarproject.briar.android.conversation.ImageActivity.NAME; import static org.briarproject.briar.android.conversation.ImageActivity.NAME;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.android.util.UiUtils.getAvatarTransitionName; import static org.briarproject.briar.android.util.UiUtils.getAvatarTransitionName;
import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName; import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName;
import static org.briarproject.briar.android.util.UiUtils.observeOnce; import static org.briarproject.briar.android.util.UiUtils.observeOnce;
@@ -129,15 +135,16 @@ import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.S
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class ConversationActivity extends BriarActivity public class ConversationActivity extends BriarActivity
implements EventListener, ConversationListener, TextInputListener, implements EventListener, ConversationListener, SendListener,
TextCache, AttachmentCache { TextCache, AttachmentCache, AttachImageListener {
public static final String CONTACT_ID = "briar.CONTACT_ID"; public static final String CONTACT_ID = "briar.CONTACT_ID";
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(ConversationActivity.class.getName()); Logger.getLogger(ConversationActivity.class.getName());
private static final String SHOW_ONBOARDING_INTRODUCTION =
"showOnboardingIntroduction"; private static final int TRANSITION_DURATION_MS = 500;
private static final int ONBOARDING_DELAY_MS = 250;
@Inject @Inject
AndroidNotificationManager notificationManager; AndroidNotificationManager notificationManager;
@@ -146,20 +153,8 @@ public class ConversationActivity extends BriarActivity
@Inject @Inject
@CryptoExecutor @CryptoExecutor
Executor cryptoExecutor; Executor cryptoExecutor;
@Inject
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>(); ViewModelProvider.Factory viewModelFactory;
private AttachmentController attachmentController;
private ConversationViewModel viewModel;
private ConversationVisitor visitor;
private ConversationAdapter adapter;
private Toolbar toolbar;
private CircleImageView toolbarAvatar;
private ImageView toolbarStatus;
private TextView toolbarTitle;
private BriarRecyclerView list;
private LinearLayoutManager layoutManager;
private TextInputView textInputView;
// Fields that are accessed from background threads must be volatile // Fields that are accessed from background threads must be volatile
@Inject @Inject
@@ -182,22 +177,37 @@ public class ConversationActivity extends BriarActivity
volatile BlogSharingManager blogSharingManager; volatile BlogSharingManager blogSharingManager;
@Inject @Inject
volatile GroupInvitationManager groupInvitationManager; volatile GroupInvitationManager groupInvitationManager;
@Inject
ViewModelProvider.Factory viewModelFactory;
private volatile ContactId contactId;
@Nullable
private volatile GroupId messagingGroupId;
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
private final Observer<String> contactNameObserver = name -> { private final Observer<String> contactNameObserver = name -> {
requireNonNull(name); requireNonNull(name);
loadMessages(); loadMessages();
}; };
private AttachmentController attachmentController;
private ConversationViewModel viewModel;
private ConversationVisitor visitor;
private ConversationAdapter adapter;
private Toolbar toolbar;
private CircleImageView toolbarAvatar;
private ImageView toolbarStatus;
private TextView toolbarTitle;
private BriarRecyclerView list;
private LinearLayoutManager layoutManager;
private TextInputView textInputView;
private TextSendController sendController;
@Nullable
private Parcelable layoutManagerState;
private volatile ContactId contactId;
@Override @Override
public void onCreate(@Nullable Bundle state) { public void onCreate(@Nullable Bundle state) {
if (SDK_INT >= 21) { if (SDK_INT >= 21) {
Transition slide = new Slide(END); // Spurious lint warning - using END causes a crash
@SuppressLint("RtlHardcoded")
Transition slide = new Slide(RIGHT);
slide.setDuration(TRANSITION_DURATION_MS);
setSceneTransitionAnimation(slide, null, slide); setSceneTransitionAnimation(slide, null, slide);
} }
super.onCreate(state); super.onCreate(state);
@@ -209,7 +219,6 @@ public class ConversationActivity extends BriarActivity
viewModel = ViewModelProviders.of(this, viewModelFactory) viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ConversationViewModel.class); .get(ConversationViewModel.class);
viewModel.setContactId(contactId);
attachmentController = viewModel.getAttachmentController(); attachmentController = viewModel.getAttachmentController();
setContentView(R.layout.activity_conversation); setContentView(R.layout.activity_conversation);
@@ -233,6 +242,8 @@ public class ConversationActivity extends BriarActivity
requireNonNull(deleted); requireNonNull(deleted);
if (deleted) finish(); if (deleted) finish();
}); });
viewModel.getAddedPrivateMessage().observe(this,
this::onAddedPrivateMessage);
setTransitionName(toolbarAvatar, getAvatarTransitionName(contactId)); setTransitionName(toolbarAvatar, getAvatarTransitionName(contactId));
setTransitionName(toolbarStatus, getBulbTransitionName(contactId)); setTransitionName(toolbarStatus, getBulbTransitionName(contactId));
@@ -247,7 +258,29 @@ public class ConversationActivity extends BriarActivity
list.setEmptyText(getString(R.string.no_private_messages)); list.setEmptyText(getString(R.string.no_private_messages));
textInputView = findViewById(R.id.text_input_container); textInputView = findViewById(R.id.text_input_container);
textInputView.setListener(this); if (FEATURE_FLAG_IMAGE_ATTACHMENTS) {
ImagePreview imagePreview = findViewById(R.id.imagePreview);
sendController = new TextAttachmentController(textInputView,
imagePreview, this, this);
observeOnce(viewModel.hasImageSupport(), this, hasSupport -> {
if (hasSupport != null && hasSupport) {
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS
((TextAttachmentController) sendController)
.setImagesSupported();
}
});
} else {
sendController = new TextSendController(textInputView, this, false);
}
textInputView.setSendController(sendController);
textInputView.setMaxTextLength(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
textInputView.setEnabled(false);
textInputView.addOnKeyboardShownListener(this::scrollToBottom);
}
private void scrollToBottom() {
int items = adapter.getItemCount();
if (items > 0) list.scrollToPosition(items - 1);
} }
@Override @Override
@@ -256,7 +289,8 @@ public class ConversationActivity extends BriarActivity
} }
@Override @Override
protected void onActivityResult(int request, int result, Intent data) { protected void onActivityResult(int request, int result,
@Nullable Intent data) {
super.onActivityResult(request, result, data); super.onActivityResult(request, result, data);
if (request == REQUEST_INTRODUCTION && result == RESULT_OK) { if (request == REQUEST_INTRODUCTION && result == RESULT_OK) {
@@ -264,6 +298,9 @@ public class ConversationActivity extends BriarActivity
Snackbar.LENGTH_SHORT); Snackbar.LENGTH_SHORT);
snackbar.getView().setBackgroundResource(R.color.briar_primary); snackbar.getView().setBackgroundResource(R.color.briar_primary);
snackbar.show(); snackbar.show();
} else if (request == REQUEST_ATTACH_IMAGE && result == RESULT_OK) {
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS
((TextAttachmentController) sendController).onImageReceived(data);
} }
} }
@@ -278,6 +315,16 @@ public class ConversationActivity extends BriarActivity
list.startPeriodicUpdate(); list.startPeriodicUpdate();
} }
@Override
public void onResume() {
super.onResume();
// Trigger loading of contact data, noop if data was loaded already.
//
// We can only start loading data *after* we are sure
// the user has signed in. After sign-in, onCreate() isn't run again.
if (signedIn()) viewModel.setContactId(contactId);
}
@Override @Override
public void onStop() { public void onStop() {
super.onStop(); super.onStop();
@@ -287,16 +334,39 @@ public class ConversationActivity extends BriarActivity
list.stopPeriodicUpdate(); list.stopPeriodicUpdate();
} }
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (layoutManager != null) {
layoutManagerState = layoutManager.onSaveInstanceState();
outState.putParcelable("layoutManager", layoutManagerState);
}
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
layoutManagerState = savedInstanceState.getParcelable("layoutManager");
}
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu items for use in the action bar // Inflate the menu items for use in the action bar
MenuInflater inflater = getMenuInflater(); MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.conversation_actions, menu); inflater.inflate(R.menu.conversation_actions, menu);
enableIntroductionActionIfAvailable( // enable introduction action if available
menu.findItem(R.id.action_introduction)); observeOnce(viewModel.showIntroductionAction(), this, enable -> {
enableAliasActionIfAvailable( if (enable != null && enable) {
menu.findItem(R.id.action_set_alias)); menu.findItem(R.id.action_introduction).setEnabled(true);
// show introduction onboarding, if needed
observeOnce(viewModel.showIntroductionOnboarding(), this,
this::showIntroductionOnboarding);
}
});
// enable alias action if available
observeOnce(viewModel.getContact(), this, contact ->
menu.findItem(R.id.action_set_alias).setEnabled(true));
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
} }
@@ -359,33 +429,10 @@ public class ConversationActivity extends BriarActivity
Long.compare(b.getTimestamp(), a.getTimestamp())); Long.compare(b.getTimestamp(), a.getTimestamp()));
if (!sorted.isEmpty()) { if (!sorted.isEmpty()) {
// If the latest header is a private message, eagerly load // If the latest header is a private message, eagerly load
// its text so we can set the scroll position correctly // its size so we can set the scroll position correctly
ConversationMessageHeader latest = sorted.get(0); ConversationMessageHeader latest = sorted.get(0);
if (latest instanceof PrivateMessageHeader) { if (latest instanceof PrivateMessageHeader) {
MessageId id = latest.getId(); eagerlyLoadMessageSize((PrivateMessageHeader) latest);
PrivateMessageHeader h = (PrivateMessageHeader) latest;
if (h.hasText()) {
String text = textCache.get(id);
if (text == null) {
LOG.info(
"Eagerly loading text of latest message");
text = messagingManager.getMessageText(id);
textCache.put(id, text);
}
}
if (!h.getAttachmentHeaders().isEmpty()) {
List<AttachmentItem> items =
attachmentController.get(id);
if (items == null) {
LOG.info(
"Eagerly loading image size for latest message");
items = attachmentController.getAttachmentItems(
attachmentController
.getMessageAttachments(
h.getAttachmentHeaders()));
attachmentController.put(id, items);
}
}
} }
} }
displayMessages(revision, sorted); displayMessages(revision, sorted);
@@ -397,17 +444,51 @@ 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, 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 = attachmentController.get(id);
if (items == null) {
LOG.info("Eagerly loading image size for latest message");
items = attachmentController.getAttachmentItems(
attachmentController.getMessageAttachments(
h.getAttachmentHeaders()));
attachmentController.put(id, items);
}
}
}
private void displayMessages(int revision, private void displayMessages(int revision,
Collection<ConversationMessageHeader> headers) { Collection<ConversationMessageHeader> headers) {
runOnUiThreadUnlessDestroyed(() -> { runOnUiThreadUnlessDestroyed(() -> {
if (revision == adapter.getRevision()) { if (revision == adapter.getRevision()) {
adapter.incrementRevision(); adapter.incrementRevision();
textInputView.setSendButtonEnabled(true); textInputView.setEnabled(true);
// start observing onboarding after enabling (only once, because
// we only update this when an onboarding should be shown)
observeOnce(viewModel.showImageOnboarding(), this,
this::showImageOnboarding);
List<ConversationItem> items = createItems(headers); List<ConversationItem> items = createItems(headers);
adapter.addAll(items); adapter.addAll(items);
list.showData(); list.showData();
// Scroll to the bottom if (layoutManagerState == null) {
list.scrollToPosition(adapter.getItemCount() - 1); scrollToBottom();
} else {
// Restore the previous scroll position
layoutManager.onRestoreInstanceState(layoutManagerState);
}
} else { } else {
LOG.info("Concurrent update, reloading"); LOG.info("Concurrent update, reloading");
loadMessages(); loadMessages();
@@ -448,21 +529,28 @@ public class ConversationActivity extends BriarActivity
Pair<Integer, ConversationMessageItem> pair = Pair<Integer, ConversationMessageItem> pair =
adapter.getMessageItem(m); adapter.getMessageItem(m);
if (pair != null) { if (pair != null) {
boolean scroll = shouldScrollWhenUpdatingMessage();
pair.getSecond().setText(text); pair.getSecond().setText(text);
boolean bottom = adapter.isScrolledToBottom(layoutManager);
adapter.notifyItemChanged(pair.getFirst()); adapter.notifyItemChanged(pair.getFirst());
if (bottom) list.scrollToPosition(adapter.getItemCount() - 1); if (scroll) scrollToBottom();
} }
}); });
} }
// When a message's text or attachments are loaded, scroll to the bottom
// if the conversation is visible and we were previously at the bottom
private boolean shouldScrollWhenUpdatingMessage() {
return getLifecycle().getCurrentState().isAtLeast(STARTED)
&& adapter.isScrolledToBottom(layoutManager);
}
private void loadMessageAttachments(MessageId messageId, private void loadMessageAttachments(MessageId messageId,
List<AttachmentHeader> headers) { List<AttachmentHeader> headers) {
runOnDbThread(() -> { runOnDbThread(() -> {
try { try {
List<Pair<AttachmentHeader, Attachment>> attachments = List<Pair<AttachmentHeader, Attachment>> attachments =
attachmentController.getMessageAttachments(headers); attachmentController.getMessageAttachments(headers);
// TODO move getting the items off to the IoExecutor // TODO move getting the items off to IoExecutor, if size == 1
List<AttachmentItem> items = List<AttachmentItem> items =
attachmentController.getAttachmentItems(attachments); attachmentController.getAttachmentItems(attachments);
displayMessageAttachments(messageId, items); displayMessageAttachments(messageId, items);
@@ -479,10 +567,10 @@ public class ConversationActivity extends BriarActivity
Pair<Integer, ConversationMessageItem> pair = Pair<Integer, ConversationMessageItem> pair =
adapter.getMessageItem(m); adapter.getMessageItem(m);
if (pair != null) { if (pair != null) {
boolean scroll = shouldScrollWhenUpdatingMessage();
pair.getSecond().setAttachments(items); pair.getSecond().setAttachments(items);
boolean bottom = adapter.isScrolledToBottom(layoutManager);
adapter.notifyItemChanged(pair.getFirst()); adapter.notifyItemChanged(pair.getFirst());
if (bottom) list.scrollToPosition(adapter.getItemCount() - 1); if (scroll) scrollToBottom();
} }
}); });
} }
@@ -531,10 +619,13 @@ public class ConversationActivity extends BriarActivity
private void addConversationItem(ConversationItem item) { private void addConversationItem(ConversationItem item) {
runOnUiThreadUnlessDestroyed(() -> { runOnUiThreadUnlessDestroyed(() -> {
boolean bottom = adapter.isScrolledToBottom(layoutManager);
adapter.incrementRevision(); adapter.incrementRevision();
adapter.add(item); adapter.add(item);
if (bottom) list.scrollToPosition(adapter.getItemCount() - 1); // When adding a new message, scroll to the bottom if the
// conversation is visible, even if we're not currently at
// the bottom
if (getLifecycle().getCurrentState().isAtLeast(STARTED))
scrollToBottom();
}); });
} }
@@ -570,14 +661,18 @@ public class ConversationActivity extends BriarActivity
} }
@Override @Override
public void onSendClick(String text) { public void onAttachImage(Intent intent) {
if (text.isEmpty()) return; startActivityForResult(intent, REQUEST_ATTACH_IMAGE);
text = StringUtils.truncateUtf8(text, MAX_PRIVATE_MESSAGE_TEXT_LENGTH); }
@Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
if (isNullOrEmpty(text) && imageUris.isEmpty())
throw new AssertionError();
long timestamp = System.currentTimeMillis(); long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, getMinTimestampForNewMessage()); timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
if (messagingGroupId == null) loadGroupId(text, timestamp); viewModel.sendMessage(text, imageUris, timestamp);
else createMessage(text, timestamp); textInputView.clearText();
textInputView.setText("");
} }
private long getMinTimestampForNewMessage() { private long getMinTimestampForNewMessage() {
@@ -586,48 +681,10 @@ public class ConversationActivity extends BriarActivity
return item == null ? 0 : item.getTime() + 1; return item == null ? 0 : item.getTime() + 1;
} }
private void loadGroupId(String text, long timestamp) { private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) {
runOnDbThread(() -> { if (h == null) return;
try { addConversationItem(h.accept(visitor));
messagingGroupId = viewModel.onAddedPrivateMessageSeen();
messagingManager.getConversationId(contactId);
createMessage(text, timestamp);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
private void createMessage(String text, long timestamp) {
cryptoExecutor.execute(() -> {
try {
//noinspection ConstantConditions init in loadGroupId()
storeMessage(privateMessageFactory.createPrivateMessage(
messagingGroupId, timestamp, text, emptyList()), text);
} catch (FormatException e) {
throw new RuntimeException(e);
}
});
}
private void storeMessage(PrivateMessage m, String text) {
runOnDbThread(() -> {
try {
long start = now();
messagingManager.addLocalMessage(m);
logDuration(LOG, "Storing message", start);
Message message = m.getMessage();
PrivateMessageHeader h = new PrivateMessageHeader(
message.getId(), message.getGroupId(),
message.getTimestamp(), true, false, false, false,
true, emptyList());
textCache.put(message.getId(), text);
addConversationItem(h.accept(visitor));
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
} }
private void askToRemoveContact() { private void askToRemoveContact() {
@@ -664,74 +721,70 @@ public class ConversationActivity extends BriarActivity
}); });
} }
private void enableIntroductionActionIfAvailable(MenuItem item) { private void showImageOnboarding(@Nullable Boolean show) {
runOnDbThread(() -> { if (show == null || !show) return;
try { if (SDK_INT >= 21) {
if (contactManager.getActiveContacts().size() > 1) { // show onboarding only after the enter transition has ended
enableIntroductionAction(item); // otherwise the tap target animation won't play
Settings settings = textInputView.postDelayed(this::showImageOnboarding,
settingsManager.getSettings(SETTINGS_NAMESPACE); TRANSITION_DURATION_MS + ONBOARDING_DELAY_MS);
if (settings.getBoolean(SHOW_ONBOARDING_INTRODUCTION, } else {
true)) { showImageOnboarding();
showIntroductionOnboarding(); }
}
}
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
} }
private void enableAliasActionIfAvailable(MenuItem item) { private void showImageOnboarding() {
observeOnce(viewModel.getContact(), this, c -> item.setEnabled(true)); // remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS
((TextAttachmentController) sendController)
.showImageOnboarding(this, () ->
viewModel.onImageOnboardingSeen());
} }
private void enableIntroductionAction(MenuItem item) { private void showIntroductionOnboarding(@Nullable Boolean show) {
runOnUiThreadUnlessDestroyed(() -> item.setEnabled(true)); if (show == null || !show) return;
if (SDK_INT >= 21) {
// show onboarding only after the enter transition has ended
// otherwise the tap target animation won't play
textInputView.postDelayed(this::showIntroductionOnboarding,
TRANSITION_DURATION_MS + ONBOARDING_DELAY_MS);
} else {
showIntroductionOnboarding();
}
} }
private void showIntroductionOnboarding() { private void showIntroductionOnboarding() {
runOnUiThreadUnlessDestroyed(() -> { // find view of overflow icon
// find view of overflow icon View target = null;
View target = null; for (int i = 0; i < toolbar.getChildCount(); i++) {
for (int i = 0; i < toolbar.getChildCount(); i++) { if (toolbar.getChildAt(i) instanceof ActionMenuView) {
if (toolbar.getChildAt(i) instanceof ActionMenuView) { ActionMenuView menu = (ActionMenuView) toolbar.getChildAt(i);
ActionMenuView menu = // The overflow icon should be the last child of the menu
(ActionMenuView) toolbar.getChildAt(i); target = menu.getChildAt(menu.getChildCount() - 1);
target = menu.getChildAt(menu.getChildCount() - 1); // If the menu hasn't been populated yet, use the menu itself
break; // as the target
} if (target == null) target = menu;
} break;
if (target == null) {
LOG.warning("No Overflow Icon found!");
return;
} }
}
if (target == null) {
LOG.warning("No Overflow Icon found!");
return;
}
PromptStateChangeListener listener = (prompt, state) -> { PromptStateChangeListener listener = (prompt, state) -> {
if (state == STATE_DISMISSED || state == STATE_FINISHED) { if (state == STATE_DISMISSED || state == STATE_FINISHED) {
introductionOnboardingSeen(); viewModel.onIntroductionOnboardingSeen();
}
};
new MaterialTapTargetPrompt.Builder(ConversationActivity.this,
R.style.OnboardingDialogTheme).setTarget(target)
.setPrimaryText(R.string.introduction_onboarding_title)
.setSecondaryText(R.string.introduction_onboarding_text)
.setIcon(R.drawable.ic_more_vert_accent)
.setPromptStateChangeListener(listener)
.show();
});
}
private void introductionOnboardingSeen() {
runOnDbThread(() -> {
try {
Settings settings = new Settings();
settings.putBoolean(SHOW_ONBOARDING_INTRODUCTION, false);
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
} catch (DbException e) {
logException(LOG, WARNING, e);
} }
}); };
new MaterialTapTargetPrompt.Builder(ConversationActivity.this,
R.style.OnboardingDialogTheme).setTarget(target)
.setPrimaryText(R.string.introduction_onboarding_title)
.setSecondaryText(R.string.introduction_onboarding_text)
.setIcon(R.drawable.ic_more_vert_accent)
.setBackgroundColour(
ContextCompat.getColor(this, R.color.briar_primary))
.setPromptStateChangeListener(listener)
.show();
} }
@Override @Override
@@ -825,19 +878,18 @@ public class ConversationActivity extends BriarActivity
} else { } else {
name = getString(R.string.you); name = getString(R.string.you);
} }
ArrayList<AttachmentItem> attachments =
new ArrayList<>(messageItem.getAttachments());
Intent i = new Intent(this, ImageActivity.class); Intent i = new Intent(this, ImageActivity.class);
i.putExtra(ATTACHMENT, item); i.putParcelableArrayListExtra(ATTACHMENTS, attachments);
i.putExtra(ATTACHMENT_POSITION, attachments.indexOf(item));
i.putExtra(NAME, name); i.putExtra(NAME, name);
i.putExtra(DATE, messageItem.getTime()); i.putExtra(DATE, messageItem.getTime());
if (SDK_INT >= 23) { // restoring list position should not trigger android bug #224270
String transitionName = item.getTransitionName(); String transitionName = item.getTransitionName();
ActivityOptionsCompat options = ActivityOptionsCompat options =
makeSceneTransitionAnimation(this, view, transitionName); makeSceneTransitionAnimation(this, view, transitionName);
ActivityCompat.startActivity(this, i, options.toBundle()); ActivityCompat.startActivity(this, i, options.toBundle());
} else {
// work-around for android bug #224270
startActivity(i);
}
} }
@DatabaseExecutor @DatabaseExecutor

View File

@@ -3,6 +3,7 @@ package org.briarproject.briar.android.conversation;
import android.content.Context; import android.content.Context;
import android.support.annotation.LayoutRes; import android.support.annotation.LayoutRes;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView.RecycledViewPool;
import android.util.SparseArray; import android.util.SparseArray;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -21,11 +22,17 @@ class ConversationAdapter
extends BriarAdapter<ConversationItem, ConversationItemViewHolder> { extends BriarAdapter<ConversationItem, ConversationItemViewHolder> {
private ConversationListener listener; private ConversationListener listener;
private final RecycledViewPool imageViewPool;
private final ImageItemDecoration imageItemDecoration;
ConversationAdapter(Context ctx, ConversationAdapter(Context ctx,
ConversationListener conversationListener) { ConversationListener conversationListener) {
super(ctx, ConversationItem.class); super(ctx, ConversationItem.class);
listener = conversationListener; listener = conversationListener;
// This shares the same pool for view recycling between all image lists
imageViewPool = new RecycledViewPool();
// Share the item decoration as well
imageItemDecoration = new ImageItemDecoration(ctx);
} }
@LayoutRes @LayoutRes
@@ -42,15 +49,17 @@ class ConversationAdapter
type, viewGroup, false); type, viewGroup, false);
switch (type) { switch (type) {
case R.layout.list_item_conversation_msg_in: case R.layout.list_item_conversation_msg_in:
return new ConversationMessageViewHolder(v, true); return new ConversationMessageViewHolder(v, listener, true,
imageViewPool, imageItemDecoration);
case R.layout.list_item_conversation_msg_out: case R.layout.list_item_conversation_msg_out:
return new ConversationMessageViewHolder(v, false); return new ConversationMessageViewHolder(v, listener, false,
imageViewPool, imageItemDecoration);
case R.layout.list_item_conversation_notice_in: case R.layout.list_item_conversation_notice_in:
return new ConversationNoticeViewHolder(v, true); return new ConversationNoticeViewHolder(v, listener, true);
case R.layout.list_item_conversation_notice_out: case R.layout.list_item_conversation_notice_out:
return new ConversationNoticeViewHolder(v, false); return new ConversationNoticeViewHolder(v, listener, false);
case R.layout.list_item_conversation_request: case R.layout.list_item_conversation_request:
return new ConversationRequestViewHolder(v, true); return new ConversationRequestViewHolder(v, listener, true);
default: default:
throw new IllegalArgumentException("Unknown ConversationItem"); throw new IllegalArgumentException("Unknown ConversationItem");
} }
@@ -59,7 +68,7 @@ class ConversationAdapter
@Override @Override
public void onBindViewHolder(ConversationItemViewHolder ui, int position) { public void onBindViewHolder(ConversationItemViewHolder ui, int position) {
ConversationItem item = items.get(position); ConversationItem item = items.get(position);
ui.bind(item, listener); ui.bind(item);
listener.onItemVisible(item); listener.onItemVisible(item);
} }

View File

@@ -18,14 +18,17 @@ import static org.briarproject.briar.android.util.UiUtils.formatDate;
@NotNullByDefault @NotNullByDefault
abstract class ConversationItemViewHolder extends ViewHolder { abstract class ConversationItemViewHolder extends ViewHolder {
protected final ConversationListener listener;
protected final ConstraintLayout layout; protected final ConstraintLayout layout;
@Nullable @Nullable
private final OutItemViewHolder outViewHolder; private final OutItemViewHolder outViewHolder;
private final TextView text; private final TextView text;
protected final TextView time; protected final TextView time;
ConversationItemViewHolder(View v, boolean isIncoming) { ConversationItemViewHolder(View v, ConversationListener listener,
boolean isIncoming) {
super(v); super(v);
this.listener = listener;
this.outViewHolder = isIncoming ? null : new OutItemViewHolder(v); this.outViewHolder = isIncoming ? null : new OutItemViewHolder(v);
layout = v.findViewById(R.id.layout); layout = v.findViewById(R.id.layout);
text = v.findViewById(R.id.text); text = v.findViewById(R.id.text);
@@ -33,7 +36,7 @@ abstract class ConversationItemViewHolder extends ViewHolder {
} }
@CallSuper @CallSuper
void bind(ConversationItem item, ConversationListener listener) { void bind(ConversationItem item) {
if (item.getText() != null) { if (item.getText() != null) {
text.setText(trim(item.getText())); text.setText(trim(item.getText()));
} }

View File

@@ -1,63 +1,45 @@
package org.briarproject.briar.android.conversation; package org.briarproject.briar.android.conversation;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.support.annotation.DrawableRes;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.support.constraint.ConstraintSet; import android.support.constraint.ConstraintSet;
import android.support.v4.content.ContextCompat; import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.RecycledViewPool;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView;
import com.bumptech.glide.load.Transformation;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.conversation.glide.BriarImageTransformation;
import org.briarproject.briar.android.conversation.glide.GlideApp;
import static android.os.Build.VERSION.SDK_INT; import static android.support.constraint.ConstraintSet.WRAP_CONTENT;
import static android.support.v4.view.ViewCompat.LAYOUT_DIRECTION_RTL; import static android.support.v4.content.ContextCompat.getColor;
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
@UiThread @UiThread
@NotNullByDefault @NotNullByDefault
class ConversationMessageViewHolder extends ConversationItemViewHolder { class ConversationMessageViewHolder extends ConversationItemViewHolder {
@DrawableRes private final ImageAdapter adapter;
private static final int ERROR_RES = R.drawable.ic_image_broken;
private final ImageView imageView;
private final ViewGroup statusLayout; private final ViewGroup statusLayout;
private final int timeColor, timeColorBubble; private final int timeColor, timeColorBubble;
private final int radiusBig, radiusSmall;
private final boolean isRtl;
private final ConstraintSet textConstraints = new ConstraintSet(); private final ConstraintSet textConstraints = new ConstraintSet();
private final ConstraintSet imageConstraints = new ConstraintSet(); private final ConstraintSet imageConstraints = new ConstraintSet();
private final ConstraintSet imageTextConstraints = new ConstraintSet(); private final ConstraintSet imageTextConstraints = new ConstraintSet();
ConversationMessageViewHolder(View v, boolean isIncoming) { ConversationMessageViewHolder(View v, ConversationListener listener,
super(v, isIncoming); boolean isIncoming, RecycledViewPool imageViewPool,
imageView = v.findViewById(R.id.imageView); ImageItemDecoration imageItemDecoration) {
super(v, listener, isIncoming);
statusLayout = v.findViewById(R.id.statusLayout); statusLayout = v.findViewById(R.id.statusLayout);
radiusBig = v.getContext().getResources()
.getDimensionPixelSize(R.dimen.message_bubble_radius_big); // image list
radiusSmall = v.getContext().getResources() RecyclerView list = v.findViewById(R.id.imageList);
.getDimensionPixelSize(R.dimen.message_bubble_radius_small); list.setRecycledViewPool(imageViewPool);
adapter = new ImageAdapter(v.getContext(), listener);
list.setAdapter(adapter);
list.addItemDecoration(imageItemDecoration);
// remember original status text color // remember original status text color
timeColor = time.getCurrentTextColor(); timeColor = time.getCurrentTextColor();
timeColorBubble = timeColorBubble = getColor(v.getContext(), R.color.briar_white);
ContextCompat.getColor(v.getContext(), R.color.briar_white);
// find out if we are showing a RTL language, Use the configuration,
// because getting the layout direction of views is not reliable
Configuration config =
imageView.getContext().getResources().getConfiguration();
isRtl = SDK_INT >= 17 &&
config.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
// clone constraint sets from layout files // clone constraint sets from layout files
textConstraints textConstraints
@@ -77,85 +59,55 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
} }
@Override @Override
void bind(ConversationItem conversationItem, void bind(ConversationItem conversationItem) {
ConversationListener listener) { super.bind(conversationItem);
super.bind(conversationItem, listener);
ConversationMessageItem item = ConversationMessageItem item =
(ConversationMessageItem) conversationItem; (ConversationMessageItem) conversationItem;
if (item.getAttachments().isEmpty()) { if (item.getAttachments().isEmpty()) {
bindTextItem(); bindTextItem();
} else { } else {
bindImageItem(item, listener); bindImageItem(item);
} }
} }
private void bindTextItem() { private void bindTextItem() {
clearImage(); resetStatusLayoutForText();
textConstraints.applyTo(layout);
adapter.clear();
}
private void bindImageItem(ConversationMessageItem item) {
ConstraintSet constraintSet;
if (item.getText() == null) {
statusLayout.setBackgroundResource(R.drawable.msg_status_bubble);
time.setTextColor(timeColorBubble);
constraintSet = imageConstraints;
} else {
resetStatusLayoutForText();
constraintSet = imageTextConstraints;
}
if (item.getAttachments().size() == 1) {
// apply image size constraints for a single image
AttachmentItem attachment = item.getAttachments().get(0);
int width = attachment.getThumbnailWidth();
int height = attachment.getThumbnailHeight();
constraintSet.constrainWidth(R.id.imageList, width);
constraintSet.constrainHeight(R.id.imageList, height);
} else {
// bubble adapts to size of image list
constraintSet.constrainWidth(R.id.imageList, WRAP_CONTENT);
constraintSet.constrainHeight(R.id.imageList, WRAP_CONTENT);
}
constraintSet.applyTo(layout);
adapter.setConversationItem(item);
}
private void resetStatusLayoutForText() {
statusLayout.setBackgroundResource(0); statusLayout.setBackgroundResource(0);
// also reset padding (the background drawable defines some) // also reset padding (the background drawable defines some)
statusLayout.setPadding(0, 0, 0, 0); statusLayout.setPadding(0, 0, 0, 0);
time.setTextColor(timeColor); time.setTextColor(timeColor);
textConstraints.applyTo(layout);
}
private void bindImageItem(ConversationMessageItem item,
ConversationListener listener) {
// TODO show more than just the first image
AttachmentItem attachment = item.getAttachments().get(0);
ConstraintSet constraintSet;
if (item.getText() == null) {
statusLayout
.setBackgroundResource(R.drawable.msg_status_bubble);
time.setTextColor(timeColorBubble);
constraintSet = imageConstraints;
} else {
statusLayout.setBackgroundResource(0);
// also reset padding (the background drawable defines some)
statusLayout.setPadding(0, 0, 0, 0);
time.setTextColor(timeColor);
constraintSet = imageTextConstraints;
}
// apply image size constraints, so glides picks them up for scaling
int width = attachment.getThumbnailWidth();
int height = attachment.getThumbnailHeight();
constraintSet.constrainWidth(R.id.imageView, width);
constraintSet.constrainHeight(R.id.imageView, height);
constraintSet.applyTo(layout);
if (attachment.hasError()) {
clearImage();
imageView.setImageResource(ERROR_RES);
} else {
loadImage(item, attachment, listener);
}
}
private void clearImage() {
GlideApp.with(imageView)
.clear(imageView);
imageView.setOnClickListener(null);
}
private void loadImage(ConversationMessageItem item,
AttachmentItem attachment, ConversationListener listener) {
boolean leftCornerSmall =
(isIncoming() && !isRtl) || (!isIncoming() && isRtl);
boolean bottomRound = item.getText() == null;
Transformation<Bitmap> transformation = new BriarImageTransformation(
radiusSmall, radiusBig, leftCornerSmall, bottomRound);
GlideApp.with(imageView)
.load(attachment)
.diskCacheStrategy(NONE)
.error(ERROR_RES)
.transform(transformation)
.transition(withCrossFade())
.into(imageView)
.waitForLayout();
imageView.setOnClickListener(
view -> listener.onAttachmentClicked(view, item, attachment));
} }
} }

View File

@@ -19,16 +19,17 @@ class ConversationNoticeViewHolder extends ConversationItemViewHolder {
private final TextView msgText; private final TextView msgText;
ConversationNoticeViewHolder(View v, boolean isIncoming) { ConversationNoticeViewHolder(View v, ConversationListener listener,
super(v, isIncoming); boolean isIncoming) {
super(v, listener, isIncoming);
msgText = v.findViewById(R.id.msgText); msgText = v.findViewById(R.id.msgText);
} }
@Override @Override
@CallSuper @CallSuper
void bind(ConversationItem item, ConversationListener listener) { void bind(ConversationItem item) {
ConversationNoticeItem notice = (ConversationNoticeItem) item; ConversationNoticeItem notice = (ConversationNoticeItem) item;
super.bind(notice, listener); super.bind(notice);
String text = notice.getMsgText(); String text = notice.getMsgText();
if (isNullOrEmpty(text)) { if (isNullOrEmpty(text)) {

View File

@@ -17,16 +17,17 @@ class ConversationRequestViewHolder extends ConversationNoticeViewHolder {
private final Button acceptButton; private final Button acceptButton;
private final Button declineButton; private final Button declineButton;
ConversationRequestViewHolder(View v, boolean isIncoming) { ConversationRequestViewHolder(View v, ConversationListener listener,
super(v, isIncoming); boolean isIncoming) {
super(v, listener, isIncoming);
acceptButton = v.findViewById(R.id.acceptButton); acceptButton = v.findViewById(R.id.acceptButton);
declineButton = v.findViewById(R.id.declineButton); declineButton = v.findViewById(R.id.declineButton);
} }
@Override @Override
void bind(ConversationItem item, ConversationListener listener) { void bind(ConversationItem item) {
ConversationRequestItem request = (ConversationRequestItem) item; ConversationRequestItem request = (ConversationRequestItem) item;
super.bind(request, listener); super.bind(request);
if (request.wasAnswered() && request.canBeOpened()) { if (request.wasAnswered() && request.canBeOpened()) {
acceptButton.setVisibility(VISIBLE); acceptButton.setVisibility(VISIBLE);

View File

@@ -5,19 +5,40 @@ import android.arch.lifecycle.AndroidViewModel;
import android.arch.lifecycle.LiveData; import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData; import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.Transformations; import android.arch.lifecycle.Transformations;
import android.content.ContentResolver;
import android.net.Uri;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.briar.android.util.UiUtils; import org.briarproject.briar.android.util.UiUtils;
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.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessage;
import org.briarproject.briar.api.messaging.PrivateMessageFactory;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -26,19 +47,34 @@ import javax.inject.Inject;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; 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.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.conversation.AttachmentDimensions.getAttachmentDimensions;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
@NotNullByDefault @NotNullByDefault
public class ConversationViewModel extends AndroidViewModel { public class ConversationViewModel extends AndroidViewModel {
private static Logger LOG = private static Logger LOG =
getLogger(ConversationViewModel.class.getName()); getLogger(ConversationViewModel.class.getName());
private static final String SHOW_ONBOARDING_IMAGE =
"showOnboardingImage";
private static final String SHOW_ONBOARDING_INTRODUCTION =
"showOnboardingIntroduction";
@DatabaseExecutor @DatabaseExecutor
private final Executor dbExecutor; private final Executor dbExecutor;
@CryptoExecutor
private final Executor cryptoExecutor;
// TODO replace with TransactionManager once it exists
private final DatabaseComponent db;
private final MessagingManager messagingManager;
private final ContactManager contactManager; private final ContactManager contactManager;
private final SettingsManager settingsManager;
private final PrivateMessageFactory privateMessageFactory;
private final AttachmentController attachmentController; private final AttachmentController attachmentController;
@Nullable @Nullable
@@ -48,38 +84,64 @@ public class ConversationViewModel extends AndroidViewModel {
Transformations.map(contact, c -> c.getAuthor().getId()); Transformations.map(contact, c -> c.getAuthor().getId());
private final LiveData<String> contactName = private final LiveData<String> contactName =
Transformations.map(contact, UiUtils::getContactDisplayName); Transformations.map(contact, UiUtils::getContactDisplayName);
private final MutableLiveData<Boolean> imageSupport =
new MutableLiveData<>();
private final MutableLiveData<Boolean> showImageOnboarding =
new MutableLiveData<>();
private final MutableLiveData<Boolean> showIntroductionOnboarding =
new MutableLiveData<>();
private final MutableLiveData<Boolean> showIntroductionAction =
new MutableLiveData<>();
private final MutableLiveData<Boolean> contactDeleted = private final MutableLiveData<Boolean> contactDeleted =
new MutableLiveData<>(); new MutableLiveData<>();
private final MutableLiveData<GroupId> messagingGroupId =
new MutableLiveData<>();
private final MutableLiveData<PrivateMessageHeader> addedHeader =
new MutableLiveData<>();
@Inject @Inject
ConversationViewModel(Application application, ConversationViewModel(Application application,
@DatabaseExecutor Executor dbExecutor, @DatabaseExecutor Executor dbExecutor,
ContactManager contactManager, MessagingManager messagingManager) { @CryptoExecutor Executor cryptoExecutor, DatabaseComponent db,
MessagingManager messagingManager, ContactManager contactManager,
SettingsManager settingsManager,
PrivateMessageFactory privateMessageFactory) {
super(application); super(application);
this.dbExecutor = dbExecutor; this.dbExecutor = dbExecutor;
this.cryptoExecutor = cryptoExecutor;
this.db = db;
this.messagingManager = messagingManager;
this.contactManager = contactManager; this.contactManager = contactManager;
this.settingsManager = settingsManager;
this.privateMessageFactory = privateMessageFactory;
this.attachmentController = new AttachmentController(messagingManager, this.attachmentController = new AttachmentController(messagingManager,
application.getResources()); getAttachmentDimensions(application.getResources()));
contactDeleted.setValue(false); contactDeleted.setValue(false);
} }
/**
* Setting the {@link ContactId} automatically triggers loading of other
* data.
*/
void setContactId(ContactId contactId) { void setContactId(ContactId contactId) {
if (this.contactId == null) { if (this.contactId == null) {
this.contactId = contactId; this.contactId = contactId;
loadContact(); loadContact(contactId);
} else if (!contactId.equals(this.contactId)) { } else if (!contactId.equals(this.contactId)) {
throw new IllegalStateException(); throw new IllegalStateException();
} }
} }
private void loadContact() { private void loadContact(ContactId contactId) {
dbExecutor.execute(() -> { dbExecutor.execute(() -> {
try { try {
long start = now(); long start = now();
Contact c = Contact c = contactManager.getContact(contactId);
contactManager.getContact(requireNonNull(contactId));
contact.postValue(c); contact.postValue(c);
logDuration(LOG, "Loading contact", start); logDuration(LOG, "Loading contact", start);
start = now();
checkFeaturesAndOnboarding(contactId);
logDuration(LOG, "Checking for image support", start);
} catch (NoSuchContactException e) { } catch (NoSuchContactException e) {
contactDeleted.postValue(true); contactDeleted.postValue(true);
} catch (DbException e) { } catch (DbException e) {
@@ -93,13 +155,179 @@ public class ConversationViewModel extends AndroidViewModel {
try { try {
contactManager.setContactAlias(requireNonNull(contactId), contactManager.setContactAlias(requireNonNull(contactId),
alias.isEmpty() ? null : alias); alias.isEmpty() ? null : alias);
loadContact(); loadContact(contactId);
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
} }
}); });
} }
void sendMessage(@Nullable String text, List<Uri> uris, long timestamp) {
if (messagingGroupId.getValue() == null) loadGroupId();
observeForeverOnce(messagingGroupId, groupId -> {
if (groupId == null) return;
// calls through to creating and storing the message
storeAttachments(groupId, text, uris, timestamp);
});
}
private void loadGroupId() {
if (contactId == null) throw new IllegalStateException();
dbExecutor.execute(() -> {
try {
messagingGroupId.postValue(
messagingManager.getConversationId(contactId));
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
@DatabaseExecutor
private void checkFeaturesAndOnboarding(ContactId c) throws DbException {
// check if images are supported
boolean imagesSupported = db.transactionWithResult(true, txn ->
messagingManager.contactSupportsImages(txn, c));
imageSupport.postValue(imagesSupported);
// check if introductions are supported
Collection<Contact> contacts = contactManager.getActiveContacts();
boolean introductionSupported = contacts.size() > 1;
showIntroductionAction.postValue(introductionSupported);
Settings settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
if (imagesSupported &&
settings.getBoolean(SHOW_ONBOARDING_IMAGE, true)) {
// check if we should show onboarding, only if images are supported
showImageOnboarding.postValue(true);
// allow observer to stop listening for changes
showIntroductionOnboarding.postValue(false);
} else {
// allow observer to stop listening for changes
showImageOnboarding.postValue(false);
// we only show one onboarding dialog at a time
if (introductionSupported &&
settings.getBoolean(SHOW_ONBOARDING_INTRODUCTION, true)) {
showIntroductionOnboarding.postValue(true);
} else {
// allow observer to stop listening for changes
showIntroductionOnboarding.postValue(false);
}
}
}
void onImageOnboardingSeen() {
onOnboardingSeen(SHOW_ONBOARDING_IMAGE);
}
void onIntroductionOnboardingSeen() {
onOnboardingSeen(SHOW_ONBOARDING_INTRODUCTION);
}
private void onOnboardingSeen(String key) {
dbExecutor.execute(() -> {
try {
Settings settings = new Settings();
settings.putBoolean(key, false);
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
private void storeAttachments(GroupId groupId, @Nullable String text,
List<Uri> uris, long timestamp) {
dbExecutor.execute(() -> {
long start = now();
List<AttachmentHeader> attachments = new ArrayList<>();
List<AttachmentItem> items = new ArrayList<>();
boolean needsSize = uris.size() == 1;
for (Uri uri : uris) {
Pair<AttachmentHeader, AttachmentItem> pair =
createAttachmentHeader(groupId, uri, timestamp,
needsSize);
if (pair == null) continue;
attachments.add(pair.getFirst());
items.add(pair.getSecond());
}
logDuration(LOG, "Storing attachments", start);
createMessage(groupId, text, attachments, items, timestamp);
});
}
@Nullable
@DatabaseExecutor
private Pair<AttachmentHeader, AttachmentItem> createAttachmentHeader(
GroupId groupId, Uri uri, long timestamp, boolean needsSize) {
InputStream is = null;
try {
ContentResolver contentResolver =
getApplication().getContentResolver();
is = contentResolver.openInputStream(uri);
if (is == null) throw new IOException();
String contentType = contentResolver.getType(uri);
if (contentType == null) throw new IOException("null content type");
AttachmentHeader h = messagingManager
.addLocalAttachment(groupId, timestamp, contentType, is);
is.close();
// re-open stream to get AttachmentItem
is = contentResolver.openInputStream(uri);
if (is == null) throw new IOException();
AttachmentItem item = attachmentController
.getAttachmentItem(h, new Attachment(is), needsSize);
return new Pair<>(h, item);
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
return null;
} finally {
if (is != null) tryToClose(is, LOG, WARNING);
}
}
private void createMessage(GroupId groupId, @Nullable String text,
List<AttachmentHeader> attachments, List<AttachmentItem> aItems,
long timestamp) {
cryptoExecutor.execute(() -> {
try {
// TODO remove when text can be null in the backend
String msgText = text == null ? "null" : text;
PrivateMessage pm = privateMessageFactory
.createPrivateMessage(groupId, timestamp, msgText,
attachments);
attachmentController.put(pm.getMessage().getId(), aItems);
storeMessage(pm, msgText, attachments);
} catch (FormatException e) {
throw new RuntimeException(e);
}
});
}
private void storeMessage(PrivateMessage m, @Nullable String text,
List<AttachmentHeader> attachments) {
dbExecutor.execute(() -> {
try {
long start = now();
messagingManager.addLocalMessage(m);
logDuration(LOG, "Storing message", start);
Message message = m.getMessage();
PrivateMessageHeader h = new PrivateMessageHeader(
message.getId(), message.getGroupId(),
message.getTimestamp(), true, true, false, false,
text != null, attachments);
// TODO add text to cache when available here
addedHeader.postValue(h);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
@UiThread
void onAddedPrivateMessageSeen() {
addedHeader.setValue(null);
}
AttachmentController getAttachmentController() { AttachmentController getAttachmentController() {
return attachmentController; return attachmentController;
} }
@@ -116,8 +344,28 @@ public class ConversationViewModel extends AndroidViewModel {
return contactName; return contactName;
} }
LiveData<Boolean> hasImageSupport() {
return imageSupport;
}
LiveData<Boolean> showImageOnboarding() {
return showImageOnboarding;
}
LiveData<Boolean> showIntroductionOnboarding() {
return showIntroductionOnboarding;
}
LiveData<Boolean> showIntroductionAction() {
return showIntroductionAction;
}
LiveData<Boolean> isContactDeleted() { LiveData<Boolean> isContactDeleted() {
return contactDeleted; return contactDeleted;
} }
LiveData<PrivateMessageHeader> getAddedPrivateMessage() {
return addedHeader;
}
} }

View File

@@ -1,54 +1,80 @@
package org.briarproject.briar.android.conversation; package org.briarproject.briar.android.conversation;
import android.graphics.drawable.Animatable; import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi; import android.support.annotation.RequiresApi;
import android.support.design.widget.AppBarLayout; import android.support.design.widget.AppBarLayout;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.content.ContextCompat;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AlertDialog.Builder;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.transition.Fade; import android.transition.Fade;
import android.transition.Transition; import android.transition.Transition;
import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.Window; import android.view.Window;
import android.widget.TextView; import android.widget.TextView;
import com.bumptech.glide.load.DataSource; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import com.bumptech.glide.load.engine.GlideException; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.github.chrisbanes.photoview.PhotoView;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.conversation.glide.GlideApp;
import org.briarproject.briar.android.view.PullDownLayout; import org.briarproject.briar.android.view.PullDownLayout;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import static android.content.Intent.ACTION_CREATE_DOCUMENT;
import static android.content.Intent.CATEGORY_OPENABLE;
import static android.content.Intent.EXTRA_TITLE;
import static android.graphics.Color.TRANSPARENT; import static android.graphics.Color.TRANSPARENT;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import static android.support.design.widget.Snackbar.LENGTH_LONG;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN; import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE; import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
import static android.widget.ImageView.ScaleType.FIT_START;
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SAVE_ATTACHMENT;
import static org.briarproject.briar.android.util.UiUtils.formatDateAbsolute; import static org.briarproject.briar.android.util.UiUtils.formatDateAbsolute;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ImageActivity extends BriarActivity public class ImageActivity extends BriarActivity
implements PullDownLayout.Callback { implements PullDownLayout.Callback, OnGlobalLayoutListener {
final static String ATTACHMENT = "attachment"; final static String ATTACHMENTS = "attachments";
final static String ATTACHMENT_POSITION = "position";
final static String NAME = "name"; final static String NAME = "name";
final static String DATE = "date"; final static String DATE = "date";
@Inject
ViewModelProvider.Factory viewModelFactory;
private ImageViewModel viewModel;
private PullDownLayout layout; private PullDownLayout layout;
private AppBarLayout appBarLayout; private AppBarLayout appBarLayout;
private PhotoView photoView; private ViewPager viewPager;
private List<AttachmentItem> attachments;
@Override @Override
public void injectActivity(ActivityComponent component) { public void injectActivity(ActivityComponent component) {
@@ -60,18 +86,23 @@ public class ImageActivity extends BriarActivity
super.onCreate(state); super.onCreate(state);
// Transitions // Transitions
supportPostponeEnterTransition(); if (state == null) supportPostponeEnterTransition();
Window window = getWindow(); Window window = getWindow();
if (SDK_INT >= 21) { if (SDK_INT >= 21) {
Transition transition = new Fade(); Transition transition = new Fade();
setSceneTransitionAnimation(transition, null, transition); setSceneTransitionAnimation(transition, null, transition);
} }
// get View Model
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ImageViewModel.class);
viewModel.getSaveState().observe(this, this::onImageSaveStateChanged);
// inflate layout // inflate layout
setContentView(R.layout.activity_image); setContentView(R.layout.activity_image);
layout = findViewById(R.id.layout); layout = findViewById(R.id.layout);
layout.getBackground().setAlpha(255);
layout.setCallback(this); layout.setCallback(this);
layout.getViewTreeObserver().addOnGlobalLayoutListener(this);
// Status Bar // Status Bar
if (SDK_INT >= 21) { if (SDK_INT >= 21) {
@@ -88,59 +119,35 @@ public class ImageActivity extends BriarActivity
TextView dateView = toolbar.findViewById(R.id.dateView); TextView dateView = toolbar.findViewById(R.id.dateView);
// Intent Extras // Intent Extras
AttachmentItem attachment = getIntent().getParcelableExtra(ATTACHMENT); Intent i = getIntent();
String name = getIntent().getStringExtra(NAME); attachments = i.getParcelableArrayListExtra(ATTACHMENTS);
long time = getIntent().getLongExtra(DATE, 0); int position = i.getIntExtra(ATTACHMENT_POSITION, -1);
if (position == -1) throw new IllegalStateException();
String name = i.getStringExtra(NAME);
long time = i.getLongExtra(DATE, 0);
String date = formatDateAbsolute(this, time); String date = formatDateAbsolute(this, time);
contactName.setText(name); contactName.setText(name);
dateView.setText(date); dateView.setText(date);
// Image View // Set up image ViewPager
photoView = findViewById(R.id.photoView); viewPager = findViewById(R.id.viewPager);
ImagePagerAdapter pagerAdapter =
new ImagePagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(pagerAdapter);
viewPager.setCurrentItem(position);
if (SDK_INT >= 16) { if (SDK_INT >= 16) {
photoView.setOnClickListener(view -> toggleSystemUi()); viewModel.getOnImageClicked().observe(this, this::onImageClicked);
window.getDecorView().setSystemUiVisibility( window.getDecorView().setSystemUiVisibility(
SYSTEM_UI_FLAG_LAYOUT_STABLE | SYSTEM_UI_FLAG_LAYOUT_STABLE |
SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
} }
}
// Request Listener @Override
RequestListener<Drawable> listener = new RequestListener<Drawable>() { public boolean onCreateOptionsMenu(Menu menu) {
@Override getMenuInflater().inflate(R.menu.image_actions, menu);
public boolean onLoadFailed(@Nullable GlideException e, return super.onCreateOptionsMenu(menu);
Object model, Target<Drawable> target,
boolean isFirstResource) {
supportStartPostponedEnterTransition();
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model,
Target<Drawable> target, DataSource dataSource,
boolean isFirstResource) {
if (SDK_INT >= 21 && !(resource instanceof Animatable)) {
// set transition name only when not animatable,
// because the animation won't start otherwise
photoView.setTransitionName(
attachment.getTransitionName());
}
// Move image to the top if overlapping toolbar
if (isOverlappingToolbar(resource)) {
photoView.setScaleType(FIT_START);
}
supportStartPostponedEnterTransition();
return false;
}
};
// Load Image
GlideApp.with(this)
.load(attachment)
.diskCacheStrategy(NONE)
.error(R.drawable.ic_image_broken)
.dontTransform()
.addListener(listener)
.into(photoView);
} }
@Override @Override
@@ -149,11 +156,36 @@ public class ImageActivity extends BriarActivity
case android.R.id.home: case android.R.id.home:
onBackPressed(); onBackPressed();
return true; return true;
case R.id.action_save_image:
showSaveImageDialog();
return true;
default: default:
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
} }
@Override
public void onGlobalLayout() {
viewModel.setToolbarPosition(
appBarLayout.getTop(), appBarLayout.getBottom()
);
if (SDK_INT >= 16) {
layout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
layout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
}
@Override
protected void onActivityResult(int request, int result,
@Nullable Intent data) {
super.onActivityResult(request, result, data);
if (request == REQUEST_SAVE_ATTACHMENT && result == RESULT_OK &&
data != null) {
viewModel.saveImage(getVisibleAttachment(), data.getData());
}
}
@Override @Override
public void onPullStart() { public void onPullStart() {
appBarLayout.animate() appBarLayout.animate()
@@ -163,7 +195,6 @@ public class ImageActivity extends BriarActivity
@Override @Override
public void onPull(float progress) { public void onPull(float progress) {
layout.getBackground().setAlpha(Math.round((1 - progress) * 255));
} }
@Override @Override
@@ -178,6 +209,14 @@ public class ImageActivity extends BriarActivity
supportFinishAfterTransition(); supportFinishAfterTransition();
} }
@RequiresApi(api = 16)
private void onImageClicked(@Nullable Boolean clicked) {
if (clicked != null && clicked) {
toggleSystemUi();
viewModel.onOnImageClickSeen();
}
}
@RequiresApi(api = 16) @RequiresApi(api = 16)
private void toggleSystemUi() { private void toggleSystemUi() {
View decorView = getWindow().getDecorView(); View decorView = getWindow().getDecorView();
@@ -190,9 +229,8 @@ public class ImageActivity extends BriarActivity
@RequiresApi(api = 16) @RequiresApi(api = 16)
private void hideSystemUi(View decorView) { private void hideSystemUi(View decorView) {
decorView.setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_STABLE decorView.setSystemUiVisibility(SYSTEM_UI_FLAG_FULLSCREEN |
| SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN SYSTEM_UI_FLAG_LAYOUT_STABLE | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| SYSTEM_UI_FLAG_FULLSCREEN
); );
appBarLayout.animate() appBarLayout.animate()
.translationYBy(-1 * appBarLayout.getHeight()) .translationYBy(-1 * appBarLayout.getHeight())
@@ -204,8 +242,7 @@ public class ImageActivity extends BriarActivity
@RequiresApi(api = 16) @RequiresApi(api = 16)
private void showSystemUi(View decorView) { private void showSystemUi(View decorView) {
decorView.setSystemUiVisibility( decorView.setSystemUiVisibility(
SYSTEM_UI_FLAG_LAYOUT_STABLE SYSTEM_UI_FLAG_LAYOUT_STABLE | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
); );
appBarLayout.animate() appBarLayout.animate()
.translationYBy(appBarLayout.getHeight()) .translationYBy(appBarLayout.getHeight())
@@ -214,20 +251,76 @@ public class ImageActivity extends BriarActivity
.start(); .start();
} }
private boolean isOverlappingToolbar(Drawable drawable) { private void showSaveImageDialog() {
int width = drawable.getIntrinsicWidth(); OnClickListener okListener = (dialog, which) -> {
int height = drawable.getIntrinsicHeight(); if (SDK_INT >= 19) {
float widthPercentage = photoView.getWidth() / (float) width; Intent intent = getCreationIntent();
float heightPercentage = photoView.getHeight() / (float) height; startActivityForResult(intent, REQUEST_SAVE_ATTACHMENT);
float scaleFactor = Math.min(widthPercentage, heightPercentage); } else {
int realWidth = (int) (width * scaleFactor); viewModel.saveImage(getVisibleAttachment());
int realHeight = (int) (height * scaleFactor); }
// return if photo doesn't use the full width, };
// because it will be moved to the right otherwise Builder builder = new Builder(this, R.style.BriarDialogTheme);
if (realWidth < photoView.getWidth()) return false; builder.setTitle(getString(R.string.dialog_title_save_image));
int drawableTop = (photoView.getHeight() - realHeight) / 2; builder.setMessage(getString(R.string.dialog_message_save_image));
return drawableTop < appBarLayout.getBottom() && Drawable icon = ContextCompat.getDrawable(this, R.drawable.ic_security);
drawableTop != appBarLayout.getTop(); DrawableCompat.setTint(requireNonNull(icon),
ContextCompat.getColor(this, R.color.color_primary));
builder.setIcon(icon);
builder.setPositiveButton(R.string.save_image, okListener);
builder.setNegativeButton(R.string.cancel, null);
builder.show();
}
@RequiresApi(api = 19)
private Intent getCreationIntent() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss",
Locale.getDefault());
String fileName = sdf.format(new Date());
Intent intent = new Intent(ACTION_CREATE_DOCUMENT);
intent.addCategory(CATEGORY_OPENABLE);
intent.setType(getVisibleAttachment().getMimeType());
intent.putExtra(EXTRA_TITLE, fileName);
return intent;
}
private void onImageSaveStateChanged(@Nullable Boolean error) {
if (error == null) return;
int stringRes = error ?
R.string.save_image_error : R.string.save_image_success;
int colorRes = error ?
R.color.briar_red : R.color.briar_primary;
Snackbar s = Snackbar.make(layout, stringRes, LENGTH_LONG);
s.getView().setBackgroundResource(colorRes);
s.show();
viewModel.onSaveStateSeen();
}
AttachmentItem getVisibleAttachment() {
return attachments.get(viewPager.getCurrentItem());
}
private class ImagePagerAdapter extends FragmentStatePagerAdapter {
private boolean isFirst = true;
private ImagePagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
Fragment f = ImageFragment
.newInstance(attachments.get(position), isFirst);
isFirst = false;
return f;
}
@Override
public int getCount() {
return attachments.size();
}
} }
} }

View File

@@ -0,0 +1,155 @@
package org.briarproject.briar.android.conversation;
import android.content.Context;
import android.content.res.Resources;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView.Adapter;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.conversation.glide.Radii;
import java.util.ArrayList;
import java.util.List;
import static android.content.Context.WINDOW_SERVICE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.util.UiUtils.isRtl;
@NotNullByDefault
class ImageAdapter extends Adapter<ImageViewHolder> {
private final List<AttachmentItem> items = new ArrayList<>();
private final ConversationListener listener;
private final int imageSize;
private final int radiusBig, radiusSmall;
private final boolean isRtl;
@Nullable
private ConversationMessageItem conversationItem;
ImageAdapter(Context ctx, ConversationListener listener) {
this.listener = listener;
imageSize = getImageSize(ctx);
Resources res = ctx.getResources();
radiusBig =
res.getDimensionPixelSize(R.dimen.message_bubble_radius_big);
radiusSmall =
res.getDimensionPixelSize(R.dimen.message_bubble_radius_small);
isRtl = isRtl(ctx);
}
@Override
public ImageViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate(
R.layout.list_item_image, viewGroup, false);
return new ImageViewHolder(v, imageSize);
}
@Override
public void onBindViewHolder(ImageViewHolder imageViewHolder,
int position) {
// get item
requireNonNull(conversationItem);
AttachmentItem item = items.get(position);
// set onClick listener
imageViewHolder.itemView.setOnClickListener(v ->
listener.onAttachmentClicked(v, conversationItem, item)
);
// bind view holder
int size = items.size();
boolean isIncoming = conversationItem.isIncoming();
boolean hasText = conversationItem.getText() != null;
Radii r = getRadii(position, size, isIncoming, hasText);
imageViewHolder.bind(item, r, size == 1, singleInRow(position, size));
}
@Override
public int getItemCount() {
return items.size();
}
void setConversationItem(ConversationMessageItem item) {
this.conversationItem = item;
this.items.clear();
this.items.addAll(item.getAttachments());
notifyDataSetChanged();
}
private int getImageSize(Context ctx) {
Resources res = ctx.getResources();
WindowManager windowManager =
(WindowManager) ctx.getSystemService(WINDOW_SERVICE);
DisplayMetrics displayMetrics = new DisplayMetrics();
if (windowManager == null) {
return res.getDimensionPixelSize(
R.dimen.message_bubble_image_default);
}
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
int imageSize = displayMetrics.widthPixels / 3;
int maxSize = res.getDimensionPixelSize(
R.dimen.message_bubble_image_max_width);
return Math.min(imageSize, maxSize);
}
private Radii getRadii(int pos, int num, boolean isIncoming,
boolean hasText) {
boolean left = isLeft(pos);
boolean single = num == 1;
// Top Row
int topLeft;
int topRight;
if (single) {
topLeft = isIncoming ? radiusSmall : radiusBig;
topRight = !isIncoming ? radiusSmall : radiusBig;
} else if (isTopRow(pos)) {
topLeft = left ? (isIncoming ? radiusSmall : radiusBig) : 0;
topRight = !left ? (!isIncoming ? radiusSmall : radiusBig) : 0;
} else {
topLeft = 0;
topRight = 0;
}
// Bottom Row
boolean singleInRow = singleInRow(pos, num);
int bottomLeft;
int bottomRight;
if (!hasText && isBottomRow(pos, num)) {
bottomLeft = singleInRow || left ? radiusBig : 0;
bottomRight = singleInRow || !left ? radiusBig : 0;
} else {
bottomLeft = 0;
bottomRight = 0;
}
if (isRtl) return new Radii(topRight, topLeft, bottomRight, bottomLeft);
return new Radii(topLeft, topRight, bottomLeft, bottomRight);
}
void clear() {
items.clear();
notifyDataSetChanged();
}
static boolean isTopRow(int pos) {
return pos < 2;
}
static boolean isLeft(int pos) {
return pos % 2 == 0;
}
static boolean isBottomRow(int pos, int num) {
return num % 2 == 0 ?
pos >= num - 2 : // last two, if even
pos > num - 2; // last one, if odd
}
static boolean singleInRow(int pos, int num) {
// last item of an odd number
return num % 2 != 0 && pos == num -1;
}
}

View File

@@ -0,0 +1,133 @@
package org.briarproject.briar.android.conversation;
import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.github.chrisbanes.photoview.PhotoView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.conversation.glide.GlideApp;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.inject.Inject;
import static android.os.Build.VERSION.SDK_INT;
import static android.widget.ImageView.ScaleType.FIT_START;
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION;
@MethodsNotNullByDefault
@ParametersAreNonnullByDefault
public class ImageFragment extends Fragment {
private final static String IS_FIRST = "isFirst";
@Inject
ViewModelProvider.Factory viewModelFactory;
private AttachmentItem attachment;
private boolean isFirst;
private ImageViewModel viewModel;
private PhotoView photoView;
static ImageFragment newInstance(AttachmentItem a, boolean isFirst) {
ImageFragment f = new ImageFragment();
Bundle args = new Bundle();
args.putParcelable(ATTACHMENT_POSITION, a);
args.putBoolean(IS_FIRST, isFirst);
f.setArguments(args);
return f;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
BaseActivity a = (BaseActivity) requireNonNull(getActivity());
a.getActivityComponent().inject(this);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = requireNonNull(getArguments());
attachment = requireNonNull(args.getParcelable(ATTACHMENT_POSITION));
isFirst = args.getBoolean(IS_FIRST);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_image, container,
false);
viewModel = ViewModelProviders.of(requireNonNull(getActivity()),
viewModelFactory).get(ImageViewModel.class);
photoView = v.findViewById(R.id.photoView);
photoView.setScaleLevels(1, 2, 4);
photoView.setOnClickListener(view -> viewModel.clickImage());
// Request Listener
RequestListener<Drawable> listener = new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e,
Object model, Target<Drawable> target,
boolean isFirstResource) {
if (getActivity() != null && isFirst)
getActivity().supportStartPostponedEnterTransition();
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model,
Target<Drawable> target, DataSource dataSource,
boolean isFirstResource) {
if (SDK_INT >= 21 && !(resource instanceof Animatable)) {
// set transition name only when not animatable,
// because the animation won't start otherwise
photoView.setTransitionName(
attachment.getTransitionName());
}
// Move image to the top if overlapping toolbar
if (viewModel.isOverlappingToolbar(photoView, resource)) {
photoView.setScaleType(FIT_START);
}
if (getActivity() != null && isFirst) {
getActivity().supportStartPostponedEnterTransition();
}
return false;
}
};
// Load Image
GlideApp.with(this)
.load(attachment)
// TODO allow if size < maxTextureSize ?
// .override(SIZE_ORIGINAL)
.diskCacheStrategy(NONE)
.error(R.drawable.ic_image_broken)
.addListener(listener)
.into(photoView);
return v;
}
}

View File

@@ -0,0 +1,29 @@
package org.briarproject.briar.android.conversation;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.InputStream;
@NotNullByDefault
interface ImageHelper {
DecodeResult decodeStream(InputStream is);
@Nullable
String getExtensionFromMimeType(String mimeType);
class DecodeResult {
final int width;
final int height;
final String mimeType;
DecodeResult(int width, int height, String mimeType) {
this.width = width;
this.height = height;
this.mimeType = mimeType;
}
}
}

View File

@@ -0,0 +1,54 @@
package org.briarproject.briar.android.conversation;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ItemDecoration;
import android.support.v7.widget.RecyclerView.State;
import android.view.View;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import static org.briarproject.briar.android.conversation.ImageAdapter.isBottomRow;
import static org.briarproject.briar.android.conversation.ImageAdapter.isLeft;
import static org.briarproject.briar.android.conversation.ImageAdapter.isTopRow;
import static org.briarproject.briar.android.conversation.ImageAdapter.singleInRow;
import static org.briarproject.briar.android.util.UiUtils.isRtl;
@NotNullByDefault
class ImageItemDecoration extends ItemDecoration {
private final int border;
private final boolean isRtl;
ImageItemDecoration(Context ctx) {
Resources res = ctx.getResources();
// for pixel perfection, add a pixel to the border if it has an odd size
int b = res.getDimensionPixelSize(R.dimen.message_bubble_border);
int realBorderSize = b % 2 == 0 ? b : b + 1;
// we are applying half the border around the insides of each image
// to prevent differently sized images looking slightly broken
border = realBorderSize / 2;
// find out if we are showing a RTL language
isRtl = isRtl(ctx);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
State state) {
if (state.getItemCount() == 1) return;
int pos = parent.getChildAdapterPosition(view);
int num = state.getItemCount();
boolean start = isLeft(pos) ^ isRtl;
outRect.top = isTopRow(pos) ? 0 : border;
outRect.left = start ? 0 : border;
outRect.right = start && !singleInRow(pos, num) ? border : 0;
outRect.bottom = isBottomRow(pos, num) ? 0 : border;
}
}

View File

@@ -0,0 +1,74 @@
package org.briarproject.briar.android.conversation;
import android.graphics.Bitmap;
import android.support.annotation.DrawableRes;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.support.v7.widget.StaggeredGridLayoutManager.LayoutParams;
import android.view.View;
import android.widget.ImageView;
import com.bumptech.glide.load.Transformation;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.conversation.glide.BriarImageTransformation;
import org.briarproject.briar.android.conversation.glide.GlideApp;
import org.briarproject.briar.android.conversation.glide.Radii;
import static android.os.Build.VERSION.SDK_INT;
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
@NotNullByDefault
class ImageViewHolder extends ViewHolder {
@DrawableRes
private static final int ERROR_RES = R.drawable.ic_image_broken;
protected final ImageView imageView;
private final int imageSize;
ImageViewHolder(View v, int imageSize) {
super(v);
imageView = v.findViewById(R.id.imageView);
this.imageSize = imageSize;
}
void bind(AttachmentItem attachment, Radii r, boolean single,
boolean needsStretch) {
if (attachment.hasError()) {
GlideApp.with(imageView)
.clear(imageView);
imageView.setImageResource(ERROR_RES);
} else {
setImageViewDimensions(attachment, single, needsStretch);
loadImage(attachment, r);
if (SDK_INT >= 21) {
imageView.setTransitionName(attachment.getTransitionName());
}
}
}
private void setImageViewDimensions(AttachmentItem a, boolean single,
boolean needsStretch) {
LayoutParams params = (LayoutParams) imageView.getLayoutParams();
int width = needsStretch ? imageSize * 2 : imageSize;
params.width = single ? a.getThumbnailWidth() : width;
params.height = single ? a.getThumbnailHeight() : imageSize;
params.setFullSpan(!single && needsStretch);
imageView.setLayoutParams(params);
}
private void loadImage(AttachmentItem a, Radii r) {
Transformation<Bitmap> transformation = new BriarImageTransformation(r);
GlideApp.with(imageView)
.load(a)
.diskCacheStrategy(NONE)
.error(ERROR_RES)
.transform(transformation)
.transition(withCrossFade())
.into(imageView)
.waitForLayout();
}
}

View File

@@ -0,0 +1,214 @@
package org.briarproject.briar.android.conversation;
import android.app.Application;
import android.arch.lifecycle.AndroidViewModel;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.view.View;
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.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.MessagingManager;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.media.MediaScannerConnection.scanFile;
import static android.os.Environment.DIRECTORY_PICTURES;
import static android.os.Environment.getExternalStoragePublicDirectory;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.copyAndClose;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
public class ImageViewModel extends AndroidViewModel {
private static Logger LOG = getLogger(ImageViewModel.class.getName());
private final MessagingManager messagingManager;
@DatabaseExecutor
private final Executor dbExecutor;
@IoExecutor
private final Executor ioExecutor;
/**
* true means there was an error saving the image, false if image was saved.
*/
private final MutableLiveData<Boolean> saveState = new MutableLiveData<>();
private final MutableLiveData<Boolean> imageClicked =
new MutableLiveData<>();
private int toolbarTop, toolbarBottom;
@Inject
ImageViewModel(Application application,
MessagingManager messagingManager,
@DatabaseExecutor Executor dbExecutor,
@IoExecutor Executor ioExecutor) {
super(application);
this.messagingManager = messagingManager;
this.dbExecutor = dbExecutor;
this.ioExecutor = ioExecutor;
}
void clickImage() {
imageClicked.setValue(true);
}
/**
* A LiveData that is true if the image was clicked,
* false if it wasn't.
*
* Call {@link #onOnImageClickSeen()} after consuming an update.
*/
LiveData<Boolean> getOnImageClicked() {
return imageClicked;
}
@UiThread
void onOnImageClickSeen() {
imageClicked.setValue(false);
}
void setToolbarPosition(int top, int bottom) {
toolbarTop = top;
toolbarBottom = bottom;
}
boolean isOverlappingToolbar(View screenView, Drawable drawable) {
int width = drawable.getIntrinsicWidth();
int height = drawable.getIntrinsicHeight();
float widthPercentage = screenView.getWidth() / (float) width;
float heightPercentage = screenView.getHeight() / (float) height;
float scaleFactor = Math.min(widthPercentage, heightPercentage);
int realWidth = (int) (width * scaleFactor);
int realHeight = (int) (height * scaleFactor);
// return if image doesn't use the full width,
// because it will be moved to the right otherwise
if (realWidth < screenView.getWidth()) return false;
int drawableTop = (screenView.getHeight() - realHeight) / 2;
return drawableTop < toolbarBottom && drawableTop != toolbarTop;
}
/**
* A LiveData that is true if there was an error
* and false if the image was saved.
* It can be null otherwise, if no image was saved recently.
*
* Call {@link #onSaveStateSeen()} after consuming an update.
*/
LiveData<Boolean> getSaveState() {
return saveState;
}
@UiThread
void onSaveStateSeen() {
saveState.setValue(null);
}
/**
* Saves the attachment to a writeable {@link Uri}.
*/
@UiThread
void saveImage(AttachmentItem attachment, @Nullable Uri uri) {
if (uri == null) {
saveState.setValue(true);
} else {
saveImage(attachment, () -> getOutputStream(uri), null);
}
}
/**
* Saves the attachment on external storage,
* assuming the permission was granted during install time.
*/
void saveImage(AttachmentItem attachment) {
File file = getImageFile(attachment);
saveImage(attachment, () -> getOutputStream(file), () -> scanFile(
getApplication(), new String[] {file.toString()}, null, null));
}
private void saveImage(AttachmentItem attachment, OutputStreamProvider osp,
@Nullable Runnable afterCopy) {
MessageId messageId = attachment.getMessageId();
dbExecutor.execute(() -> {
try {
Attachment a = messagingManager.getAttachment(messageId);
copyImageFromDb(a, osp, afterCopy);
} catch (DbException e) {
logException(LOG, WARNING, e);
saveState.postValue(true);
}
});
}
private void copyImageFromDb(Attachment a, OutputStreamProvider osp,
@Nullable Runnable afterCopy) {
ioExecutor.execute(() -> {
try {
InputStream is = a.getStream();
OutputStream os = osp.getOutputStream();
copyAndClose(is, os);
if (afterCopy != null) afterCopy.run();
saveState.postValue(false);
} catch (IOException e) {
logException(LOG, WARNING, e);
saveState.postValue(true);
}
});
}
private String getFileName() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss",
Locale.getDefault());
return sdf.format(new Date());
}
private File getImageFile(AttachmentItem attachment) {
File path = getExternalStoragePublicDirectory(DIRECTORY_PICTURES);
//noinspection ResultOfMethodCallIgnored
path.mkdirs();
String fileName = getFileName();
String ext = "." + attachment.getExtension();
File file = new File(path, fileName + ext);
int i = 1;
while (file.exists()) {
file = new File(path, fileName + " (" + i + ")" + ext);
}
return file;
}
private OutputStream getOutputStream(File file) throws IOException {
return new FileOutputStream(file);
}
private OutputStream getOutputStream(Uri uri) throws IOException {
OutputStream os =
getApplication().getContentResolver().openOutputStream(uri);
if (os == null) throw new IOException();
return os;
}
private interface OutputStreamProvider {
OutputStream getOutputStream() throws IOException;
}
}

View File

@@ -40,7 +40,7 @@ class BriarDataFetcher implements DataFetcher<InputStream> {
private volatile boolean cancel = false; private volatile boolean cancel = false;
@Inject @Inject
public BriarDataFetcher(MessagingManager messagingManager, BriarDataFetcher(MessagingManager messagingManager,
@DatabaseExecutor Executor dbExecutor, AttachmentItem attachment) { @DatabaseExecutor Executor dbExecutor, AttachmentItem attachment) {
this.messagingManager = messagingManager; this.messagingManager = messagingManager;
this.dbExecutor = dbExecutor; this.dbExecutor = dbExecutor;

View File

@@ -7,10 +7,8 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop;
public class BriarImageTransformation extends MultiTransformation<Bitmap> { public class BriarImageTransformation extends MultiTransformation<Bitmap> {
public BriarImageTransformation(int smallRadius, int radius, public BriarImageTransformation(Radii r) {
boolean leftCornerSmall, boolean bottomRound) { super(new CenterCrop(), new CustomCornersTransformation(r));
super(new CenterCrop(), new ImageCornerTransformation(
smallRadius, radius, leftCornerSmall, bottomRound));
} }
} }

View File

@@ -22,7 +22,7 @@ public final class BriarModelLoader
@Inject @Inject
BriarDataFetcherFactory dataFetcherFactory; BriarDataFetcherFactory dataFetcherFactory;
public BriarModelLoader(BriarApplication app) { BriarModelLoader(BriarApplication app) {
app.getApplicationComponent().inject(this); app.getApplicationComponent().inject(this);
} }

View File

@@ -16,7 +16,7 @@ class BriarModelLoaderFactory
private final BriarApplication app; private final BriarApplication app;
public BriarModelLoaderFactory(BriarApplication app) { BriarModelLoaderFactory(BriarApplication app) {
this.app = app; this.app = app;
} }

View File

@@ -0,0 +1,129 @@
package org.briarproject.briar.android.conversation.glide;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.support.annotation.NonNull;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.security.MessageDigest;
import javax.annotation.concurrent.Immutable;
import static android.graphics.Bitmap.Config.ARGB_8888;
import static android.graphics.Shader.TileMode.CLAMP;
@Immutable
@NotNullByDefault
class CustomCornersTransformation extends BitmapTransformation {
private static final String ID =
CustomCornersTransformation.class.getName();
private final Radii radii;
CustomCornersTransformation(Radii radii) {
this.radii = radii;
}
@Override
protected Bitmap transform(BitmapPool pool, Bitmap toTransform,
int outWidth, int outHeight) {
int width = toTransform.getWidth();
int height = toTransform.getHeight();
Bitmap bitmap = pool.get(width, height, ARGB_8888);
bitmap.setHasAlpha(true);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(new BitmapShader(toTransform, CLAMP, CLAMP));
drawRect(canvas, paint, width, height);
return bitmap;
}
private void drawRect(Canvas canvas, Paint paint, float width,
float height) {
drawTopLeft(canvas, paint, radii.topLeft, width, height);
drawTopRight(canvas, paint, radii.topRight, width, height);
drawBottomLeft(canvas, paint, radii.bottomLeft, width, height);
drawBottomRight(canvas, paint, radii.bottomRight, width, height);
}
private void drawTopLeft(Canvas canvas, Paint paint, int radius,
float width, float height) {
RectF rect = new RectF(
0,
0,
width / 2 + radius + 1,
height / 2 + radius + 1
);
if (radius == 0) canvas.drawRect(rect, paint);
else canvas.drawRoundRect(rect, radius, radius, paint);
}
private void drawTopRight(Canvas canvas, Paint paint, int radius,
float width, float height) {
RectF rect = new RectF(
width / 2 - radius,
0,
width,
height / 2 + radius + 1
);
if (radius == 0) canvas.drawRect(rect, paint);
else canvas.drawRoundRect(rect, radius, radius, paint);
}
private void drawBottomLeft(Canvas canvas, Paint paint, int radius,
float width, float height) {
RectF rect = new RectF(
0,
height / 2 - radius,
width / 2 + radius + 1,
height
);
if (radius == 0) canvas.drawRect(rect, paint);
else canvas.drawRoundRect(rect, radius, radius, paint);
}
private void drawBottomRight(Canvas canvas, Paint paint, int radius,
float width, float height) {
RectF rect = new RectF(
width / 2 - radius,
height / 2 - radius,
width,
height
);
if (radius == 0) canvas.drawRect(rect, paint);
else canvas.drawRoundRect(rect, radius, radius, paint);
}
@Override
public String toString() {
return "ImageCornerTransformation(" + radii + ")";
}
@Override
public boolean equals(Object o) {
return o instanceof CustomCornersTransformation &&
radii.equals(((CustomCornersTransformation) o).radii);
}
@Override
public int hashCode() {
return ID.hashCode() + radii.hashCode();
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update((ID + radii).getBytes(CHARSET));
}
}

View File

@@ -1,111 +0,0 @@
package org.briarproject.briar.android.conversation.glide;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.support.annotation.NonNull;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.security.MessageDigest;
import javax.annotation.concurrent.Immutable;
import static android.graphics.Bitmap.Config.ARGB_8888;
import static android.graphics.Shader.TileMode.CLAMP;
@Immutable
@NotNullByDefault
class ImageCornerTransformation extends BitmapTransformation {
private static final String ID = ImageCornerTransformation.class.getName();
private final int smallRadius, radius;
private final boolean leftCornerSmall, bottomRound;
ImageCornerTransformation(int smallRadius, int radius,
boolean leftCornerSmall, boolean bottomRound) {
this.smallRadius = smallRadius;
this.radius = radius;
this.leftCornerSmall = leftCornerSmall;
this.bottomRound = bottomRound;
}
@Override
protected Bitmap transform(BitmapPool pool, Bitmap toTransform,
int outWidth, int outHeight) {
int width = toTransform.getWidth();
int height = toTransform.getHeight();
Bitmap bitmap = pool.get(width, height, ARGB_8888);
bitmap.setHasAlpha(true);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(new BitmapShader(toTransform, CLAMP, CLAMP));
drawRect(canvas, paint, width, height);
return bitmap;
}
private void drawRect(Canvas canvas, Paint paint, float width,
float height) {
drawSmallCorner(canvas, paint, width);
drawBigCorners(canvas, paint, width, height);
}
private void drawSmallCorner(Canvas canvas, Paint paint, float width) {
float left = leftCornerSmall ? 0 : width - radius;
float right = leftCornerSmall ? radius : width;
canvas.drawRoundRect(new RectF(left, 0, right, radius),
smallRadius, smallRadius, paint);
}
private void drawBigCorners(Canvas canvas, Paint paint, float width,
float height) {
float top = bottomRound ? 0 : radius;
RectF rect = new RectF(0, top, width, height);
if (bottomRound) {
canvas.drawRoundRect(rect, radius, radius, paint);
} else {
canvas.drawRect(rect, paint);
canvas.drawRoundRect(new RectF(0, 0, width, radius * 2),
radius, radius, paint);
}
}
@Override
public String toString() {
return "ImageCornerTransformation(smallRadius=" + smallRadius +
", radius=" + radius + ", leftCornerSmall=" + leftCornerSmall +
", bottomRound=" + bottomRound + ")";
}
@Override
public boolean equals(Object o) {
return o instanceof ImageCornerTransformation &&
((ImageCornerTransformation) o).smallRadius == smallRadius &&
((ImageCornerTransformation) o).radius == radius &&
((ImageCornerTransformation) o).leftCornerSmall ==
leftCornerSmall &&
((ImageCornerTransformation) o).bottomRound == bottomRound;
}
@Override
public int hashCode() {
return ID.hashCode() + (smallRadius << 16) ^ (radius << 2) ^
(leftCornerSmall ? 2 : 0) ^ (bottomRound ? 1 : 0);
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update((ID + '|' + smallRadius + '|' + radius + '|' +
leftCornerSmall + '|' + bottomRound).getBytes(CHARSET));
}
}

View File

@@ -0,0 +1,41 @@
package org.briarproject.briar.android.conversation.glide;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
public class Radii {
public final int topLeft, topRight, bottomLeft, bottomRight;
public Radii(int topLeft, int topRight, int bottomLeft, int bottomRight) {
this.topLeft = topLeft;
this.topRight = topRight;
this.bottomLeft = bottomLeft;
this.bottomRight = bottomRight;
}
@Override
public boolean equals(@Nullable Object o) {
return o instanceof Radii &&
topLeft == ((Radii) o).topLeft &&
topRight == ((Radii) o).topRight &&
bottomLeft == ((Radii) o).bottomLeft &&
bottomRight == ((Radii) o).bottomRight;
}
@Override
public int hashCode() {
return topLeft << 24 ^ topRight << 16 ^ bottomLeft << 8 ^ bottomRight;
}
@Override
public String toString() {
return "Radii(topLeft=" + topLeft +
",topRight=" + topRight +
",bottomLeft=" + bottomLeft +
",bottomRight=" + bottomRight;
}
}

View File

@@ -44,6 +44,7 @@ import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE; import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
@@ -80,13 +81,18 @@ public class ForumListFragment extends BaseEventFragment implements
return fragment; return fragment;
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Nullable @Nullable
@Override @Override
public View onCreateView(LayoutInflater inflater, public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) { @Nullable Bundle savedInstanceState) {
getActivity().setTitle(R.string.forums_button); requireNonNull(getActivity()).setTitle(R.string.forums_button);
View contentView = View contentView =
inflater.inflate(R.layout.fragment_forum_list, container, inflater.inflate(R.layout.fragment_forum_list, container,
@@ -102,7 +108,7 @@ public class ForumListFragment extends BaseEventFragment implements
snackbar.getView().setBackgroundResource(R.color.briar_primary); snackbar.getView().setBackgroundResource(R.color.briar_primary);
snackbar.setAction(R.string.show, this); snackbar.setAction(R.string.show, this);
snackbar.setActionTextColor(ContextCompat snackbar.setActionTextColor(ContextCompat
.getColor(getContext(), R.color.briar_button_text_positive)); .getColor(getActivity(), R.color.briar_button_text_positive));
return contentView; return contentView;
} }
@@ -112,11 +118,6 @@ public class ForumListFragment extends BaseEventFragment implements
return TAG; return TAG;
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();

View File

@@ -10,11 +10,15 @@ import android.support.v4.app.FragmentActivity;
import android.view.MenuItem; import android.view.MenuItem;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.android.DestroyableContext; import org.briarproject.briar.android.DestroyableContext;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public abstract class BaseFragment extends Fragment public abstract class BaseFragment extends Fragment
implements DestroyableContext { implements DestroyableContext {
@@ -22,12 +26,15 @@ public abstract class BaseFragment extends Fragment
public abstract String getUniqueTag(); public abstract String getUniqueTag();
public abstract void injectFragment(ActivityComponent component);
@Override @Override
public void onAttach(Context context) { public void onAttach(Context context) {
super.onAttach(context); super.onAttach(context);
listener = (BaseFragmentListener) context; listener = (BaseFragmentListener) context;
injectFragment(listener.getActivityComponent());
}
public void injectFragment(ActivityComponent component) {
// fragments that need to inject, can override this method
} }
@Override @Override
@@ -38,12 +45,6 @@ public abstract class BaseFragment extends Fragment
setHasOptionsMenu(true); setHasOptionsMenu(true);
} }
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
injectFragment(listener.getActivityComponent());
}
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {

View File

@@ -10,7 +10,6 @@ import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@@ -57,9 +56,4 @@ public class ErrorFragment extends BaseFragment {
return v; return v;
} }
@Override
public void injectFragment(ActivityComponent component) {
// not necessary
}
} }

View File

@@ -1,7 +1,8 @@
package org.briarproject.briar.android.introduction; package org.briarproject.briar.android.introduction;
import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -11,6 +12,8 @@ import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionRegistry; import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
@@ -28,10 +31,14 @@ import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID; import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
@UiThread
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ContactChooserFragment extends BaseFragment { public class ContactChooserFragment extends BaseFragment {
public static final String TAG = ContactChooserFragment.class.getName(); public static final String TAG = ContactChooserFragment.class.getName();
@@ -51,7 +58,6 @@ public class ContactChooserFragment extends BaseFragment {
volatile ConnectionRegistry connectionRegistry; volatile ConnectionRegistry connectionRegistry;
public static ContactChooserFragment newInstance(ContactId id) { public static ContactChooserFragment newInstance(ContactId id) {
Bundle args = new Bundle(); Bundle args = new Bundle();
ContactChooserFragment fragment = new ContactChooserFragment(); ContactChooserFragment fragment = new ContactChooserFragment();
@@ -61,13 +67,13 @@ public class ContactChooserFragment extends BaseFragment {
} }
@Override @Override
public void onAttach(Context context) { public void injectFragment(ActivityComponent component) {
super.onAttach(context); component.inject(this);
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
Bundle savedInstanceState) { @Nullable Bundle savedInstanceState) {
View contentView = inflater.inflate(R.layout.list, container, false); View contentView = inflater.inflate(R.layout.list, container, false);
@@ -77,14 +83,16 @@ public class ContactChooserFragment extends BaseFragment {
Contact c2 = item.getContact(); Contact c2 = item.getContact();
showMessageScreen(c1, c2); showMessageScreen(c1, c2);
}; };
adapter = new ContactListAdapter(getActivity(), onContactClickListener); adapter = new ContactListAdapter(requireNonNull(getActivity()),
onContactClickListener);
list = contentView.findViewById(R.id.list); list = contentView.findViewById(R.id.list);
list.setLayoutManager(new LinearLayoutManager(getActivity())); list.setLayoutManager(new LinearLayoutManager(getActivity()));
list.setAdapter(adapter); list.setAdapter(adapter);
list.setEmptyText(R.string.no_contacts); list.setEmptyText(R.string.no_contacts);
contactId = new ContactId(getArguments().getInt(CONTACT_ID)); contactId = new ContactId(
requireNonNull(getArguments()).getInt(CONTACT_ID));
return contentView; return contentView;
} }
@@ -107,11 +115,6 @@ public class ContactChooserFragment extends BaseFragment {
return TAG; return TAG;
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
private void loadContacts() { private void loadContacts() {
listener.runOnDbThread(() -> { listener.runOnDbThread(() -> {
try { try {

View File

@@ -1,8 +1,8 @@
package org.briarproject.briar.android.introduction; package org.briarproject.briar.android.introduction;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -17,14 +17,17 @@ import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.util.StringUtils; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextInputView.TextInputListener; import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.api.introduction.IntroductionManager; import org.briarproject.briar.api.introduction.IntroductionManager;
import java.util.List;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
@@ -36,13 +39,16 @@ import static android.app.Activity.RESULT_OK;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_SHORT; import static android.widget.Toast.LENGTH_SHORT;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException; 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.getContactDisplayName;
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_TEXT_LENGTH; import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_TEXT_LENGTH;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class IntroductionMessageFragment extends BaseFragment public class IntroductionMessageFragment extends BaseFragment
implements TextInputListener { implements SendListener {
public static final String TAG = public static final String TAG =
IntroductionMessageFragment.class.getName(); IntroductionMessageFragment.class.getName();
@@ -72,11 +78,6 @@ public class IntroductionMessageFragment extends BaseFragment
return fragment; return fragment;
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override @Override
public void onAttach(Context context) { public void onAttach(Context context) {
super.onAttach(context); super.onAttach(context);
@@ -84,8 +85,14 @@ public class IntroductionMessageFragment extends BaseFragment
} }
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, public void injectFragment(ActivityComponent component) {
ViewGroup container, Bundle savedInstanceState) { component.inject(this);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
// change toolbar text // change toolbar text
ActionBar actionBar = introductionActivity.getSupportActionBar(); ActionBar actionBar = introductionActivity.getSupportActionBar();
@@ -93,11 +100,26 @@ public class IntroductionMessageFragment extends BaseFragment
actionBar.setTitle(R.string.introduction_message_title); actionBar.setTitle(R.string.introduction_message_title);
} }
// get contact IDs from fragment arguments
Bundle args = requireNonNull(getArguments());
int contactId1 = args.getInt(CONTACT_ID_1, -1);
int contactId2 = args.getInt(CONTACT_ID_2, -1);
if (contactId1 == -1 || contactId2 == -1) {
throw new AssertionError("Use newInstance() to instantiate");
}
// inflate view // inflate view
View v = inflater.inflate(R.layout.introduction_message, container, View v = inflater.inflate(R.layout.introduction_message, container,
false); false);
ui = new ViewHolder(v); ui = new ViewHolder(v);
ui.message.setSendButtonEnabled(false); TextSendController sendController =
new TextSendController(ui.message, this, true);
ui.message.setSendController(sendController);
ui.message.setMaxTextLength(MAX_INTRODUCTION_TEXT_LENGTH);
ui.message.setEnabled(false);
// get contacts and then show view
prepareToSetUpViews(contactId1, contactId2);
return v; return v;
} }
@@ -105,16 +127,6 @@ public class IntroductionMessageFragment extends BaseFragment
@Override @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
// get contact IDs from fragment arguments
int contactId1 = getArguments().getInt(CONTACT_ID_1, -1);
int contactId2 = getArguments().getInt(CONTACT_ID_2, -1);
if (contactId1 == -1 || contactId2 == -1) {
throw new java.lang.InstantiationError(
"You need to use newInstance() to instantiate");
}
// get contacts and then show view
prepareToSetUpViews(contactId1, contactId2);
} }
@Override @Override
@@ -156,13 +168,10 @@ public class IntroductionMessageFragment extends BaseFragment
ui.progressBar.setVisibility(GONE); ui.progressBar.setVisibility(GONE);
if (possible) { if (possible) {
// set button action
ui.message.setListener(IntroductionMessageFragment.this);
// show views // show views
ui.notPossible.setVisibility(GONE); ui.notPossible.setVisibility(GONE);
ui.message.setVisibility(VISIBLE); ui.message.setVisibility(VISIBLE);
ui.message.setSendButtonEnabled(true); ui.message.setEnabled(true);
ui.message.showSoftKeyboard(); ui.message.showSoftKeyboard();
} else { } else {
ui.notPossible.setVisibility(VISIBLE); ui.notPossible.setVisibility(VISIBLE);
@@ -184,14 +193,11 @@ public class IntroductionMessageFragment extends BaseFragment
} }
@Override @Override
public void onSendClick(@NonNull String text) { public void onSendClick(@Nullable String text, List<Uri> imageUris) {
// disable button to prevent accidental double invitations // disable button to prevent accidental double invitations
ui.message.setSendButtonEnabled(false); ui.message.setEnabled(false);
String txt = ui.message.getText().toString(); makeIntroduction(contact1, contact2, text);
if (txt.isEmpty()) txt = null;
else txt = StringUtils.truncateUtf8(txt, MAX_INTRODUCTION_TEXT_LENGTH);
makeIntroduction(contact1, contact2, txt);
// don't wait for the introduction to be made before finishing activity // don't wait for the introduction to be made before finishing activity
introductionActivity.hideSoftKeyboard(ui.message); introductionActivity.hideSoftKeyboard(ui.message);

View File

@@ -44,6 +44,11 @@ public class ContactExchangeErrorFragment extends BaseFragment {
return TAG; return TAG;
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Nullable @Nullable
@Override @Override
public View onCreateView(LayoutInflater inflater, public View onCreateView(LayoutInflater inflater,
@@ -74,11 +79,6 @@ public class ContactExchangeErrorFragment extends BaseFragment {
return v; return v;
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
private void triggerFeedback() { private void triggerFeedback() {
finish(); finish();
UiUtils.triggerFeedback(androidExecutor); UiUtils.triggerFeedback(androidExecutor);

View File

@@ -10,7 +10,6 @@ import android.widget.ScrollView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.BaseFragment;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -39,11 +38,6 @@ public class IntroFragment extends BaseFragment {
return fragment; return fragment;
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override @Override
public void onAttach(Context context) { public void onAttach(Context context) {
super.onAttach(context); super.onAttach(context);

View File

@@ -53,6 +53,7 @@ import static android.view.View.VISIBLE;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.widget.LinearLayout.HORIZONTAL; import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.Toast.LENGTH_LONG; import static android.widget.Toast.LENGTH_LONG;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
@@ -136,7 +137,8 @@ public class KeyAgreementFragment extends BaseEventFragment
@Override @Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) { public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
getActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR); requireNonNull(getActivity())
.setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR);
cameraView.setPreviewConsumer(new QrCodeDecoder(this)); cameraView.setPreviewConsumer(new QrCodeDecoder(this));
} }

View File

@@ -18,6 +18,7 @@ import javax.annotation.Nullable;
import static android.view.inputmethod.EditorInfo.IME_ACTION_NEXT; import static android.view.inputmethod.EditorInfo.IME_ACTION_NEXT;
import static android.view.inputmethod.EditorInfo.IME_ACTION_NONE; 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.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.briar.android.util.UiUtils.setError; import static org.briarproject.briar.android.util.UiUtils.setError;
@@ -35,11 +36,16 @@ public class AuthorNameFragment extends SetupFragment {
return new AuthorNameFragment(); return new AuthorNameFragment();
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override @Override
public View onCreateView(LayoutInflater inflater, public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) { @Nullable Bundle savedInstanceState) {
getActivity().setTitle(getString(R.string.setup_title)); requireNonNull(getActivity()).setTitle(getString(R.string.setup_title));
View v = inflater.inflate(R.layout.fragment_setup_author_name, View v = inflater.inflate(R.layout.fragment_setup_author_name,
container, false); container, false);
authorNameWrapper = v.findViewById(R.id.nickname_entry_wrapper); authorNameWrapper = v.findViewById(R.id.nickname_entry_wrapper);
@@ -57,11 +63,6 @@ public class AuthorNameFragment extends SetupFragment {
return TAG; return TAG;
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override @Override
protected String getHelpText() { protected String getHelpText() {
return getString(R.string.setup_name_explanation); return getString(R.string.setup_name_explanation);

View File

@@ -19,6 +19,7 @@ import org.briarproject.briar.android.util.UiUtils;
import static android.view.View.INVISIBLE; import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_DOZE_WHITELISTING; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_DOZE_WHITELISTING;
import static org.briarproject.briar.android.util.UiUtils.showOnboardingDialog; import static org.briarproject.briar.android.util.UiUtils.showOnboardingDialog;
@@ -39,11 +40,16 @@ public class DozeFragment extends SetupFragment
return new DozeFragment(); return new DozeFragment();
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override @Override
public View onCreateView(LayoutInflater inflater, public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) { @Nullable Bundle savedInstanceState) {
getActivity().setTitle(getString(R.string.setup_doze_title)); requireNonNull(getActivity()).setTitle(getString(R.string.setup_doze_title));
setHasOptionsMenu(false); setHasOptionsMenu(false);
View v = inflater.inflate(R.layout.fragment_setup_doze, container, View v = inflater.inflate(R.layout.fragment_setup_doze, container,
false); false);
@@ -65,11 +71,6 @@ public class DozeFragment extends SetupFragment
return TAG; return TAG;
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override @Override
protected String getHelpText() { protected String getHelpText() {
return getString(R.string.setup_doze_explanation); return getString(R.string.setup_doze_explanation);

View File

@@ -23,6 +23,7 @@ import static android.content.Context.INPUT_METHOD_SERVICE;
import static android.view.View.INVISIBLE; import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK; import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@@ -43,11 +44,16 @@ public class PasswordFragment extends SetupFragment {
return new PasswordFragment(); return new PasswordFragment();
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override @Override
public View onCreateView(LayoutInflater inflater, public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) { @Nullable Bundle savedInstanceState) {
getActivity().setTitle(getString(R.string.setup_password_intro)); requireNonNull(getActivity()).setTitle(getString(R.string.setup_password_intro));
View v = inflater.inflate(R.layout.fragment_setup_password, container, View v = inflater.inflate(R.layout.fragment_setup_password, container,
false); false);
@@ -64,6 +70,11 @@ public class PasswordFragment extends SetupFragment {
passwordConfirmation.addTextChangedListener(this); passwordConfirmation.addTextChangedListener(this);
nextButton.setOnClickListener(this); nextButton.setOnClickListener(this);
if (!setupController.needToShowDozeFragment()) {
nextButton.setText(R.string.create_account_button);
passwordConfirmation.setImeOptions(IME_ACTION_DONE);
}
return v; return v;
} }
@@ -72,17 +83,6 @@ public class PasswordFragment extends SetupFragment {
return TAG; return TAG;
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
// the controller is not yet available in onCreateView()
if (!setupController.needToShowDozeFragment()) {
nextButton.setText(R.string.create_account_button);
passwordConfirmation.setImeOptions(IME_ACTION_DONE);
}
}
@Override @Override
protected String getHelpText() { protected String getHelpText() {
return getString(R.string.setup_password_explanation); return getString(R.string.setup_password_explanation);

View File

@@ -11,7 +11,8 @@ import android.view.View.OnClickListener;
import android.widget.TextView; import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener; import android.widget.TextView.OnEditorActionListener;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.BaseFragment;
@@ -22,7 +23,8 @@ import static android.view.inputmethod.EditorInfo.IME_ACTION_NEXT;
import static org.briarproject.briar.android.util.UiUtils.enterPressed; import static org.briarproject.briar.android.util.UiUtils.enterPressed;
import static org.briarproject.briar.android.util.UiUtils.showOnboardingDialog; import static org.briarproject.briar.android.util.UiUtils.showOnboardingDialog;
@NotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault
abstract class SetupFragment extends BaseFragment implements TextWatcher, abstract class SetupFragment extends BaseFragment implements TextWatcher,
OnEditorActionListener, OnClickListener { OnEditorActionListener, OnClickListener {

View File

@@ -1,14 +1,13 @@
package org.briarproject.briar.android.logout; package org.briarproject.briar.android.logout;
import android.os.Build; import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BaseActivity;
import java.util.logging.Logger; import java.util.logging.Logger;
public class ExitActivity extends BaseActivity { import static android.os.Build.VERSION.SDK_INT;
public class ExitActivity extends Activity {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(ExitActivity.class.getName()); Logger.getLogger(ExitActivity.class.getName());
@@ -16,14 +15,9 @@ public class ExitActivity extends BaseActivity {
@Override @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
super.onCreate(state); super.onCreate(state);
if (Build.VERSION.SDK_INT >= 21) finishAndRemoveTask(); if (SDK_INT >= 21) finishAndRemoveTask();
else finish(); else finish();
LOG.info("Exiting"); LOG.info("Exiting");
System.exit(0); System.exit(0);
} }
@Override
public void injectActivity(ActivityComponent component) {
}
} }

View File

@@ -1,20 +1,13 @@
package org.briarproject.briar.android.logout; package org.briarproject.briar.android.logout;
import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import org.briarproject.briar.android.activity.ActivityComponent; public class HideUiActivity extends Activity {
import org.briarproject.briar.android.activity.BaseActivity;
public class HideUiActivity extends BaseActivity {
@Override @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
super.onCreate(state); super.onCreate(state);
finish(); finish();
} }
@Override
public void injectActivity(ActivityComponent component) {
}
} }

View File

@@ -5,19 +5,21 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.BaseFragment;
import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class SignOutFragment extends BaseFragment { public class SignOutFragment extends BaseFragment {
public static final String TAG = SignOutFragment.class.getName(); public static final String TAG = SignOutFragment.class.getName();
@Override @Override
public View onCreateView(@Nonnull LayoutInflater inflater, public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) { @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_sign_out, container, false); return inflater.inflate(R.layout.fragment_sign_out, container, false);
@@ -27,9 +29,4 @@ public class SignOutFragment extends BaseFragment {
public String getUniqueTag() { public String getUniqueTag() {
return TAG; return TAG;
} }
@Override
public void injectFragment(ActivityComponent component) {
// no need to inject
}
} }

View File

@@ -5,6 +5,7 @@ import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.NavigationView; import android.support.design.widget.NavigationView;
import android.support.design.widget.NavigationView.OnNavigationItemSelectedListener; import android.support.design.widget.NavigationView.OnNavigationItemSelectedListener;
import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityCompat;
@@ -12,6 +13,7 @@ import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction; import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v4.widget.DrawerLayout; import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -25,6 +27,8 @@ import android.widget.TextView;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.BluetoothConstants; import org.briarproject.bramble.api.plugin.BluetoothConstants;
import org.briarproject.bramble.api.plugin.LanTcpConstants; import org.briarproject.bramble.api.plugin.LanTcpConstants;
import org.briarproject.bramble.api.plugin.TorConstants; import org.briarproject.bramble.api.plugin.TorConstants;
@@ -54,6 +58,7 @@ import static android.support.v4.view.GravityCompat.START;
import static android.support.v4.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED; import static android.support.v4.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
import static org.briarproject.briar.android.BriarService.EXTRA_STARTUP_FAILED; import static org.briarproject.briar.android.BriarService.EXTRA_STARTUP_FAILED;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSWORD; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSWORD;
@@ -61,6 +66,8 @@ import static org.briarproject.briar.android.navdrawer.NavDrawerController.Expir
import static org.briarproject.briar.android.navdrawer.NavDrawerController.ExpiryWarning.UPDATE; import static org.briarproject.briar.android.navdrawer.NavDrawerController.ExpiryWarning.UPDATE;
import static org.briarproject.briar.android.util.UiUtils.getDaysUntilExpiry; import static org.briarproject.briar.android.util.UiUtils.getDaysUntilExpiry;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class NavDrawerActivity extends BriarActivity implements public class NavDrawerActivity extends BriarActivity implements
BaseFragmentListener, TransportStateListener, BaseFragmentListener, TransportStateListener,
OnNavigationItemSelectedListener { OnNavigationItemSelectedListener {
@@ -112,9 +119,8 @@ public class NavDrawerActivity extends BriarActivity implements
component.inject(this); component.inject(this);
} }
@SuppressWarnings("ConstantConditions")
@Override @Override
public void onCreate(Bundle state) { public void onCreate(@Nullable Bundle state) {
super.onCreate(state); super.onCreate(state);
exitIfStartupFailed(getIntent()); exitIfStartupFailed(getIntent());
setContentView(R.layout.activity_nav_drawer); setContentView(R.layout.activity_nav_drawer);
@@ -125,8 +131,9 @@ public class NavDrawerActivity extends BriarActivity implements
GridView transportsView = findViewById(R.id.transportsView); GridView transportsView = findViewById(R.id.transportsView);
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true); ActionBar actionBar = requireNonNull(getSupportActionBar());
getSupportActionBar().setHomeButtonEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar,
R.string.nav_drawer_open_description, R.string.nav_drawer_open_description,
@@ -165,7 +172,8 @@ public class NavDrawerActivity extends BriarActivity implements
} }
@Override @Override
protected void onActivityResult(int request, int result, Intent data) { protected void onActivityResult(int request, int result,
@Nullable Intent data) {
super.onActivityResult(request, result, data); super.onActivityResult(request, result, data);
if (request == REQUEST_PASSWORD && result == RESULT_OK) { if (request == REQUEST_PASSWORD && result == RESULT_OK) {
controller.shouldAskForDozeWhitelisting(this, controller.shouldAskForDozeWhitelisting(this,
@@ -253,7 +261,7 @@ public class NavDrawerActivity extends BriarActivity implements
} }
@Override @Override
public void onPostCreate(Bundle savedInstanceState) { public void onPostCreate(@Nullable Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState); super.onPostCreate(savedInstanceState);
drawerToggle.syncState(); drawerToggle.syncState();
} }

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