diff --git a/briar-android/build.gradle b/briar-android/build.gradle index fd2145417..0ccadfcc8 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -105,6 +105,7 @@ dependencies { implementation "com.android.support:cardview-v7:$supportVersion" implementation "com.android.support:support-annotations:$supportVersion" implementation 'com.android.support.constraint:constraint-layout:1.1.3' + implementation "android.arch.lifecycle:extensions:1.1.1" implementation('ch.acra:acra:4.11') { exclude module: 'support-v4' diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java index 6971b49a6..a3834cce0 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java @@ -26,6 +26,7 @@ import org.briarproject.bramble.api.system.LocationUtils; import org.briarproject.bramble.plugin.tor.CircumventionProvider; import org.briarproject.briar.BriarCoreEagerSingletons; import org.briarproject.briar.BriarCoreModule; +import org.briarproject.briar.android.contact.ConversationViewModel; import org.briarproject.briar.android.login.SignInReminderReceiver; import org.briarproject.briar.android.reporting.BriarReportSender; import org.briarproject.briar.android.view.TextInputView; @@ -164,6 +165,8 @@ public interface AndroidComponent void inject(TextInputView textInputView); + void inject(ConversationViewModel conversationViewModel); + // Eager singleton load void inject(AppModule.EagerSingletons init); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/AliasDialogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/AliasDialogFragment.java new file mode 100644 index 000000000..407445912 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/AliasDialogFragment.java @@ -0,0 +1,90 @@ +package org.briarproject.briar.android.contact; + +import android.arch.lifecycle.ViewModelProviders; +import android.graphics.Point; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.app.AppCompatDialogFragment; +import android.view.Display; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.Button; +import android.widget.TextView; + +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.briar.R; + +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +public class AliasDialogFragment extends AppCompatDialogFragment { + + final static String TAG = AliasDialogFragment.class.getName(); + + private ConversationViewModel viewModel; + private ContactId contactId; + private TextView aliasEditText; + + public static AliasDialogFragment newInstance(ContactId id) { + AliasDialogFragment f = new AliasDialogFragment(); + + Bundle args = new Bundle(); + args.putInt("contactId", id.getInt()); + f.setArguments(args); + + return f; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (getArguments() == null) throw new IllegalArgumentException(); + int contactIdInt = getArguments().getInt("contactId", -1); + if (contactIdInt == -1) throw new IllegalArgumentException(); + contactId = new ContactId(contactIdInt); + + setStyle(STYLE_NO_TITLE, R.style.BriarDialogTheme); + + viewModel = + ViewModelProviders.of(getActivity()).get(ConversationViewModel.class); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + + View v = inflater.inflate(R.layout.fragment_alias_dialog, container, + false); + + aliasEditText = v.findViewById(R.id.aliasEditText); + + Button setButton = v.findViewById(R.id.setButton); + setButton.setOnClickListener(v1 -> { + viewModel.setContactAlias(contactId, + aliasEditText.getText().toString()); + getDialog().dismiss(); + }); + + Button cancelButton = v.findViewById(R.id.cancelButton); + cancelButton.setOnClickListener(v1 -> getDialog().cancel()); + + return v; + } + + @Override + public void onResume() { + Window window = getDialog().getWindow(); + if (window == null) { + super.onResume(); + return; + } + Point size = new Point(); + Display display = window.getWindowManager().getDefaultDisplay(); + display.getSize(size); + window.setLayout((int) (size.x * 0.75), WRAP_CONTENT); + super.onResume(); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/BaseContactListAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/BaseContactListAdapter.java index 1f577c366..5e58597ce 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/BaseContactListAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/BaseContactListAdapter.java @@ -1,6 +1,7 @@ package org.briarproject.briar.android.contact; import android.content.Context; +import android.support.annotation.NonNull; import android.view.View; import org.briarproject.bramble.api.contact.ContactId; @@ -9,6 +10,7 @@ import org.briarproject.briar.android.util.BriarAdapter; import javax.annotation.Nullable; import static android.support.v7.util.SortedList.INVALID_POSITION; +import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName; public abstract class BaseContactListAdapter> extends BriarAdapter { @@ -23,15 +25,15 @@ public abstract class BaseContactListAdapter @@ -41,8 +43,7 @@ public class ContactItemViewHolder Author author = item.getContact().getAuthor(); avatar.setImageDrawable( new IdenticonDrawable(author.getId().getBytes())); - String contactName = author.getName(); - name.setText(contactName); + name.setText(getContactDisplayName(item.getContact())); if (bulb != null) { // online/offline diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java index 423f47ec9..3f7deeb17 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java @@ -1,7 +1,8 @@ package org.briarproject.briar.android.contact; -import android.arch.lifecycle.MutableLiveData; +import android.arch.lifecycle.LiveData; import android.arch.lifecycle.Observer; +import android.arch.lifecycle.ViewModelProviders; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; @@ -23,7 +24,6 @@ import android.widget.TextView; import android.widget.Toast; import org.briarproject.bramble.api.FormatException; -import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.event.ContactRemovedEvent; @@ -34,7 +34,6 @@ import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventListener; -import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.plugin.ConnectionRegistry; @@ -131,8 +130,8 @@ public class ConversationActivity extends BriarActivity Executor cryptoExecutor; private final Map textCache = new ConcurrentHashMap<>(); - private final MutableLiveData contactName = new MutableLiveData<>(); + private ConversationViewModel viewModel; private ConversationVisitor visitor; private ConversationAdapter adapter; private Toolbar toolbar; @@ -166,8 +165,6 @@ public class ConversationActivity extends BriarActivity private volatile ContactId contactId; @Nullable - private volatile AuthorId contactAuthorId; - @Nullable private volatile GroupId messagingGroupId; @SuppressWarnings("ConstantConditions") @@ -176,6 +173,9 @@ public class ConversationActivity extends BriarActivity setSceneTransitionAnimation(); super.onCreate(state); + viewModel = + ViewModelProviders.of(this).get(ConversationViewModel.class); + Intent i = getIntent(); int id = i.getIntExtra(CONTACT_ID, -1); if (id == -1) throw new IllegalStateException(); @@ -185,16 +185,29 @@ public class ConversationActivity extends BriarActivity // Custom Toolbar toolbar = setUpCustomToolbar(true); - if (toolbar != null) { - toolbarAvatar = toolbar.findViewById(R.id.contactAvatar); - toolbarStatus = toolbar.findViewById(R.id.contactStatus); - toolbarTitle = toolbar.findViewById(R.id.contactName); - } + toolbarAvatar = toolbar.findViewById(R.id.contactAvatar); + toolbarStatus = toolbar.findViewById(R.id.contactStatus); + toolbarTitle = toolbar.findViewById(R.id.contactName); + + viewModel.getContactAuthorId().observe(this, authorId -> { + toolbarAvatar.setImageDrawable( + new IdenticonDrawable(authorId.getBytes())); + // we only need this once + viewModel.getContactAuthorId().removeObservers(this); + }); + viewModel.getContactDisplayName().observe(this, contactName -> { + toolbarTitle.setText(contactName); + }); + viewModel.isContactDeleted().observe(this, deleted -> { + if (deleted != null && deleted) finish(); + }); + viewModel.loadContactDetails(contactId); setTransitionName(toolbarAvatar, getAvatarTransitionName(contactId)); setTransitionName(toolbarStatus, getBulbTransitionName(contactId)); - visitor = new ConversationVisitor(this, this, contactName); + visitor = new ConversationVisitor(this, this, + viewModel.getContactDisplayName()); adapter = new ConversationAdapter(this, this); list = findViewById(R.id.conversationView); list.setLayoutManager(new LinearLayoutManager(this)); @@ -229,7 +242,19 @@ public class ConversationActivity extends BriarActivity notificationManager.blockContactNotification(contactId); notificationManager.clearContactNotification(contactId); displayContactOnlineStatus(); - loadContactDetailsAndMessages(); + LiveData contactName = viewModel.getContactDisplayName(); + if (contactName.getValue() == null) { + // wait for contact name to be initialized + contactName.observe(this, new Observer() { + @Override + public void onChanged(@Nullable String cName) { + if (cName != null) { + loadMessages(); + contactName.removeObserver(this); + } + } + }); + } else loadMessages(); list.startPeriodicUpdate(); } @@ -266,6 +291,10 @@ public class ConversationActivity extends BriarActivity intent.putExtra(CONTACT_ID, contactId.getInt()); startActivityForResult(intent, REQUEST_INTRODUCTION); return true; + case R.id.action_set_alias: + AliasDialogFragment.newInstance(contactId).show( + getSupportFragmentManager(), AliasDialogFragment.TAG); + return true; case R.id.action_social_remove_person: askToRemoveContact(); return true; @@ -274,36 +303,6 @@ public class ConversationActivity extends BriarActivity } } - private void loadContactDetailsAndMessages() { - runOnDbThread(() -> { - try { - long start = now(); - if (contactAuthorId == null) { - Contact contact = contactManager.getContact(contactId); - contactName.postValue(contact.getAuthor().getName()); - contactAuthorId = contact.getAuthor().getId(); - } - logDuration(LOG, "Loading contact", start); - loadMessages(); - displayContactDetails(); - } catch (NoSuchContactException e) { - finishOnUiThread(); - } catch (DbException e) { - logException(LOG, WARNING, e); - } - }); - } - - // contactAuthorId and contactName are expected to be set - private void displayContactDetails() { - runOnUiThreadUnlessDestroyed(() -> { - //noinspection ConstantConditions - toolbarAvatar.setImageDrawable( - new IdenticonDrawable(contactAuthorId.getBytes())); - toolbarTitle.setText(contactName.getValue()); - }); - } - private void displayContactOnlineStatus() { runOnUiThreadUnlessDestroyed(() -> { if (connectionRegistry.isConnected(contactId)) { @@ -453,15 +452,17 @@ public class ConversationActivity extends BriarActivity private void onNewPrivateMessage(PrivateMessageHeader h) { runOnUiThreadUnlessDestroyed(() -> { if (h instanceof PrivateRequest || h instanceof PrivateResponse) { - String cName = contactName.getValue(); + String cName = viewModel.getContactDisplayName().getValue(); if (cName == null) { // Wait for the contact name to be loaded - contactName.observe(this, new Observer() { + viewModel.getContactDisplayName() + .observe(this, new Observer() { @Override public void onChanged(@Nullable String cName) { if (cName != null) { addConversationItem(h.accept(visitor)); - contactName.removeObserver(this); + viewModel.getContactDisplayName() + .removeObserver(this); } } }); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationViewModel.java new file mode 100644 index 000000000..1c132216e --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationViewModel.java @@ -0,0 +1,95 @@ +package org.briarproject.briar.android.contact; + +import android.app.Application; +import android.arch.lifecycle.AndroidViewModel; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.MutableLiveData; +import android.support.annotation.NonNull; + +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactId; +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.identity.AuthorId; +import org.briarproject.briar.android.AndroidComponent; +import org.briarproject.briar.android.BriarApplication; +import org.briarproject.briar.android.util.UiUtils; + +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static java.util.logging.Level.WARNING; +import static org.briarproject.bramble.util.LogUtils.logDuration; +import static org.briarproject.bramble.util.LogUtils.logException; +import static org.briarproject.bramble.util.LogUtils.now; + +public class ConversationViewModel extends AndroidViewModel { + + private static Logger LOG = + Logger.getLogger(ConversationViewModel.class.getName()); + + @Inject + @DatabaseExecutor + Executor dbExecutor; + @Inject + ContactManager contactManager; + + private final MutableLiveData contactAuthorId = + new MutableLiveData<>(); + private final MutableLiveData contactDeleted = + new MutableLiveData<>(); + private final MutableLiveData contactName = new MutableLiveData<>(); + + public ConversationViewModel(@NonNull Application application) { + super(application); + AndroidComponent component = + ((BriarApplication) application).getApplicationComponent(); + component.inject(this); + contactDeleted.setValue(false); + } + + void loadContactDetails(ContactId contactId) { + dbExecutor.execute(() -> { + try { + long start = now(); + Contact contact = contactManager.getContact(contactId); + contactAuthorId.postValue(contact.getAuthor().getId()); + contactName.postValue(UiUtils.getContactDisplayName(contact)); + logDuration(LOG, "Loading contact", start); + } catch (NoSuchContactException e) { + contactDeleted.postValue(true); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + void setContactAlias(ContactId contactId, String alias) { + dbExecutor.execute(() -> { + try { + contactManager.setContactAlias(contactId, + alias.isEmpty() ? null : alias); + loadContactDetails(contactId); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + LiveData getContactAuthorId() { + return contactAuthorId; + } + + LiveData isContactDeleted() { + return contactDeleted; + } + + LiveData getContactDisplayName() { + return contactName; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java index 194641f07..ea7858e48 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java @@ -38,6 +38,7 @@ import static android.view.View.VISIBLE; import static android.widget.Toast.LENGTH_SHORT; import static java.util.logging.Level.WARNING; import static org.briarproject.bramble.util.LogUtils.logException; +import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName; import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_TEXT_LENGTH; public class IntroductionMessageFragment extends BaseFragment @@ -148,8 +149,8 @@ public class IntroductionMessageFragment extends BaseFragment c2.getAuthor().getId().getBytes())); // set contact names - ui.contactName1.setText(c1.getAuthor().getName()); - ui.contactName2.setText(c2.getAuthor().getName()); + ui.contactName1.setText(getContactDisplayName(c1)); + ui.contactName2.setText(getContactDisplayName(c2)); // hide progress bar ui.progressBar.setVisibility(GONE); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/invitation/GroupInvitationViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/invitation/GroupInvitationViewHolder.java index fae57b85e..b205e2409 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/invitation/GroupInvitationViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/invitation/GroupInvitationViewHolder.java @@ -9,6 +9,8 @@ import org.briarproject.briar.api.privategroup.invitation.GroupInvitationItem; import javax.annotation.Nullable; +import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName; + class GroupInvitationViewHolder extends InvitationViewHolder { @@ -24,7 +26,7 @@ class GroupInvitationViewHolder sharedBy.setText( sharedBy.getContext().getString(R.string.groups_created_by, - item.getCreator().getAuthor().getName())); + getContactDisplayName(item.getCreator()))); } } \ No newline at end of file diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/reveal/RevealableContactViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/reveal/RevealableContactViewHolder.java index ab7777f4b..f32c34522 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/reveal/RevealableContactViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/reveal/RevealableContactViewHolder.java @@ -14,6 +14,7 @@ import javax.annotation.Nullable; import static org.briarproject.briar.android.privategroup.VisibilityHelper.getVisibilityIcon; import static org.briarproject.briar.android.privategroup.VisibilityHelper.getVisibilityString; import static org.briarproject.briar.android.util.UiUtils.GREY_OUT; +import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName; @UiThread @NotNullByDefault @@ -36,7 +37,7 @@ class RevealableContactViewHolder icon.setImageResource(getVisibilityIcon(item.getVisibility())); info.setText( getVisibilityString(info.getContext(), item.getVisibility(), - item.getContact().getAuthor().getName())); + getContactDisplayName(item.getContact()))); } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/sharing/SharingInvitationViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/sharing/SharingInvitationViewHolder.java index 6d4eae929..9b65e1c8c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/sharing/SharingInvitationViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/sharing/SharingInvitationViewHolder.java @@ -13,6 +13,8 @@ import java.util.Collection; import javax.annotation.Nullable; +import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName; + class SharingInvitationViewHolder extends InvitationViewHolder { @@ -28,7 +30,7 @@ class SharingInvitationViewHolder Collection names = new ArrayList<>(); for (Contact c : item.getNewSharers()) - names.add(c.getAuthor().getName()); + names.add(getContactDisplayName(c)); sharedBy.setText( sharedBy.getContext().getString(R.string.shared_by_format, StringUtils.join(names, ", "))); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java index ff18520f4..56dc4666b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java @@ -33,6 +33,7 @@ import android.view.View; import android.widget.TextView; import org.acra.ACRA; +import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; @@ -74,6 +75,13 @@ public class UiUtils { public static final int TEASER_LENGTH = 320; public static final float GREY_OUT = 0.5f; + public static String getContactDisplayName(Contact c) { + String name = c.getAuthor().getName(); + String alias = c.getAlias(); + if (alias == null) return name; + else return String.format("%s (%s)", alias, name); + } + public static void setError(TextInputLayout til, @Nullable String error, boolean set) { if (set) { diff --git a/briar-android/src/main/res/layout/fragment_alias_dialog.xml b/briar-android/src/main/res/layout/fragment_alias_dialog.xml new file mode 100644 index 000000000..70920e522 --- /dev/null +++ b/briar-android/src/main/res/layout/fragment_alias_dialog.xml @@ -0,0 +1,55 @@ + + + + + + + + + +