diff --git a/briar-android/build.gradle b/briar-android/build.gradle index 2d8bc6855..fed7d7253 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..c70a13c8e 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 @@ -1,5 +1,7 @@ package org.briarproject.briar.android; +import android.arch.lifecycle.ViewModelProvider; + import org.briarproject.bramble.BrambleAndroidModule; import org.briarproject.bramble.BrambleCoreEagerSingletons; import org.briarproject.bramble.BrambleCoreModule; @@ -154,6 +156,8 @@ public interface AndroidComponent CircumventionProvider circumventionProvider(); + ViewModelProvider.Factory viewModelFactory(); + void inject(SignInReminderReceiver briarService); void inject(BriarService briarService); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java index bbfb919ce..c993eacaa 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java @@ -32,6 +32,7 @@ import org.briarproject.bramble.plugin.tor.CircumventionProvider; import org.briarproject.bramble.util.AndroidUtils; import org.briarproject.bramble.util.StringUtils; import org.briarproject.briar.android.account.LockManagerImpl; +import org.briarproject.briar.android.viewmodel.ViewModelModule; import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.DozeWatchdog; import org.briarproject.briar.api.android.LockManager; @@ -57,7 +58,7 @@ import static java.util.Collections.emptyList; import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_ONION_ADDRESS; import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_PUBLIC_KEY_HEX; -@Module +@Module(includes = ViewModelModule.class) public class AppModule { static class EagerSingletons { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java index c327d64ae..00feafdaf 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java @@ -15,6 +15,7 @@ import org.briarproject.briar.android.blog.ReblogFragment; import org.briarproject.briar.android.blog.RssFeedImportActivity; import org.briarproject.briar.android.blog.RssFeedManageActivity; import org.briarproject.briar.android.blog.WriteBlogPostActivity; +import org.briarproject.briar.android.contact.AliasDialogFragment; import org.briarproject.briar.android.contact.ContactListFragment; import org.briarproject.briar.android.contact.ContactModule; import org.briarproject.briar.android.contact.ConversationActivity; @@ -211,4 +212,7 @@ public interface ActivityComponent { void inject(ScreenFilterDialogFragment fragment); void inject(ContactExchangeErrorFragment fragment); + + void inject(AliasDialogFragment aliasDialogFragment); + } 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..e4e0f644d --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/AliasDialogFragment.java @@ -0,0 +1,72 @@ +package org.briarproject.briar.android.contact; + +import android.arch.lifecycle.ViewModelProvider; +import android.arch.lifecycle.ViewModelProviders; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.app.AppCompatDialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; + +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.BriarActivity; + +import javax.inject.Inject; + +import static java.util.Objects.requireNonNull; + +public class AliasDialogFragment extends AppCompatDialogFragment { + + final static String TAG = AliasDialogFragment.class.getName(); + + @Inject + ViewModelProvider.Factory viewModelFactory; + + private ConversationViewModel viewModel; + private EditText aliasEditText; + + public static AliasDialogFragment newInstance() { + return new AliasDialogFragment(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setStyle(STYLE_NO_TITLE, R.style.BriarDialogTheme); + + if (getActivity() == null) return; + ((BriarActivity) getActivity()).getActivityComponent().inject(this); + viewModel = ViewModelProviders.of(getActivity(), viewModelFactory) + .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); + Contact contact = requireNonNull(viewModel.getContact().getValue()); + String alias = contact.getAlias(); + aliasEditText.setText(alias); + if (alias != null) aliasEditText.setSelection(alias.length()); + + Button setButton = v.findViewById(R.id.setButton); + setButton.setOnClickListener(v1 -> { + viewModel.setContactAlias(aliasEditText.getText().toString()); + getDialog().dismiss(); + }); + + Button cancelButton = v.findViewById(R.id.cancelButton); + cancelButton.setOnClickListener(v1 -> getDialog().cancel()); + + return v; + } +} 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..9f2a80bfe 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.Observer; +import android.arch.lifecycle.ViewModelProvider; +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; @@ -96,6 +95,7 @@ import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.PromptSt import static android.support.v4.view.ViewCompat.setTransitionName; import static android.support.v7.util.SortedList.INVALID_POSITION; import static android.widget.Toast.LENGTH_SHORT; +import static java.util.Objects.requireNonNull; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import static org.briarproject.bramble.util.LogUtils.logDuration; @@ -105,6 +105,7 @@ import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRO import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE; import static org.briarproject.briar.android.util.UiUtils.getAvatarTransitionName; import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName; +import static org.briarproject.briar.android.util.UiUtils.observeOnce; import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH; import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_DISMISSED; import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_FINISHED; @@ -131,8 +132,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; @@ -163,14 +164,18 @@ public class ConversationActivity extends BriarActivity volatile BlogSharingManager blogSharingManager; @Inject volatile GroupInvitationManager groupInvitationManager; + @Inject + ViewModelProvider.Factory viewModelFactory; private volatile ContactId contactId; @Nullable - private volatile AuthorId contactAuthorId; - @Nullable private volatile GroupId messagingGroupId; - @SuppressWarnings("ConstantConditions") + private final Observer contactNameObserver = name -> { + requireNonNull(name); + loadMessages(); + }; + @Override public void onCreate(@Nullable Bundle state) { setSceneTransitionAnimation(); @@ -181,20 +186,37 @@ public class ConversationActivity extends BriarActivity if (id == -1) throw new IllegalStateException(); contactId = new ContactId(id); + viewModel = ViewModelProviders.of(this, viewModelFactory) + .get(ConversationViewModel.class); + viewModel.setContactId(contactId); + setContentView(R.layout.activity_conversation); // 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); - } + toolbar = requireNonNull(setUpCustomToolbar(true)); + toolbarAvatar = toolbar.findViewById(R.id.contactAvatar); + toolbarStatus = toolbar.findViewById(R.id.contactStatus); + toolbarTitle = toolbar.findViewById(R.id.contactName); + + observeOnce(viewModel.getContactAuthorId(), this, authorId -> { + requireNonNull(authorId); + toolbarAvatar.setImageDrawable( + new IdenticonDrawable(authorId.getBytes())); + }); + viewModel.getContactDisplayName().observe(this, contactName -> { + requireNonNull(contactName); + toolbarTitle.setText(contactName); + }); + viewModel.isContactDeleted().observe(this, deleted -> { + requireNonNull(deleted); + if (deleted) finish(); + }); 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 +251,7 @@ public class ConversationActivity extends BriarActivity notificationManager.blockContactNotification(contactId); notificationManager.clearContactNotification(contactId); displayContactOnlineStatus(); - loadContactDetailsAndMessages(); + viewModel.getContactDisplayName().observe(this, contactNameObserver); list.startPeriodicUpdate(); } @@ -238,6 +260,7 @@ public class ConversationActivity extends BriarActivity super.onStop(); eventBus.removeListener(this); notificationManager.unblockContactNotification(contactId); + viewModel.getContactDisplayName().removeObserver(contactNameObserver); list.stopPeriodicUpdate(); } @@ -249,6 +272,8 @@ public class ConversationActivity extends BriarActivity enableIntroductionActionIfAvailable( menu.findItem(R.id.action_introduction)); + enableAliasActionIfAvailable( + menu.findItem(R.id.action_set_alias)); return super.onCreateOptionsMenu(menu); } @@ -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().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,21 +452,9 @@ public class ConversationActivity extends BriarActivity private void onNewPrivateMessage(PrivateMessageHeader h) { runOnUiThreadUnlessDestroyed(() -> { if (h instanceof PrivateRequest || h instanceof PrivateResponse) { - String cName = contactName.getValue(); - if (cName == null) { - // Wait for the contact name to be loaded - contactName.observe(this, new Observer() { - @Override - public void onChanged(@Nullable String cName) { - if (cName != null) { - addConversationItem(h.accept(visitor)); - contactName.removeObserver(this); - } - } - }); - } else { - addConversationItem(h.accept(visitor)); - } + // contact name might not have been loaded + observeOnce(viewModel.getContactDisplayName(), this, + name -> addConversationItem(h.accept(visitor))); } else { addConversationItem(h.accept(visitor)); loadMessageText(h.getId()); @@ -605,6 +592,10 @@ public class ConversationActivity extends BriarActivity }); } + private void enableAliasActionIfAvailable(MenuItem item) { + observeOnce(viewModel.getContact(), this, c -> item.setEnabled(true)); + } + private void enableIntroductionAction(MenuItem item) { runOnUiThreadUnlessDestroyed(() -> item.setEnabled(true)); } 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..95fd3a45b --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationViewModel.java @@ -0,0 +1,115 @@ +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.arch.lifecycle.Transformations; +import android.support.annotation.Nullable; + +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.bramble.api.nullsafety.NotNullByDefault; +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.Objects.requireNonNull; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.LogUtils.logDuration; +import static org.briarproject.bramble.util.LogUtils.logException; +import static org.briarproject.bramble.util.LogUtils.now; + +@NotNullByDefault +public class ConversationViewModel extends AndroidViewModel { + + private static Logger LOG = + getLogger(ConversationViewModel.class.getName()); + + @DatabaseExecutor + private final Executor dbExecutor; + private final ContactManager contactManager; + + @Nullable + private ContactId contactId = null; + private final MutableLiveData contact = new MutableLiveData<>(); + private final LiveData contactAuthorId = + Transformations.map(contact, c -> c.getAuthor().getId()); + private final LiveData contactName = + Transformations.map(contact, UiUtils::getContactDisplayName); + private final MutableLiveData contactDeleted = + new MutableLiveData<>(); + + @Inject + ConversationViewModel(Application application, + @DatabaseExecutor Executor dbExecutor, + ContactManager contactManager) { + super(application); + this.dbExecutor = dbExecutor; + this.contactManager = contactManager; + contactDeleted.setValue(false); + } + + void setContactId(ContactId contactId) { + if (this.contactId == null) { + this.contactId = contactId; + loadContact(); + } else if (!contactId.equals(this.contactId)) { + throw new IllegalStateException(); + } + } + + private void loadContact() { + dbExecutor.execute(() -> { + try { + long start = now(); + Contact c = + contactManager.getContact(requireNonNull(contactId)); + contact.postValue(c); + logDuration(LOG, "Loading contact", start); + } catch (NoSuchContactException e) { + contactDeleted.postValue(true); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + void setContactAlias(String alias) { + dbExecutor.execute(() -> { + try { + contactManager.setContactAlias(requireNonNull(contactId), + alias.isEmpty() ? null : alias); + loadContact(); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + LiveData getContact() { + return contact; + } + + LiveData getContactAuthorId() { + return contactAuthorId; + } + + LiveData getContactDisplayName() { + return contactName; + } + + LiveData isContactDeleted() { + return contactDeleted; + } + +} 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 27c24083c..5f59251f9 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 @@ -3,6 +3,9 @@ package org.briarproject.briar.android.util; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.KeyguardManager; +import android.arch.lifecycle.LifecycleOwner; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.Observer; import android.content.Context; import android.content.DialogInterface.OnClickListener; import android.content.Intent; @@ -11,6 +14,7 @@ import android.os.PowerManager; import android.support.annotation.AttrRes; import android.support.annotation.ColorInt; import android.support.annotation.ColorRes; +import android.support.annotation.MainThread; import android.support.annotation.Nullable; import android.support.design.widget.TextInputLayout; import android.support.v4.app.FragmentManager; @@ -314,4 +318,21 @@ public class UiUtils { keyEvent.getKeyCode() == KEYCODE_ENTER; } + /** + * Observes the given {@link LiveData} until the first change. + * If the LiveData's value is available, the {@link Observer} will be + * called right away. + */ + @MainThread + public static void observeOnce(LiveData liveData, + LifecycleOwner owner, Observer observer) { + liveData.observe(owner, new Observer() { + @Override + public void onChanged(@Nullable T t) { + observer.onChanged(t); + liveData.removeObserver(this); + } + }); + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/ViewModelFactory.java b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/ViewModelFactory.java new file mode 100644 index 000000000..f35670625 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/ViewModelFactory.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.briarproject.briar.android.viewmodel; + +import android.arch.lifecycle.ViewModel; +import android.arch.lifecycle.ViewModelProvider; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import java.util.Map; +import java.util.Map.Entry; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.inject.Singleton; + +@Singleton +@NotNullByDefault +class ViewModelFactory implements ViewModelProvider.Factory { + + private final Map, Provider> creators; + + @Inject + ViewModelFactory(Map, + Provider> creators) { + this.creators = creators; + } + + @Override + public T create(Class modelClass) { + Provider creator = creators.get(modelClass); + if (creator == null) { + for (Entry, Provider> entry : + creators.entrySet()) { + if (modelClass.isAssignableFrom(entry.getKey())) { + creator = entry.getValue(); + break; + } + } + } + if (creator == null) { + throw new IllegalArgumentException( + "unknown model class " + modelClass); + } + //noinspection unchecked + return (T) creator.get(); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/ViewModelKey.java b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/ViewModelKey.java new file mode 100644 index 000000000..bd4f3d650 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/ViewModelKey.java @@ -0,0 +1,19 @@ +package org.briarproject.briar.android.viewmodel; + +import android.arch.lifecycle.ViewModel; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import dagger.MapKey; + +@Documented +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@MapKey +@interface ViewModelKey { + Class value(); +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/ViewModelModule.java b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/ViewModelModule.java new file mode 100644 index 000000000..15edbed90 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/ViewModelModule.java @@ -0,0 +1,28 @@ +package org.briarproject.briar.android.viewmodel; + +import android.arch.lifecycle.ViewModel; +import android.arch.lifecycle.ViewModelProvider; + +import org.briarproject.briar.android.contact.ConversationViewModel; + +import javax.inject.Singleton; + +import dagger.Binds; +import dagger.Module; +import dagger.multibindings.IntoMap; + +@Module +public abstract class ViewModelModule { + + @Binds + @IntoMap + @ViewModelKey(ConversationViewModel.class) + abstract ViewModel bindConversationViewModel( + ConversationViewModel conversationViewModel); + + @Binds + @Singleton + abstract ViewModelProvider.Factory bindViewModelFactory( + ViewModelFactory viewModelFactory); + +} 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..d4846cf11 --- /dev/null +++ b/briar-android/src/main/res/layout/fragment_alias_dialog.xml @@ -0,0 +1,54 @@ + + + + + + + + + +