Merge branch '804-self-destructing-messages' into 'master'

Merge 'Self-destruct timer for messages' to master

Closes #1863

See merge request briar/briar!1396
This commit is contained in:
akwizgran
2021-04-15 15:24:19 +00:00
237 changed files with 10871 additions and 1882 deletions

View File

@@ -29,6 +29,7 @@ import org.briarproject.bramble.api.system.AndroidWakeLockManager;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
import org.briarproject.bramble.system.ClockModule;
import org.briarproject.briar.BriarCoreEagerSingletons;
import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule;
@@ -46,6 +47,7 @@ import org.briarproject.briar.api.android.DozeWatchdog;
import org.briarproject.briar.api.android.LockManager;
import org.briarproject.briar.api.android.ScreenFilterMonitor;
import org.briarproject.briar.api.attachment.AttachmentReader;
import org.briarproject.briar.api.autodelete.AutoDeleteManager;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPostFactory;
import org.briarproject.briar.api.blog.BlogSharingManager;
@@ -80,6 +82,7 @@ import dagger.Component;
BriarAccountModule.class,
AppModule.class,
AttachmentModule.class,
ClockModule.class,
MediaModule.class
})
public interface AndroidComponent
@@ -188,6 +191,8 @@ public interface AndroidComponent
Thread.UncaughtExceptionHandler exceptionHandler();
AutoDeleteManager autoDeleteManager();
void inject(SignInReminderReceiver briarService);
void inject(BriarService briarService);

View File

@@ -37,6 +37,7 @@ import org.briarproject.briar.android.splash.SplashScreenActivity;
import org.briarproject.briar.android.util.BriarNotificationBuilder;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
import org.briarproject.briar.api.conversation.ConversationResponse;
import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent;
import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent;
import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent;
@@ -226,6 +227,12 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
} else if (e instanceof ConversationMessageReceivedEvent) {
ConversationMessageReceivedEvent<?> p =
(ConversationMessageReceivedEvent<?>) e;
if (p.getMessageHeader() instanceof ConversationResponse) {
ConversationResponse r =
(ConversationResponse) p.getMessageHeader();
// don't show notification for own auto-decline responses
if (r.isAutoDecline()) return;
}
showContactNotification(p.getContactId());
} else if (e instanceof GroupMessageAddedEvent) {
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;

View File

@@ -293,6 +293,11 @@ public class AppModule {
public boolean shouldEnableProfilePictures() {
return IS_DEBUG_BUILD;
}
@Override
public boolean shouldEnableDisappearingMessages() {
return IS_DEBUG_BUILD;
}
};
}
}

View File

@@ -29,6 +29,7 @@ import org.briarproject.briar.android.contact.add.remote.NicknameFragment;
import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity;
import org.briarproject.briar.android.conversation.AliasDialogFragment;
import org.briarproject.briar.android.conversation.ConversationActivity;
import org.briarproject.briar.android.conversation.ConversationSettingsDialog;
import org.briarproject.briar.android.conversation.ImageActivity;
import org.briarproject.briar.android.conversation.ImageFragment;
import org.briarproject.briar.android.forum.CreateForumActivity;
@@ -233,4 +234,6 @@ public interface ActivityComponent {
void inject(ConfirmAvatarDialogFragment fragment);
void inject(ConversationSettingsDialog dialog);
}

View File

@@ -17,6 +17,7 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.android.view.TextSendController.SendState;
import org.briarproject.briar.android.widget.LinkDialogFragment;
import org.briarproject.briar.api.attachment.AttachmentHeader;
@@ -25,6 +26,8 @@ import java.util.List;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModelProvider;
import static android.view.View.FOCUS_DOWN;
@@ -34,6 +37,7 @@ 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.blog.BlogPostFragment.POST_ID;
import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
@MethodsNotNullByDefault
@@ -114,11 +118,12 @@ public class ReblogFragment extends BaseFragment implements SendListener {
}
@Override
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
public LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> headers, long expectedAutoDeleteTimer) {
ui.input.hideSoftKeyboard();
viewModel.repeatPost(item, text);
finish();
return new MutableLiveData<>(SENT);
}
private void showProgressBar() {

View File

@@ -31,12 +31,16 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.briar.android.view.TextSendController.SendState;
import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
@MethodsNotNullByDefault
@@ -112,8 +116,8 @@ public class WriteBlogPostActivity extends BriarActivity
}
@Override
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
public LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> headers, long expectedAutoDeleteTimer) {
if (isNullOrEmpty(text)) throw new AssertionError();
// hide publish button, show progress bar
@@ -122,6 +126,7 @@ public class WriteBlogPostActivity extends BriarActivity
progressBar.setVisibility(VISIBLE);
storePost(text);
return new MutableLiveData<>(SENT);
}
private void storePost(String text) {

View File

@@ -31,6 +31,8 @@ import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
// TODO: we can probably switch to androidx DialogFragment here but need to
// test this properly
public class AliasDialogFragment extends AppCompatDialogFragment {
final static String TAG = AliasDialogFragment.class.getName();

View File

@@ -50,6 +50,7 @@ import org.briarproject.briar.android.blog.BlogActivity;
import org.briarproject.briar.android.conversation.ConversationVisitor.AttachmentCache;
import org.briarproject.briar.android.conversation.ConversationVisitor.TextCache;
import org.briarproject.briar.android.forum.ForumActivity;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import org.briarproject.briar.android.introduction.IntroductionActivity;
import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
@@ -59,8 +60,10 @@ import org.briarproject.briar.android.view.TextAttachmentController;
import org.briarproject.briar.android.view.TextAttachmentController.AttachmentListener;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendState;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.attachment.AttachmentHeader;
import org.briarproject.briar.api.autodelete.event.ConversationMessagesDeletedEvent;
import org.briarproject.briar.api.blog.BlogSharingManager;
import org.briarproject.briar.api.client.ProtocolStateException;
import org.briarproject.briar.api.client.SessionId;
@@ -138,12 +141,14 @@ import static org.briarproject.briar.android.util.UiUtils.observeOnce;
import static org.briarproject.briar.android.view.AuthorView.setAvatar;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH;
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_IMAGES_AUTO_DELETE;
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_ONLY;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ConversationActivity extends BriarActivity
implements EventListener, ConversationListener, TextCache,
AttachmentCache, AttachmentListener, ActionMode.Callback {
implements BaseFragmentListener, EventListener, ConversationListener,
TextCache, AttachmentCache, AttachmentListener, ActionMode.Callback {
public static final String CONTACT_ID = "briar.CONTACT_ID";
@@ -268,15 +273,11 @@ public class ConversationActivity extends BriarActivity
ImagePreview imagePreview = findViewById(R.id.imagePreview);
sendController = new TextAttachmentController(textInputView,
imagePreview, this, viewModel);
viewModel.hasImageSupport().observe(this, new Observer<Boolean>() {
@Override
public void onChanged(@Nullable Boolean hasSupport) {
if (hasSupport != null && hasSupport) {
// TODO: remove cast when removing feature flag
((TextAttachmentController) sendController)
.setImagesSupported();
viewModel.hasImageSupport().removeObserver(this);
}
observeOnce(viewModel.getPrivateMessageFormat(), this, format -> {
if (format != TEXT_ONLY) {
// TODO: remove cast when removing feature flag
((TextAttachmentController) sendController)
.setImagesSupported();
}
});
} else {
@@ -286,6 +287,9 @@ public class ConversationActivity extends BriarActivity
textInputView.setMaxTextLength(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
textInputView.setReady(false);
textInputView.setOnKeyboardShownListener(this::scrollToBottom);
viewModel.getAutoDeleteTimer().observe(this, timer ->
sendController.setAutoDeleteTimer(timer));
}
private void scrollToBottom() {
@@ -369,6 +373,14 @@ public class ConversationActivity extends BriarActivity
// enable alias action if available
observeOnce(viewModel.getContactItem(), this, contact ->
menu.findItem(R.id.action_set_alias).setEnabled(true));
// Show auto-delete menu item if feature is enabled
if (featureFlags.shouldEnableDisappearingMessages()) {
MenuItem item = menu.findItem(R.id.action_conversation_settings);
item.setVisible(true);
// Enable menu item only if contact supports auto-delete
viewModel.getPrivateMessageFormat().observe(this, format ->
item.setEnabled(format == TEXT_IMAGES_AUTO_DELETE));
}
return super.onCreateOptionsMenu(menu);
}
@@ -390,6 +402,10 @@ public class ConversationActivity extends BriarActivity
AliasDialogFragment.newInstance().show(
getSupportFragmentManager(), AliasDialogFragment.TAG);
return true;
case R.id.action_conversation_settings:
if (contactId == null) return false;
onAutoDeleteTimerNoticeClicked();
return true;
case R.id.action_delete_all_messages:
askToDeleteAllMessages();
return true;
@@ -559,7 +575,7 @@ public class ConversationActivity extends BriarActivity
this::showImageOnboarding);
}
List<ConversationItem> items = createItems(headers);
adapter.addAll(items);
adapter.replaceAll(items);
list.showData();
if (layoutManagerState == null) {
scrollToBottom();
@@ -640,8 +656,8 @@ public class ConversationActivity extends BriarActivity
supportFinishAfterTransition();
}
} else if (e instanceof ConversationMessageReceivedEvent) {
ConversationMessageReceivedEvent p =
(ConversationMessageReceivedEvent) e;
ConversationMessageReceivedEvent<?> p =
(ConversationMessageReceivedEvent<?>) e;
if (p.getContactId().equals(contactId)) {
LOG.info("Message received, adding");
onNewConversationMessage(p.getMessageHeader());
@@ -658,6 +674,13 @@ public class ConversationActivity extends BriarActivity
LOG.info("Messages acked");
markMessages(m.getMessageIds(), true, true);
}
} else if (e instanceof ConversationMessagesDeletedEvent) {
ConversationMessagesDeletedEvent m =
(ConversationMessagesDeletedEvent) e;
if (m.getContactId().equals(contactId)) {
LOG.info("Messages auto-deleted");
onConversationMessagesDeleted(m.getMessageIds());
}
} else if (e instanceof ContactConnectedEvent) {
ContactConnectedEvent c = (ContactConnectedEvent) e;
if (c.getContactId().equals(contactId)) {
@@ -705,6 +728,13 @@ public class ConversationActivity extends BriarActivity
}
}
@UiThread
private void onConversationMessagesDeleted(
Collection<MessageId> messageIds) {
adapter.incrementRevision();
adapter.removeItems(messageIds);
}
@UiThread
private void markMessages(Collection<MessageId> messageIds, boolean sent,
boolean seen) {
@@ -735,20 +765,13 @@ public class ConversationActivity extends BriarActivity
}
@Override
public void onSendClick(@Nullable String text,
List<AttachmentHeader> attachmentHeaders) {
public LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> attachmentHeaders,
long expectedAutoDeleteTimer) {
if (isNullOrEmpty(text) && attachmentHeaders.isEmpty())
throw new AssertionError();
long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
viewModel.sendMessage(text, attachmentHeaders, timestamp);
textInputView.clearText();
}
private long getMinTimestampForNewMessage() {
// Don't use an earlier timestamp than the newest message
ConversationItem item = adapter.getLastItem();
return item == null ? 0 : item.getTime() + 1;
return viewModel
.sendMessage(text, attachmentHeaders, expectedAutoDeleteTimer);
}
private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) {
@@ -823,10 +846,6 @@ public class ConversationActivity extends BriarActivity
fails.add(getString(
R.string.dialog_message_not_deleted_ongoing_invitations));
}
if (result.hasNotFullyDownloaded()) {
fails.add(getString(
R.string.dialog_message_not_deleted_partly_downloaded));
}
// add problems the user can resolve
if (result.hasNotAllIntroductionSelected() &&
result.hasNotAllInvitationSelected()) {
@@ -958,13 +977,11 @@ public class ConversationActivity extends BriarActivity
adapter.notifyItemChanged(position, item);
}
runOnDbThread(() -> {
long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
try {
switch (item.getRequestType()) {
case INTRODUCTION:
respondToIntroductionRequest(item.getSessionId(),
accept, timestamp);
accept);
break;
case FORUM:
respondToForumRequest(item.getSessionId(), accept);
@@ -1038,11 +1055,18 @@ public class ConversationActivity extends BriarActivity
ActivityCompat.startActivity(this, i, options.toBundle());
}
@Override
public void onAutoDeleteTimerNoticeClicked() {
ConversationSettingsDialog dialog =
ConversationSettingsDialog.newInstance(contactId);
dialog.show(getSupportFragmentManager(),
ConversationSettingsDialog.TAG);
}
@DatabaseExecutor
private void respondToIntroductionRequest(SessionId sessionId,
boolean accept, long time) throws DbException {
introductionManager.respondToIntroduction(contactId, sessionId, time,
accept);
boolean accept) throws DbException {
introductionManager.respondToIntroduction(contactId, sessionId, accept);
}
@DatabaseExecutor

View File

@@ -13,20 +13,26 @@ import org.briarproject.briar.R;
import org.briarproject.briar.android.util.BriarAdapter;
import org.briarproject.briar.android.util.ItemReturningAdapter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import androidx.annotation.LayoutRes;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.recyclerview.selection.SelectionTracker;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
@NotNullByDefault
class ConversationAdapter
extends BriarAdapter<ConversationItem, ConversationItemViewHolder>
implements ItemReturningAdapter<ConversationItem> {
private ConversationListener listener;
private final ConversationListener listener;
private final RecycledViewPool imageViewPool;
private final ImageItemDecoration imageItemDecoration;
@Nullable
@@ -65,22 +71,20 @@ class ConversationAdapter
@LayoutRes int type) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate(
type, viewGroup, false);
switch (type) {
case R.layout.list_item_conversation_msg_in:
return new ConversationMessageViewHolder(v, listener, true,
imageViewPool, imageItemDecoration);
case R.layout.list_item_conversation_msg_out:
return new ConversationMessageViewHolder(v, listener, false,
imageViewPool, imageItemDecoration);
case R.layout.list_item_conversation_notice_in:
return new ConversationNoticeViewHolder(v, listener, true);
case R.layout.list_item_conversation_notice_out:
return new ConversationNoticeViewHolder(v, listener, false);
case R.layout.list_item_conversation_request:
return new ConversationRequestViewHolder(v, listener, true);
default:
throw new IllegalArgumentException("Unknown ConversationItem");
if (type == R.layout.list_item_conversation_msg_in) {
return new ConversationMessageViewHolder(v, listener, true,
imageViewPool, imageItemDecoration);
} else if (type == R.layout.list_item_conversation_msg_out) {
return new ConversationMessageViewHolder(v, listener, false,
imageViewPool, imageItemDecoration);
} else if (type == R.layout.list_item_conversation_notice_in) {
return new ConversationNoticeViewHolder(v, listener, true);
} else if (type == R.layout.list_item_conversation_notice_out) {
return new ConversationNoticeViewHolder(v, listener, false);
} else if (type == R.layout.list_item_conversation_request) {
return new ConversationRequestViewHolder(v, listener, true);
}
throw new IllegalArgumentException("Unknown ConversationItem");
}
@Override
@@ -107,22 +111,65 @@ class ConversationAdapter
return c1.equals(c2);
}
@Override
public void add(ConversationItem item) {
items.beginBatchedUpdates();
items.add(item);
updateTimersInBatch();
items.endBatchedUpdates();
}
@Override
public void replaceAll(Collection<ConversationItem> itemsToReplace) {
items.beginBatchedUpdates();
// there can be items already in the adapter
// SortedList takes care of duplicates and detecting changed items
items.replaceAll(itemsToReplace);
updateTimersInBatch();
items.endBatchedUpdates();
}
@UiThread
void removeItems(Collection<MessageId> messageIds) {
// Collect all items to be deleted first
// and then delete them in one batched update.
// Deleting them right away would cause issues
// due to changing list positions.
List<ConversationItem> toRemove = new ArrayList<>(messageIds.size());
for (int i = 0; i < items.size(); i++) {
ConversationItem item = items.get(i);
if (messageIds.contains(item.getId())) toRemove.add(item);
}
items.beginBatchedUpdates();
for (ConversationItem item : toRemove) items.remove(item);
items.endBatchedUpdates();
}
private void updateTimersInBatch() {
long lastTimerIncoming = NO_AUTO_DELETE_TIMER;
long lastTimerOutgoing = NO_AUTO_DELETE_TIMER;
for (int i = 0; i < items.size(); i++) {
ConversationItem c = items.get(i);
boolean itemChanged;
boolean timerChanged;
if (c.isIncoming()) {
timerChanged = lastTimerIncoming != c.getAutoDeleteTimer();
lastTimerIncoming = c.getAutoDeleteTimer();
} else {
timerChanged = lastTimerOutgoing != c.getAutoDeleteTimer();
lastTimerOutgoing = c.getAutoDeleteTimer();
}
itemChanged = c.setTimerNoticeVisible(timerChanged);
if (itemChanged) items.updateItemAt(i, c);
}
}
void setSelectionTracker(SelectionTracker<String> tracker) {
this.tracker = tracker;
}
@Nullable
ConversationItem getLastItem() {
if (items.size() > 0) {
return items.get(items.size() - 1);
} else {
return null;
}
}
SparseArray<ConversationItem> getOutgoingMessages() {
SparseArray<ConversationItem> messages = new SparseArray<>();
for (int i = 0; i < items.size(); i++) {
ConversationItem item = items.get(i);
if (!item.isIncoming()) {

View File

@@ -9,6 +9,7 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes;
import androidx.lifecycle.LiveData;
import static org.briarproject.bramble.util.StringUtils.toHexString;
@@ -22,20 +23,25 @@ abstract class ConversationItem {
protected String text;
private final MessageId id;
private final GroupId groupId;
private final long time;
private final long time, autoDeleteTimer;
private final boolean isIncoming;
private boolean read, sent, seen;
private final LiveData<String> contactName;
private boolean read, sent, seen, showTimerNotice;
ConversationItem(@LayoutRes int layoutRes, ConversationMessageHeader h) {
ConversationItem(@LayoutRes int layoutRes, ConversationMessageHeader h,
LiveData<String> contactName) {
this.layoutRes = layoutRes;
this.text = null;
this.id = h.getId();
this.groupId = h.getGroupId();
this.time = h.getTimestamp();
this.autoDeleteTimer = h.getAutoDeleteTimer();
this.read = h.isRead();
this.sent = h.isSent();
this.seen = h.isSeen();
this.isIncoming = !h.isLocal();
this.contactName = contactName;
this.showTimerNotice = false;
}
@LayoutRes
@@ -68,6 +74,10 @@ abstract class ConversationItem {
return time;
}
public long getAutoDeleteTimer() {
return autoDeleteTimer;
}
/**
* Only useful for incoming messages.
*/
@@ -111,4 +121,25 @@ abstract class ConversationItem {
return isIncoming;
}
public LiveData<String> getContactName() {
return contactName;
}
/**
* Set this to true when {@link #getAutoDeleteTimer()} has changed
* since the last message from the same peer.
*
* @return true if the value was set, false if it was already set.
*/
boolean setTimerNoticeVisible(boolean visible) {
if (this.showTimerNotice != visible) {
this.showTimerNotice = visible;
return true;
}
return false;
}
boolean isTimerNoticeVisible() {
return showTimerNotice;
}
}

View File

@@ -1,6 +1,8 @@
package org.briarproject.briar.android.conversation;
import android.content.Context;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -12,8 +14,12 @@ import androidx.annotation.UiThread;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.bramble.util.StringUtils.trim;
import static org.briarproject.briar.android.util.UiUtils.formatDate;
import static org.briarproject.briar.android.util.UiUtils.formatDuration;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
@UiThread
@NotNullByDefault
@@ -24,8 +30,9 @@ abstract class ConversationItemViewHolder extends ViewHolder {
protected final ConstraintLayout layout;
@Nullable
private final OutItemViewHolder outViewHolder;
private final TextView text;
private final TextView topNotice, text;
protected final TextView time;
protected final ImageView bomb;
@Nullable
private String itemKey = null;
@@ -33,11 +40,13 @@ abstract class ConversationItemViewHolder extends ViewHolder {
boolean isIncoming) {
super(v);
this.listener = listener;
this.outViewHolder = isIncoming ? null : new OutItemViewHolder(v);
outViewHolder = isIncoming ? null : new OutItemViewHolder(v);
root = v;
topNotice = v.findViewById(R.id.topNotice);
layout = v.findViewById(R.id.layout);
text = v.findViewById(R.id.text);
time = v.findViewById(R.id.time);
bomb = v.findViewById(R.id.bomb);
}
@CallSuper
@@ -45,6 +54,8 @@ abstract class ConversationItemViewHolder extends ViewHolder {
itemKey = item.getKey();
root.setActivated(selected);
setTopNotice(item);
if (item.getText() != null) {
text.setText(trim(item.getText()));
}
@@ -52,6 +63,9 @@ abstract class ConversationItemViewHolder extends ViewHolder {
long timestamp = item.getTime();
time.setText(formatDate(time.getContext(), timestamp));
boolean showBomb = item.getAutoDeleteTimer() != NO_AUTO_DELETE_TIMER;
bomb.setVisibility(showBomb ? VISIBLE : GONE);
if (outViewHolder != null) outViewHolder.bind(item);
}
@@ -64,4 +78,35 @@ abstract class ConversationItemViewHolder extends ViewHolder {
return itemKey;
}
private void setTopNotice(ConversationItem item) {
if (item.isTimerNoticeVisible()) {
Context ctx = itemView.getContext();
topNotice.setVisibility(VISIBLE);
boolean enabled = item.getAutoDeleteTimer() != NO_AUTO_DELETE_TIMER;
String duration = enabled ?
formatDuration(ctx, item.getAutoDeleteTimer()) : "";
String tapToLearnMore = ctx.getString(R.string.tap_to_learn_more);
String text;
if (item.isIncoming()) {
String name = item.getContactName().getValue();
text = enabled ?
ctx.getString(R.string.auto_delete_msg_contact_enabled,
name, duration, tapToLearnMore) :
ctx.getString(R.string.auto_delete_msg_contact_disabled,
name, tapToLearnMore);
} else {
text = enabled ?
ctx.getString(R.string.auto_delete_msg_you_enabled,
duration, tapToLearnMore) :
ctx.getString(R.string.auto_delete_msg_you_disabled,
tapToLearnMore);
}
topNotice.setText(text);
topNotice.setOnClickListener(
v -> listener.onAutoDeleteTimerNoticeClicked());
} else {
topNotice.setVisibility(GONE);
}
}
}

View File

@@ -18,4 +18,6 @@ interface ConversationListener {
void onAttachmentClicked(View view, ConversationMessageItem messageItem,
AttachmentItem attachmentItem);
void onAutoDeleteTimerNoticeClicked();
}

View File

@@ -10,6 +10,7 @@ import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes;
import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
@NotThreadSafe
@NotNullByDefault
@@ -18,8 +19,8 @@ class ConversationMessageItem extends ConversationItem {
private final List<AttachmentItem> attachments;
ConversationMessageItem(@LayoutRes int layoutRes, PrivateMessageHeader h,
List<AttachmentItem> attachments) {
super(layoutRes, h);
LiveData<String> contactName, List<AttachmentItem> attachments) {
super(layoutRes, h, contactName);
this.attachments = attachments;
}

View File

@@ -1,5 +1,6 @@
package org.briarproject.briar.android.conversation;
import android.content.res.ColorStateList;
import android.view.View;
import android.view.ViewGroup;
@@ -14,6 +15,7 @@ import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT;
import static androidx.core.content.ContextCompat.getColor;
import static androidx.core.widget.ImageViewCompat.setImageTintList;
@UiThread
@NotNullByDefault
@@ -84,6 +86,7 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
if (item.getText() == null) {
statusLayout.setBackgroundResource(R.drawable.msg_status_bubble);
time.setTextColor(timeColorBubble);
setImageTintList(bomb, ColorStateList.valueOf(timeColorBubble));
constraintSet = imageConstraints;
} else {
resetStatusLayoutForText();
@@ -111,6 +114,7 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
// also reset padding (the background drawable defines some)
statusLayout.setPadding(0, 0, 0, 0);
time.setTextColor(timeColor);
setImageTintList(bomb, ColorStateList.valueOf(timeColor));
}
}

View File

@@ -8,6 +8,7 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes;
import androidx.lifecycle.LiveData;
@NotThreadSafe
@NotNullByDefault
@@ -17,15 +18,15 @@ class ConversationNoticeItem extends ConversationItem {
private final String msgText;
ConversationNoticeItem(@LayoutRes int layoutRes, String text,
ConversationRequest r) {
super(layoutRes, r);
LiveData<String> contactName, ConversationRequest<?> r) {
super(layoutRes, r, contactName);
this.text = text;
this.msgText = r.getText();
}
ConversationNoticeItem(@LayoutRes int layoutRes, String text,
ConversationResponse r) {
super(layoutRes, r);
LiveData<String> contactName, ConversationResponse r) {
super(layoutRes, r, contactName);
this.text = text;
this.msgText = null;
}

View File

@@ -11,6 +11,7 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes;
import androidx.lifecycle.LiveData;
@NotThreadSafe
@NotNullByDefault
@@ -26,14 +27,15 @@ class ConversationRequestItem extends ConversationNoticeItem {
private boolean answered;
ConversationRequestItem(@LayoutRes int layoutRes, String text,
RequestType type, ConversationRequest r) {
super(layoutRes, text, r);
LiveData<String> contactName, RequestType type,
ConversationRequest<?> r) {
super(layoutRes, text, contactName, r);
this.requestType = type;
this.sessionId = r.getSessionId();
this.answered = r.wasAnswered();
if (r instanceof InvitationRequest) {
this.requestedGroupId = ((Shareable) r.getNameable()).getId();
this.canBeOpened = ((InvitationRequest) r).canBeOpened();
this.canBeOpened = ((InvitationRequest<?>) r).canBeOpened();
} else {
this.requestedGroupId = null;
this.canBeOpened = false;

View File

@@ -0,0 +1,126 @@
package org.briarproject.briar.android.conversation;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.widget.OnboardingFullDialogFragment;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SwitchCompat;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import static java.util.logging.Level.INFO;
import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ConversationSettingsDialog extends DialogFragment {
final static String TAG = ConversationSettingsDialog.class.getName();
private static final Logger LOG = Logger.getLogger(TAG);
@Inject
ViewModelProvider.Factory viewModelFactory;
private ConversationViewModel viewModel;
static ConversationSettingsDialog newInstance(ContactId contactId) {
Bundle args = new Bundle();
args.putInt(CONTACT_ID, contactId.getInt());
ConversationSettingsDialog dialog = new ConversationSettingsDialog();
dialog.setArguments(args);
return dialog;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
injectFragment(((BaseFragment.BaseFragmentListener) context)
.getActivityComponent());
}
public void injectFragment(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(ConversationViewModel.class);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NO_FRAME,
R.style.BriarFullScreenDialogTheme);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_conversation_settings,
container, false);
Bundle args = requireArguments();
int id = args.getInt(CONTACT_ID, -1);
if (id == -1) throw new IllegalStateException();
ContactId contactId = new ContactId(id);
FragmentActivity activity = requireActivity();
viewModel = new ViewModelProvider(activity, viewModelFactory)
.get(ConversationViewModel.class);
viewModel.setContactId(contactId);
Toolbar toolbar = view.findViewById(R.id.toolbar);
toolbar.setNavigationOnClickListener(v -> dismiss());
SwitchCompat switchDisappearingMessages = view.findViewById(
R.id.switchDisappearingMessages);
switchDisappearingMessages.setOnCheckedChangeListener(
(button, value) -> viewModel.setAutoDeleteTimerEnabled(value));
Button buttonLearnMore =
view.findViewById(R.id.buttonLearnMore);
buttonLearnMore.setOnClickListener(e -> showLearnMoreDialog());
viewModel.getAutoDeleteTimer()
.observe(getViewLifecycleOwner(), timer -> {
if (LOG.isLoggable(INFO)) {
LOG.info("Received auto delete timer: " + timer);
}
boolean disappearingMessages =
timer != NO_AUTO_DELETE_TIMER;
switchDisappearingMessages
.setChecked(disappearingMessages);
switchDisappearingMessages.setEnabled(true);
});
return view;
}
private void showLearnMoreDialog() {
OnboardingFullDialogFragment.newInstance(
R.string.disappearing_messages_title,
R.string.disappearing_messages_explanation_long
).show(getChildFragmentManager(), OnboardingFullDialogFragment.TAG);
}
}

View File

@@ -10,6 +10,7 @@ import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
@@ -28,16 +29,22 @@ import org.briarproject.briar.android.attachment.AttachmentResult;
import org.briarproject.briar.android.attachment.AttachmentRetriever;
import org.briarproject.briar.android.contact.ContactItem;
import org.briarproject.briar.android.util.UiUtils;
import org.briarproject.briar.android.view.TextSendController.SendState;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.briarproject.briar.api.attachment.AttachmentHeader;
import org.briarproject.briar.api.autodelete.AutoDeleteManager;
import org.briarproject.briar.api.autodelete.UnexpectedTimerException;
import org.briarproject.briar.api.autodelete.event.AutoDeleteTimerMirroredEvent;
import org.briarproject.briar.api.avatar.event.AvatarUpdatedEvent;
import org.briarproject.briar.api.conversation.ConversationManager;
import org.briarproject.briar.api.identity.AuthorInfo;
import org.briarproject.briar.api.identity.AuthorManager;
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.PrivateMessageFormat;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
@@ -55,6 +62,7 @@ import androidx.lifecycle.MutableLiveData;
import static androidx.lifecycle.Transformations.map;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
@@ -62,6 +70,13 @@ import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
import static org.briarproject.briar.android.view.TextSendController.SendState.ERROR;
import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
import static org.briarproject.briar.android.view.TextSendController.SendState.UNEXPECTED_TIMER;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
import static org.briarproject.briar.api.autodelete.AutoDeleteManager.DEFAULT_TIMER_DURATION;
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_IMAGES;
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_ONLY;
@NotNullByDefault
public class ConversationViewModel extends DbViewModel
@@ -84,6 +99,8 @@ public class ConversationViewModel extends DbViewModel
private final PrivateMessageFactory privateMessageFactory;
private final AttachmentRetriever attachmentRetriever;
private final AttachmentCreator attachmentCreator;
private final AutoDeleteManager autoDeleteManager;
private final ConversationManager conversationManager;
@Nullable
private ContactId contactId = null;
@@ -92,7 +109,7 @@ public class ConversationViewModel extends DbViewModel
private final LiveData<String> contactName = map(contactItem, c ->
UiUtils.getContactDisplayName(c.getContact()));
private final LiveData<GroupId> messagingGroupId;
private final MutableLiveData<Boolean> imageSupport =
private final MutableLiveData<PrivateMessageFormat> privateMessageFormat =
new MutableLiveData<>();
private final MutableLiveEvent<Boolean> showImageOnboarding =
new MutableLiveEvent<>();
@@ -100,8 +117,10 @@ public class ConversationViewModel extends DbViewModel
new MutableLiveEvent<>();
private final MutableLiveData<Boolean> showIntroductionAction =
new MutableLiveData<>();
private final MutableLiveData<Boolean> contactDeleted =
private final MutableLiveData<Long> autoDeleteTimer =
new MutableLiveData<>();
private final MutableLiveData<Boolean> contactDeleted =
new MutableLiveData<>(false);
private final MutableLiveEvent<PrivateMessageHeader> addedHeader =
new MutableLiveEvent<>();
@@ -118,7 +137,9 @@ public class ConversationViewModel extends DbViewModel
SettingsManager settingsManager,
PrivateMessageFactory privateMessageFactory,
AttachmentRetriever attachmentRetriever,
AttachmentCreator attachmentCreator) {
AttachmentCreator attachmentCreator,
AutoDeleteManager autoDeleteManager,
ConversationManager conversationManager) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.db = db;
this.eventBus = eventBus;
@@ -129,10 +150,10 @@ public class ConversationViewModel extends DbViewModel
this.privateMessageFactory = privateMessageFactory;
this.attachmentRetriever = attachmentRetriever;
this.attachmentCreator = attachmentCreator;
this.autoDeleteManager = autoDeleteManager;
this.conversationManager = conversationManager;
messagingGroupId = map(contactItem, c ->
messagingManager.getContactGroup(c.getContact()).getId());
contactDeleted.setValue(false);
eventBus.addListener(this);
}
@@ -152,6 +173,11 @@ public class ConversationViewModel extends DbViewModel
runOnDbThread(() -> attachmentRetriever
.loadAttachmentItem(a.getMessageId()));
}
} else if (e instanceof AutoDeleteTimerMirroredEvent) {
AutoDeleteTimerMirroredEvent a = (AutoDeleteTimerMirroredEvent) e;
if (a.getContactId().equals(contactId)) {
autoDeleteTimer.setValue(a.getNewTimer());
}
} else if (e instanceof AvatarUpdatedEvent) {
AvatarUpdatedEvent a = (AvatarUpdatedEvent) e;
if (a.getContactId().equals(contactId)) {
@@ -201,6 +227,11 @@ public class ConversationViewModel extends DbViewModel
contactItem.postValue(new ContactItem(c, authorInfo));
logDuration(LOG, "Loading contact", start);
start = now();
long timer = db.transactionWithResult(true, txn ->
autoDeleteManager.getAutoDeleteTimer(txn, contactId));
autoDeleteTimer.postValue(timer);
logDuration(LOG, "Getting auto-delete timer", start);
start = now();
checkFeaturesAndOnboarding(contactId);
logDuration(LOG, "Checking for image support", start);
} catch (NoSuchContactException e) {
@@ -215,7 +246,7 @@ public class ConversationViewModel extends DbViewModel
runOnDbThread(() -> {
try {
long start = now();
messagingManager.setReadFlag(g, m, true);
conversationManager.setReadFlag(g, m, true);
logDuration(LOG, "Marking read", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
@@ -235,20 +266,6 @@ public class ConversationViewModel extends DbViewModel
});
}
@UiThread
void sendMessage(@Nullable String text,
List<AttachmentHeader> headers, long timestamp) {
// messagingGroupId is loaded with the contact
observeForeverOnce(messagingGroupId, groupId -> {
requireNonNull(groupId);
observeForeverOnce(imageSupport, hasImageSupport -> {
requireNonNull(hasImageSupport);
createMessage(groupId, text, headers, timestamp,
hasImageSupport);
});
});
}
@Override
@UiThread
public LiveData<AttachmentResult> storeAttachments(Collection<Uri> uris,
@@ -275,10 +292,12 @@ public class ConversationViewModel extends DbViewModel
@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 images and auto-deletion are supported
PrivateMessageFormat format = db.transactionWithResult(true, txn ->
messagingManager.getContactMessageFormat(txn, c));
if (LOG.isLoggable(INFO))
LOG.info("PrivateMessageFormat loaded: " + format.name());
privateMessageFormat.postValue(format);
// check if introductions are supported
Collection<Contact> contacts = contactManager.getContacts();
@@ -287,7 +306,7 @@ public class ConversationViewModel extends DbViewModel
// we only show one onboarding dialog at a time
Settings settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
if (imagesSupported &&
if (format != TEXT_ONLY &&
settings.getBoolean(SHOW_ONBOARDING_IMAGE, true)) {
onOnboardingShown(SHOW_ONBOARDING_IMAGE);
showImageOnboarding.postEvent(true);
@@ -306,39 +325,82 @@ public class ConversationViewModel extends DbViewModel
}
@UiThread
private void createMessage(GroupId groupId, @Nullable String text,
List<AttachmentHeader> headers, long timestamp,
boolean hasImageSupport) {
LiveData<SendState> sendMessage(@Nullable String text,
List<AttachmentHeader> headers, long expectedTimer) {
MutableLiveData<SendState> liveData = new MutableLiveData<>();
runOnDbThread(() -> {
try {
db.transaction(false, txn -> {
long start = now();
PrivateMessage m = createMessage(txn, text, headers,
expectedTimer);
messagingManager.addLocalMessage(txn, m);
logDuration(LOG, "Storing message", start);
Message message = m.getMessage();
PrivateMessageHeader h = new PrivateMessageHeader(
message.getId(), message.getGroupId(),
message.getTimestamp(), true, true, false, false,
m.hasText(), m.getAttachmentHeaders(),
m.getAutoDeleteTimer());
// TODO add text to cache when available here
MessageId id = message.getId();
txn.attach(() -> {
attachmentCreator.onAttachmentsSent(id);
liveData.setValue(SENT);
addedHeader.setEvent(h);
});
});
} catch (UnexpectedTimerException e) {
liveData.postValue(UNEXPECTED_TIMER);
} catch (DbException e) {
logException(LOG, WARNING, e);
liveData.postValue(ERROR);
}
});
return liveData;
}
private PrivateMessage createMessage(Transaction txn, @Nullable String text,
List<AttachmentHeader> headers, long expectedTimer)
throws DbException {
// Sending is only possible (setReady(true)) after loading all messages
// which happens after the contact has been loaded.
// privateMessageFormat is loaded together with contact
Contact contact = requireNonNull(contactItem.getValue()).getContact();
GroupId groupId = messagingManager.getContactGroup(contact).getId();
PrivateMessageFormat format =
requireNonNull(privateMessageFormat.getValue());
long timestamp = conversationManager
.getTimestampForOutgoingMessage(txn, requireNonNull(contactId));
try {
PrivateMessage pm;
if (hasImageSupport) {
pm = privateMessageFactory.createPrivateMessage(groupId,
if (format == TEXT_ONLY) {
return privateMessageFactory.createLegacyPrivateMessage(
groupId, timestamp, requireNonNull(text));
} else if (format == TEXT_IMAGES) {
return privateMessageFactory.createPrivateMessage(groupId,
timestamp, text, headers);
} else {
pm = privateMessageFactory.createLegacyPrivateMessage(
groupId, timestamp, requireNonNull(text));
long timer = autoDeleteManager
.getAutoDeleteTimer(txn, contactId, timestamp);
if (timer != expectedTimer)
throw new UnexpectedTimerException();
return privateMessageFactory.createPrivateMessage(groupId,
timestamp, text, headers, timer);
}
storeMessage(pm);
} catch (FormatException e) {
throw new AssertionError(e);
}
}
@UiThread
private void storeMessage(PrivateMessage m) {
attachmentCreator.onAttachmentsSent(m.getMessage().getId());
void setAutoDeleteTimerEnabled(boolean enabled) {
long timer = enabled ? DEFAULT_TIMER_DURATION : NO_AUTO_DELETE_TIMER;
// ContactId is set before menu gets inflated and UI interaction
final ContactId c = requireNonNull(contactId);
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, true, false, false,
m.hasText(), m.getAttachmentHeaders());
// TODO add text to cache when available here
addedHeader.postEvent(h);
db.transaction(false, txn ->
autoDeleteManager.setAutoDeleteTimer(txn, c, timer));
autoDeleteTimer.postValue(timer);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
@@ -357,8 +419,8 @@ public class ConversationViewModel extends DbViewModel
return contactName;
}
LiveData<Boolean> hasImageSupport() {
return imageSupport;
LiveData<PrivateMessageFormat> getPrivateMessageFormat() {
return privateMessageFormat;
}
LiveEvent<Boolean> showImageOnboarding() {
@@ -373,6 +435,10 @@ public class ConversationViewModel extends DbViewModel
return showIntroductionAction;
}
LiveData<Long> getAutoDeleteTimer() {
return autoDeleteTimer;
}
LiveData<Boolean> isContactDeleted() {
return contactDeleted;
}

View File

@@ -60,10 +60,12 @@ class ConversationVisitor implements
}
if (h.isLocal()) {
item = new ConversationMessageItem(
R.layout.list_item_conversation_msg_out, h, attachments);
R.layout.list_item_conversation_msg_out, h, contactName,
attachments);
} else {
item = new ConversationMessageItem(
R.layout.list_item_conversation_msg_in, h, attachments);
R.layout.list_item_conversation_msg_in, h, contactName,
attachments);
}
if (h.hasText()) {
String text = textCache.getText(h.getId());
@@ -79,13 +81,15 @@ class ConversationVisitor implements
String text = ctx.getString(R.string.blogs_sharing_invitation_sent,
r.getName(), contactName.getValue());
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r);
R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else {
String text = ctx.getString(
R.string.blogs_sharing_invitation_received,
contactName.getValue(), r.getName());
return new ConversationRequestItem(
R.layout.list_item_conversation_request, text, BLOG, r);
R.layout.list_item_conversation_request, text, contactName,
BLOG, r);
}
}
@@ -98,13 +102,18 @@ class ConversationVisitor implements
text = ctx.getString(
R.string.blogs_sharing_response_accepted_sent,
contactName.getValue());
} else if (r.isAutoDecline()) {
text = ctx.getString(
R.string.blogs_sharing_response_declined_auto,
contactName.getValue());
} else {
text = ctx.getString(
R.string.blogs_sharing_response_declined_sent,
contactName.getValue());
}
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r);
R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else {
String text;
if (r.wasAccepted()) {
@@ -117,7 +126,8 @@ class ConversationVisitor implements
contactName.getValue());
}
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_in, text, r);
R.layout.list_item_conversation_notice_in, text,
contactName, r);
}
}
@@ -128,13 +138,15 @@ class ConversationVisitor implements
String text = ctx.getString(R.string.forum_invitation_sent,
r.getName(), contactName.getValue());
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r);
R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else {
String text = ctx.getString(
R.string.forum_invitation_received,
contactName.getValue(), r.getName());
return new ConversationRequestItem(
R.layout.list_item_conversation_request, text, FORUM, r);
R.layout.list_item_conversation_request, text, contactName,
FORUM, r);
}
}
@@ -147,13 +159,18 @@ class ConversationVisitor implements
text = ctx.getString(
R.string.forum_invitation_response_accepted_sent,
contactName.getValue());
} else if (r.isAutoDecline()) {
text = ctx.getString(
R.string.forum_invitation_response_declined_auto,
contactName.getValue());
} else {
text = ctx.getString(
R.string.forum_invitation_response_declined_sent,
contactName.getValue());
}
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r);
R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else {
String text;
if (r.wasAccepted()) {
@@ -166,7 +183,8 @@ class ConversationVisitor implements
contactName.getValue());
}
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_in, text, r);
R.layout.list_item_conversation_notice_in, text,
contactName, r);
}
}
@@ -178,13 +196,15 @@ class ConversationVisitor implements
R.string.groups_invitations_invitation_sent,
contactName.getValue(), r.getName());
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r);
R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else {
String text = ctx.getString(
R.string.groups_invitations_invitation_received,
contactName.getValue(), r.getName());
return new ConversationRequestItem(
R.layout.list_item_conversation_request, text, GROUP, r);
R.layout.list_item_conversation_request, text, contactName,
GROUP, r);
}
}
@@ -197,13 +217,18 @@ class ConversationVisitor implements
text = ctx.getString(
R.string.groups_invitations_response_accepted_sent,
contactName.getValue());
} else if (r.isAutoDecline()) {
text = ctx.getString(
R.string.groups_invitations_response_declined_auto,
contactName.getValue());
} else {
text = ctx.getString(
R.string.groups_invitations_response_declined_sent,
contactName.getValue());
}
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r);
R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else {
String text;
if (r.wasAccepted()) {
@@ -216,7 +241,8 @@ class ConversationVisitor implements
contactName.getValue());
}
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_in, text, r);
R.layout.list_item_conversation_notice_in, text,
contactName, r);
}
}
@@ -227,7 +253,8 @@ class ConversationVisitor implements
String text = ctx.getString(R.string.introduction_request_sent,
contactName.getValue(), name);
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r);
R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else {
String text;
if (r.wasAnswered()) {
@@ -243,7 +270,7 @@ class ConversationVisitor implements
contactName.getValue(), name);
}
return new ConversationRequestItem(
R.layout.list_item_conversation_request, text,
R.layout.list_item_conversation_request, text, contactName,
INTRODUCTION, r);
}
}
@@ -262,13 +289,18 @@ class ConversationVisitor implements
text = ctx.getString(
R.string.introduction_response_accepted_sent,
introducedAuthor) + suffix;
} else if (r.isAutoDecline()) {
text = ctx.getString(
R.string.introduction_response_declined_auto,
introducedAuthor);
} else {
text = ctx.getString(
R.string.introduction_response_declined_sent,
introducedAuthor);
}
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r);
R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else {
String text;
if (r.wasAccepted()) {
@@ -288,7 +320,8 @@ class ConversationVisitor implements
introducedAuthor);
}
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_in, text, r);
R.layout.list_item_conversation_notice_in, text,
contactName, r);
}
}

View File

@@ -25,6 +25,8 @@ import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModelProvider;
import de.hdodenhof.circleimageview.CircleImageView;
@@ -34,6 +36,8 @@ import static android.view.View.VISIBLE;
import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.android.view.AuthorView.setAvatar;
import static org.briarproject.briar.android.view.TextSendController.SendState;
import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_TEXT_LENGTH;
@MethodsNotNullByDefault
@@ -129,8 +133,8 @@ public class IntroductionMessageFragment extends BaseFragment
}
@Override
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
public LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> headers, long expectedAutoDeleteTimer) {
// disable button to prevent accidental double invitations
ui.message.setReady(false);
@@ -141,6 +145,7 @@ public class IntroductionMessageFragment extends BaseFragment
FragmentActivity activity = requireActivity();
activity.setResult(RESULT_OK);
activity.supportFinishAfterTransition();
return new MutableLiveData<>(SENT);
}
private static class ViewHolder {

View File

@@ -165,10 +165,9 @@ class IntroductionViewModel extends ContactsViewModel {
runOnDbThread(() -> {
// actually make the introduction
try {
long timestamp = System.currentTimeMillis();
introductionManager.makeIntroduction(
info.getContact1().getContact(),
info.getContact2().getContact(), text, timestamp);
info.getContact2().getContact(), text);
} catch (DbException e) {
logException(LOG, WARNING, e);
androidExecutor.runOnUiThread(() -> Toast.makeText(

View File

@@ -7,6 +7,8 @@ import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
@@ -15,6 +17,8 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.autodelete.AutoDeleteManager;
import org.briarproject.briar.api.conversation.ConversationManager;
import org.briarproject.briar.api.identity.AuthorManager;
import org.briarproject.briar.api.privategroup.GroupMessage;
import org.briarproject.briar.api.privategroup.GroupMessageFactory;
@@ -36,6 +40,8 @@ import javax.inject.Inject;
import androidx.annotation.Nullable;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.bramble.util.LogUtils.logException;
@Immutable
@@ -44,9 +50,12 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
implements CreateGroupController {
private static final Logger LOG =
Logger.getLogger(CreateGroupControllerImpl.class.getName());
getLogger(CreateGroupControllerImpl.class.getName());
private final Executor cryptoExecutor;
private final TransactionManager db;
private final AutoDeleteManager autoDeleteManager;
private final ConversationManager conversationManager;
private final ContactManager contactManager;
private final IdentityManager identityManager;
private final PrivateGroupFactory groupFactory;
@@ -57,17 +66,27 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
private final Clock clock;
@Inject
CreateGroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
CreateGroupControllerImpl(
@DatabaseExecutor Executor dbExecutor,
@CryptoExecutor Executor cryptoExecutor,
LifecycleManager lifecycleManager, ContactManager contactManager,
AuthorManager authorManager, IdentityManager identityManager,
TransactionManager db,
AutoDeleteManager autoDeleteManager,
ConversationManager conversationManager,
LifecycleManager lifecycleManager,
ContactManager contactManager,
AuthorManager authorManager,
IdentityManager identityManager,
PrivateGroupFactory groupFactory,
GroupMessageFactory groupMessageFactory,
PrivateGroupManager groupManager,
GroupInvitationFactory groupInvitationFactory,
GroupInvitationManager groupInvitationManager, Clock clock) {
GroupInvitationManager groupInvitationManager,
Clock clock) {
super(dbExecutor, lifecycleManager, contactManager, authorManager);
this.cryptoExecutor = cryptoExecutor;
this.db = db;
this.autoDeleteManager = autoDeleteManager;
this.conversationManager = conversationManager;
this.contactManager = contactManager;
this.identityManager = identityManager;
this.groupFactory = groupFactory;
@@ -131,16 +150,14 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
ResultExceptionHandler<Void, DbException> handler) {
runOnDbThread(() -> {
try {
LocalAuthor localAuthor = identityManager.getLocalAuthor();
List<Contact> contacts = new ArrayList<>();
for (ContactId c : contactIds) {
try {
contacts.add(contactManager.getContact(c));
} catch (NoSuchContactException e) {
// Continue
}
}
signInvitations(g, localAuthor, contacts, text, handler);
db.transaction(false, txn -> {
LocalAuthor localAuthor =
identityManager.getLocalAuthor(txn);
List<InvitationContext> contexts =
createInvitationContexts(txn, contactIds);
txn.attach(() -> signInvitations(g, localAuthor, contexts,
text, handler));
});
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
@@ -148,17 +165,32 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
});
}
private List<InvitationContext> createInvitationContexts(Transaction txn,
Collection<ContactId> contactIds) throws DbException {
List<InvitationContext> contexts = new ArrayList<>();
for (ContactId c : contactIds) {
try {
Contact contact = contactManager.getContact(txn, c);
long timestamp = conversationManager
.getTimestampForOutgoingMessage(txn, c);
long timer = autoDeleteManager.getAutoDeleteTimer(txn, c,
timestamp);
contexts.add(new InvitationContext(contact, timestamp, timer));
} catch (NoSuchContactException e) {
// Continue
}
}
return contexts;
}
private void signInvitations(GroupId g, LocalAuthor localAuthor,
Collection<Contact> contacts, @Nullable String text,
List<InvitationContext> contexts, @Nullable String text,
ResultExceptionHandler<Void, DbException> handler) {
cryptoExecutor.execute(() -> {
long timestamp = clock.currentTimeMillis();
List<InvitationContext> contexts = new ArrayList<>();
for (Contact c : contacts) {
byte[] signature = groupInvitationFactory.signInvitation(c, g,
timestamp, localAuthor.getPrivateKey());
contexts.add(new InvitationContext(c.getId(), timestamp,
signature));
for (InvitationContext ctx : contexts) {
ctx.signature = groupInvitationFactory.signInvitation(
ctx.contact, g, ctx.timestamp,
localAuthor.getPrivateKey());
}
sendInvitations(g, contexts, text, handler);
});
@@ -169,11 +201,12 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
ResultExceptionHandler<Void, DbException> handler) {
runOnDbThread(() -> {
try {
for (InvitationContext context : contexts) {
for (InvitationContext ctx : contexts) {
try {
groupInvitationManager.sendInvitation(g,
context.contactId, text, context.timestamp,
context.signature);
ctx.contact.getId(), text, ctx.timestamp,
requireNonNull(ctx.signature),
ctx.autoDeleteTimer);
} catch (NoSuchContactException e) {
// Continue
}
@@ -188,15 +221,16 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
private static class InvitationContext {
private final ContactId contactId;
private final long timestamp;
private final byte[] signature;
private final Contact contact;
private final long timestamp, autoDeleteTimer;
@Nullable
private byte[] signature = null;
private InvitationContext(ContactId contactId, long timestamp,
byte[] signature) {
this.contactId = contactId;
private InvitationContext(Contact contact, long timestamp,
long autoDeleteTimer) {
this.contact = contact;
this.timestamp = timestamp;
this.signature = signature;
this.autoDeleteTimer = autoDeleteTimer;
}
}
}

View File

@@ -15,6 +15,7 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.LargeTextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.android.view.TextSendController.SendState;
import org.briarproject.briar.api.attachment.AttachmentHeader;
import java.util.List;
@@ -22,6 +23,10 @@ import java.util.List;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -79,13 +84,14 @@ public abstract class BaseMessageFragment extends BaseFragment
}
@Override
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
public LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> headers, long expectedAutoDeleteTimer) {
// disable button to prevent accidental double actions
sendController.setReady(false);
message.hideSoftKeyboard();
listener.onButtonClick(text);
return new MutableLiveData<>(SENT);
}
@UiThread

View File

@@ -10,11 +10,9 @@ import org.briarproject.bramble.api.db.NoSuchGroupException;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.api.blog.BlogSharingManager;
import org.briarproject.briar.api.conversation.ConversationManager;
import org.briarproject.briar.api.identity.AuthorManager;
import java.util.Collection;
@@ -26,6 +24,7 @@ import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@Immutable
@@ -34,22 +33,17 @@ class ShareBlogControllerImpl extends ContactSelectorControllerImpl
implements ShareBlogController {
private final static Logger LOG =
Logger.getLogger(ShareBlogControllerImpl.class.getName());
getLogger(ShareBlogControllerImpl.class.getName());
private final ConversationManager conversationManager;
private final BlogSharingManager blogSharingManager;
private final Clock clock;
@Inject
ShareBlogControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, ContactManager contactManager,
AuthorManager authorManager,
ConversationManager conversationManager,
BlogSharingManager blogSharingManager, Clock clock) {
BlogSharingManager blogSharingManager) {
super(dbExecutor, lifecycleManager, contactManager, authorManager);
this.conversationManager = conversationManager;
this.blogSharingManager = blogSharingManager;
this.clock = clock;
}
@Override
@@ -64,10 +58,7 @@ class ShareBlogControllerImpl extends ContactSelectorControllerImpl
try {
for (ContactId c : contacts) {
try {
long time = Math.max(clock.currentTimeMillis(),
conversationManager.getGroupCount(c)
.getLatestMsgTime() + 1);
blogSharingManager.sendInvitation(g, c, text, time);
blogSharingManager.sendInvitation(g, c, text);
} catch (NoSuchContactException | NoSuchGroupException e) {
logException(LOG, WARNING, e);
}

View File

@@ -10,10 +10,8 @@ import org.briarproject.bramble.api.db.NoSuchGroupException;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.api.conversation.ConversationManager;
import org.briarproject.briar.api.forum.ForumSharingManager;
import org.briarproject.briar.api.identity.AuthorManager;
@@ -26,6 +24,7 @@ import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@Immutable
@@ -34,22 +33,17 @@ class ShareForumControllerImpl extends ContactSelectorControllerImpl
implements ShareForumController {
private final static Logger LOG =
Logger.getLogger(ShareForumControllerImpl.class.getName());
getLogger(ShareForumControllerImpl.class.getName());
private final ConversationManager conversationManager;
private final ForumSharingManager forumSharingManager;
private final Clock clock;
@Inject
ShareForumControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, ContactManager contactManager,
AuthorManager authorManager,
ConversationManager conversationManager,
ForumSharingManager forumSharingManager, Clock clock) {
ForumSharingManager forumSharingManager) {
super(dbExecutor, lifecycleManager, contactManager, authorManager);
this.conversationManager = conversationManager;
this.forumSharingManager = forumSharingManager;
this.clock = clock;
}
@Override
@@ -64,10 +58,7 @@ class ShareForumControllerImpl extends ContactSelectorControllerImpl
try {
for (ContactId c : contacts) {
try {
long time = Math.max(clock.currentTimeMillis(),
conversationManager.getGroupCount(c)
.getLatestMsgTime() + 1);
forumSharingManager.sendInvitation(g, c, text, time);
forumSharingManager.sendInvitation(g, c, text);
} catch (NoSuchContactException | NoSuchGroupException e) {
logException(LOG, WARNING, e);
}

View File

@@ -19,6 +19,7 @@ import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.android.view.TextSendController.SendState;
import org.briarproject.briar.android.view.UnreadMessageButton;
import org.briarproject.briar.api.attachment.AttachmentHeader;
@@ -29,10 +30,13 @@ import javax.annotation.Nullable;
import androidx.annotation.CallSuper;
import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.recyclerview.widget.LinearLayoutManager;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -231,8 +235,8 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
}
@Override
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
public LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> headers, long expectedAutoDeleteTimer) {
if (isNullOrEmpty(text)) throw new AssertionError();
MessageId replyId = getViewModel().getReplyId();
@@ -241,6 +245,7 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
textInput.clearText();
getViewModel().setReplyId(null);
updateTextInput();
return new MutableLiveData<>(SENT);
}
protected abstract int getMaxTextLength();

View File

@@ -6,6 +6,7 @@ import android.app.KeyguardManager;
import android.content.Context;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.PowerManager;
@@ -75,6 +76,7 @@ import static android.text.format.DateUtils.FORMAT_ABBREV_TIME;
import static android.text.format.DateUtils.FORMAT_SHOW_DATE;
import static android.text.format.DateUtils.FORMAT_SHOW_TIME;
import static android.text.format.DateUtils.FORMAT_SHOW_YEAR;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.WEEK_IN_MILLIS;
import static android.text.format.DateUtils.YEAR_IN_MILLIS;
@@ -166,6 +168,36 @@ public class UiUtils {
return DateUtils.formatDateTime(ctx, time, flags);
}
/**
* Returns the given duration in a human-friendly format. For example,
* "7 days" or "1 hour 3 minutes".
*/
public static String formatDuration(Context ctx, long millis) {
Resources r = ctx.getResources();
if (millis >= DAY_IN_MILLIS) {
int days = (int) (millis / DAY_IN_MILLIS);
int rest = (int) (millis % DAY_IN_MILLIS);
String dayStr =
r.getQuantityString(R.plurals.duration_days, days, days);
if (rest < HOUR_IN_MILLIS / 2) return dayStr;
else return dayStr + " " + formatDuration(ctx, rest);
} else if (millis >= HOUR_IN_MILLIS) {
int hours = (int) (millis / HOUR_IN_MILLIS);
int rest = (int) (millis % HOUR_IN_MILLIS);
String hourStr =
r.getQuantityString(R.plurals.duration_hours, hours, hours);
if (rest < MINUTE_IN_MILLIS / 2) return hourStr;
else return hourStr + " " + formatDuration(ctx, rest);
} else {
int minutes =
(int) ((millis + MINUTE_IN_MILLIS / 2) / MINUTE_IN_MILLIS);
// anything less than one minute is shown as one minute
if (minutes < 1) minutes = 1;
return r.getQuantityString(R.plurals.duration_minutes, minutes,
minutes);
}
}
public static long getDaysUntilExpiry() {
long now = System.currentTimeMillis();
return (EXPIRY_DATE - now) / DAYS.toMillis(1);

View File

@@ -5,6 +5,7 @@ import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import org.briarproject.briar.R;
@@ -19,6 +20,7 @@ import static java.util.Objects.requireNonNull;
public class CompositeSendButton extends FrameLayout {
private final AppCompatImageButton sendButton, imageButton;
private final ImageView bombBadge;
private final ProgressBar progressBar;
private boolean hasImageSupport = false;
@@ -32,6 +34,7 @@ public class CompositeSendButton extends FrameLayout {
sendButton = findViewById(R.id.sendButton);
imageButton = findViewById(R.id.imageButton);
bombBadge = findViewById(R.id.bombBadge);
progressBar = findViewById(R.id.progressBar);
}
@@ -71,6 +74,10 @@ public class CompositeSendButton extends FrameLayout {
return hasImageSupport;
}
public void setBombVisible(boolean visible) {
bombBadge.setVisibility(visible ? VISIBLE : INVISIBLE);
}
public void showImageButton(boolean showImageButton, boolean sendEnabled) {
if (showImageButton) {
imageButton.setVisibility(VISIBLE);

View File

@@ -26,7 +26,6 @@ import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.appcompat.app.AlertDialog.Builder;
import androidx.customview.view.AbsSavedState;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
@@ -39,6 +38,7 @@ import static androidx.core.content.ContextCompat.getColor;
import static androidx.customview.view.AbsSavedState.EMPTY_STATE;
import static androidx.lifecycle.Lifecycle.State.DESTROYED;
import static org.briarproject.briar.android.util.UiUtils.resolveColorAttribute;
import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
@UiThread
@@ -52,7 +52,6 @@ public class TextAttachmentController extends TextSendController
private final AttachmentManager attachmentManager;
private final List<Uri> imageUris = new ArrayList<>();
private final CharSequence textHint;
private boolean loadingUris = false;
public TextAttachmentController(TextInputView v, ImagePreview imagePreview,
@@ -66,23 +65,44 @@ public class TextAttachmentController extends TextSendController
sendButton = (CompositeSendButton) compositeSendButton;
sendButton.setOnImageClickListener(view -> onImageButtonClicked());
textHint = textInput.getHint();
}
@Override
protected void updateViewState() {
textInput.setEnabled(ready && !loadingUris);
boolean sendEnabled = ready && !loadingUris &&
(!textIsEmpty || canSendEmptyText());
super.updateViewState();
if (loadingUris) {
sendButton.showProgress(true);
} else if (imageUris.isEmpty()) {
sendButton.showProgress(false);
sendButton.showImageButton(textIsEmpty, sendEnabled);
sendButton.showImageButton(textIsEmpty, isSendButtonEnabled());
} else {
sendButton.showProgress(false);
sendButton.showImageButton(false, sendEnabled);
sendButton.showImageButton(false, isSendButtonEnabled());
}
}
@Override
protected boolean isTextInputEnabled() {
return super.isTextInputEnabled() && !loadingUris;
}
@Override
protected boolean isSendButtonEnabled() {
return super.isSendButtonEnabled() && !loadingUris;
}
@Override
protected boolean isBombVisible() {
return super.isBombVisible() && (!textIsEmpty || !imageUris.isEmpty());
}
@Override
protected CharSequence getCurrentTextHint() {
if (imageUris.isEmpty()) {
return super.getCurrentTextHint();
} else {
Context ctx = textInput.getContext();
return ctx.getString(R.string.image_caption_hint);
}
}
@@ -91,11 +111,17 @@ public class TextAttachmentController extends TextSendController
if (canSend()) {
if (loadingUris) throw new AssertionError();
listener.onSendClick(textInput.getText(),
attachmentManager.getAttachmentHeadersForSending());
reset();
attachmentManager.getAttachmentHeadersForSending(),
expectedTimer).observe(listener, this::onSendStateChanged);
}
}
@Override
protected void onSendStateChanged(SendState sendState) {
super.onSendStateChanged(sendState);
if (sendState == SENT) reset();
}
@Override
protected boolean canSendEmptyText() {
return !imageUris.isEmpty();
@@ -154,6 +180,7 @@ public class TextAttachmentController extends TextSendController
private void onNewUris(boolean restart, List<Uri> newUris) {
if (newUris.isEmpty()) return;
if (loadingUris) throw new AssertionError();
if (textIsEmpty) onStartingMessage();
loadingUris = true;
if (newUris.size() > MAX_ATTACHMENTS_PER_MESSAGE) {
newUris = newUris.subList(0, MAX_ATTACHMENTS_PER_MESSAGE);
@@ -161,7 +188,6 @@ public class TextAttachmentController extends TextSendController
}
imageUris.addAll(newUris);
updateViewState();
textInput.setHint(R.string.image_caption_hint);
List<ImagePreviewItem> items = ImagePreviewItem.fromUris(imageUris);
imagePreview.showPreview(items);
// store attachments and show preview when successful
@@ -207,8 +233,6 @@ public class TextAttachmentController extends TextSendController
}
private void reset() {
// restore hint
textInput.setHint(textHint);
// hide image layout
imagePreview.setVisibility(GONE);
// reset image URIs
@@ -303,7 +327,7 @@ public class TextAttachmentController extends TextSendController
}
@UiThread
public interface AttachmentListener extends SendListener, LifecycleOwner {
public interface AttachmentListener extends SendListener {
void onAttachImage(Intent intent);

View File

@@ -1,7 +1,9 @@
package org.briarproject.briar.android.view;
import android.content.Context;
import android.os.Parcelable;
import android.view.View;
import android.widget.Toast;
import com.google.android.material.snackbar.Snackbar;
@@ -12,11 +14,20 @@ import org.briarproject.briar.api.attachment.AttachmentHeader;
import java.util.List;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import static android.widget.Toast.LENGTH_LONG;
import static com.google.android.material.snackbar.Snackbar.LENGTH_SHORT;
import static java.util.Collections.emptyList;
import static org.briarproject.briar.android.view.TextSendController.SendState.ERROR;
import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
import static org.briarproject.briar.android.view.TextSendController.SendState.UNEXPECTED_TIMER;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
@UiThread
@NotNullByDefault
@@ -26,8 +37,12 @@ public class TextSendController implements TextInputListener {
protected final View compositeSendButton;
protected final SendListener listener;
protected boolean ready = true, textIsEmpty = true;
protected boolean textIsEmpty = true;
private boolean ready = true;
private long currentTimer = NO_AUTO_DELETE_TIMER;
protected long expectedTimer = NO_AUTO_DELETE_TIMER;
private final CharSequence defaultHint;
private final boolean allowEmptyText;
public TextSendController(TextInputView v, SendListener listener,
@@ -36,31 +51,91 @@ public class TextSendController implements TextInputListener {
this.compositeSendButton.setOnClickListener(view -> onSendEvent());
this.listener = listener;
this.textInput = v.getEmojiTextInputView();
this.defaultHint = textInput.getHint();
this.allowEmptyText = allowEmptyText;
}
@Override
public void onTextIsEmptyChanged(boolean isEmpty) {
textIsEmpty = isEmpty;
if (!isEmpty) onStartingMessage();
updateViewState();
}
@Override
public void onSendEvent() {
if (canSend()) {
listener.onSendClick(textInput.getText(), emptyList());
listener.onSendClick(textInput.getText(), emptyList(),
expectedTimer).observe(listener, this::onSendStateChanged);
}
}
@CallSuper
protected void onSendStateChanged(SendState sendState) {
if (sendState == SENT) {
textInput.clearText();
} else if (sendState == UNEXPECTED_TIMER) {
boolean enabled = expectedTimer == NO_AUTO_DELETE_TIMER;
showTimerChangedDialog(enabled);
} else if (sendState == ERROR) {
Toast.makeText(textInput.getContext(), R.string.message_error,
LENGTH_LONG).show();
}
}
/**
* Call whenever the user starts a new message,
* either by entering text or adding an attachment.
* This updates the expected auto-delete timer to the current value.
*/
protected void onStartingMessage() {
expectedTimer = currentTimer;
}
public void setReady(boolean ready) {
this.ready = ready;
updateViewState();
}
/**
* Sets the current auto delete timer and updates the UI accordingly.
*/
public void setAutoDeleteTimer(long timer) {
currentTimer = timer;
updateViewState();
}
@CallSuper
protected void updateViewState() {
textInput.setEnabled(ready);
compositeSendButton
.setEnabled(ready && (!textIsEmpty || canSendEmptyText()));
textInput.setEnabled(isTextInputEnabled());
textInput.setHint(getCurrentTextHint());
compositeSendButton.setEnabled(isSendButtonEnabled());
if (compositeSendButton instanceof CompositeSendButton) {
CompositeSendButton sendButton =
(CompositeSendButton) compositeSendButton;
sendButton.setBombVisible(isBombVisible());
}
}
protected boolean isTextInputEnabled() {
return ready;
}
protected boolean isSendButtonEnabled() {
return ready && (!textIsEmpty || canSendEmptyText());
}
protected boolean isBombVisible() {
return currentTimer != NO_AUTO_DELETE_TIMER;
}
protected CharSequence getCurrentTextHint() {
if (currentTimer == NO_AUTO_DELETE_TIMER) {
return defaultHint;
} else {
Context ctx = textInput.getContext();
return ctx.getString(R.string.message_hint_auto_delete);
}
}
protected final boolean canSend() {
@@ -76,6 +151,23 @@ public class TextSendController implements TextInputListener {
return allowEmptyText;
}
private void showTimerChangedDialog(boolean enabled) {
Context ctx = textInput.getContext();
int message =
enabled ? R.string.auto_delete_changed_warning_message_enabled :
R.string.auto_delete_changed_warning_message_disabled;
new AlertDialog.Builder(ctx, R.style.BriarDialogTheme)
.setTitle(R.string.auto_delete_changed_warning_title)
.setMessage(message)
.setPositiveButton(R.string.auto_delete_changed_warning_send,
(dialog, which) -> {
expectedTimer = currentTimer;
onSendEvent();
})
.setNegativeButton(R.string.cancel, null)
.show();
}
@Nullable
public Parcelable onSaveInstanceState(@Nullable Parcelable superState) {
return superState;
@@ -86,9 +178,11 @@ public class TextSendController implements TextInputListener {
return state;
}
@UiThread
public interface SendListener {
void onSendClick(@Nullable String text, List<AttachmentHeader> headers);
public enum SendState {SENT, ERROR, UNEXPECTED_TIMER}
public interface SendListener extends LifecycleOwner {
LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> headers, long expectedAutoDeleteTimer);
}
}

View File

@@ -0,0 +1,66 @@
package org.briarproject.briar.android.widget;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.DialogFragment;
@NotNullByDefault
public class OnboardingFullDialogFragment extends DialogFragment {
public final static String TAG =
OnboardingFullDialogFragment.class.getName();
private final static String RES_TITLE = "resTitle";
private final static String RES_CONTENT = "resContent";
public static OnboardingFullDialogFragment newInstance(@StringRes int title,
@StringRes int content) {
Bundle args = new Bundle();
args.putInt(RES_TITLE, title);
args.putInt(RES_CONTENT, content);
OnboardingFullDialogFragment f = new OnboardingFullDialogFragment();
f.setArguments(args);
return f;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NORMAL,
R.style.BriarFullScreenDialogTheme);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_onboarding_full,
container, false);
Bundle args = requireArguments();
Toolbar toolbar = view.findViewById(R.id.toolbar);
toolbar.setNavigationOnClickListener(v -> dismiss());
toolbar.setTitle(args.getInt(RES_TITLE));
TextView contentView = view.findViewById(R.id.contentView);
contentView.setText(args.getInt(RES_CONTENT));
view.findViewById(R.id.button).setOnClickListener(v -> dismiss());
return view;
}
}