diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactManager.java index d9c527021..5ae366c2e 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactManager.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactManager.java @@ -1,5 +1,6 @@ package org.briarproject.bramble.api.contact; +import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.Transaction; @@ -10,12 +11,17 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import java.util.Collection; +import java.util.regex.Pattern; import javax.annotation.Nullable; @NotNullByDefault public interface ContactManager { + int LINK_LENGTH = 64; + Pattern LINK_REGEX = + Pattern.compile("(briar://)?([a-z2-7]{" + LINK_LENGTH + "})"); + /** * Registers a hook to be called whenever a contact is added or removed. * This method should be called before @@ -55,32 +61,27 @@ public interface ContactManager { /** * Returns the static link that needs to be sent to the contact to be added. */ - String getRemoteContactLink(); - - /** - * Returns true if the given link is syntactically valid. - */ - boolean isValidRemoteContactLink(String link); + String getHandshakeLink() throws DbException; /** * Requests a new contact to be added via the given {@code link}. * * @param link The link received from the contact we want to add. * @param alias The alias the user has given this contact. - * @return A PendingContact representing the contact to be added. */ - PendingContact addRemoteContactRequest(String link, String alias); + void addPendingContact(String link, String alias) + throws DbException, FormatException; /** * Returns a list of {@link PendingContact}s. */ - Collection getPendingContacts(); + Collection getPendingContacts() throws DbException; /** * Removes a {@link PendingContact} that is in state * {@link PendingContactState FAILED}. */ - void removePendingContact(PendingContact pendingContact); + void removePendingContact(PendingContactId pendingContact) throws DbException; /** * Returns the contact with the given ID. diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionSucceededEvent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/event/ContactAddedRemotelyEvent.java similarity index 68% rename from briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionSucceededEvent.java rename to bramble-api/src/main/java/org/briarproject/bramble/api/contact/event/ContactAddedRemotelyEvent.java index 88998789c..b657d568d 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionSucceededEvent.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/event/ContactAddedRemotelyEvent.java @@ -1,4 +1,4 @@ -package org.briarproject.briar.api.introduction.event; +package org.briarproject.bramble.api.contact.event; import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.event.Event; @@ -8,11 +8,11 @@ import javax.annotation.concurrent.Immutable; @Immutable @NotNullByDefault -public class IntroductionSucceededEvent extends Event { +public class ContactAddedRemotelyEvent extends Event { private final Contact contact; - public IntroductionSucceededEvent(Contact contact) { + public ContactAddedRemotelyEvent(Contact contact) { this.contact = contact; } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/event/PendingContactRemovedEvent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/event/PendingContactRemovedEvent.java new file mode 100644 index 000000000..4d9c0e281 --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/event/PendingContactRemovedEvent.java @@ -0,0 +1,26 @@ +package org.briarproject.bramble.api.contact.event; + +import org.briarproject.bramble.api.contact.PendingContactId; +import org.briarproject.bramble.api.event.Event; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.concurrent.Immutable; + +/** + * An event that is broadcast when a pending contact is removed. + */ +@Immutable +@NotNullByDefault +public class PendingContactRemovedEvent extends Event { + + private final PendingContactId id; + + public PendingContactRemovedEvent(PendingContactId id) { + this.id = id; + } + + public PendingContactId getId() { + return id; + } + +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java index 14011f0c6..2cf100a14 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java @@ -22,16 +22,13 @@ import java.util.Collection; import java.util.List; import java.util.Random; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import javax.inject.Inject; import static java.util.Collections.emptyList; -import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; -import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.OURSELVES; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.UNKNOWN; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.UNVERIFIED; @@ -42,11 +39,8 @@ import static org.briarproject.bramble.util.StringUtils.toUtf8; @NotNullByDefault class ContactManagerImpl implements ContactManager { - private static final int LINK_LENGTH = 64; private static final String REMOTE_CONTACT_LINK = "briar://" + getRandomBase32String(LINK_LENGTH); - private static final Pattern LINK_REGEX = - Pattern.compile("(briar://)?([a-z2-7]{" + LINK_LENGTH + "})"); private final DatabaseComponent db; private final KeyManager keyManager; @@ -97,11 +91,12 @@ class ContactManagerImpl implements ContactManager { } @Override - public String getRemoteContactLink() { + public String getHandshakeLink() { // TODO replace with real implementation return REMOTE_CONTACT_LINK; } + // TODO replace with real implementation @SuppressWarnings("SameParameterValue") private static String getRandomBase32String(int length) { Random random = new Random(); @@ -115,16 +110,9 @@ class ContactManagerImpl implements ContactManager { } @Override - public boolean isValidRemoteContactLink(String link) { - return LINK_REGEX.matcher(link).matches(); - } - - @Override - public PendingContact addRemoteContactRequest(String link, String alias) { + public void addPendingContact(String link, String alias) + throws DbException { // TODO replace with real implementation - PendingContactId id = new PendingContactId(link.getBytes()); - return new PendingContact(id, new byte[MAX_PUBLIC_KEY_LENGTH], alias, - WAITING_FOR_CONNECTION, System.currentTimeMillis()); } @Override @@ -134,7 +122,7 @@ class ContactManagerImpl implements ContactManager { } @Override - public void removePendingContact(PendingContact pendingContact) { + public void removePendingContact(PendingContactId id) throws DbException { // TODO replace with real implementation } diff --git a/briar-android/artwork/ic_nearby.svg b/briar-android/artwork/ic_nearby.svg new file mode 100644 index 000000000..b25e40dee --- /dev/null +++ b/briar-android/artwork/ic_nearby.svg @@ -0,0 +1,57 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/briar-android/artwork/nickname.svg b/briar-android/artwork/nickname.svg new file mode 100644 index 000000000..7daecca04 --- /dev/null +++ b/briar-android/artwork/nickname.svg @@ -0,0 +1,176 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/briar-android/build.gradle b/briar-android/build.gradle index 2e39889e3..ac5933d02 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -118,6 +118,7 @@ dependencies { implementation 'com.google.zxing:core:3.3.3' implementation 'uk.co.samuelwall:material-tap-target-prompt:2.14.0' implementation 'com.vanniktech:emoji-google:0.5.1' + implementation 'com.github.kobakei:MaterialFabSpeedDial:1.2.1' // later versions already use androidx def glideVersion = '4.9.0' implementation("com.github.bumptech.glide:glide:$glideVersion") { exclude group: 'com.android.support' diff --git a/briar-android/src/main/AndroidManifest.xml b/briar-android/src/main/AndroidManifest.xml index 225473793..51c5c7954 100644 --- a/briar-android/src/main/AndroidManifest.xml +++ b/briar-android/src/main/AndroidManifest.xml @@ -425,5 +425,31 @@ android:launchMode="singleTask" android:theme="@style/BriarTheme.NoActionBar"/> + + + + + + + + + + + + + + + + + + diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AndroidNotificationManagerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/AndroidNotificationManagerImpl.java index 05de52a70..e2912f920 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AndroidNotificationManagerImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AndroidNotificationManagerImpl.java @@ -42,7 +42,7 @@ import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.blog.event.BlogPostAddedEvent; import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent; import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent; -import org.briarproject.briar.api.introduction.event.IntroductionSucceededEvent; +import org.briarproject.bramble.api.contact.event.ContactAddedRemotelyEvent; import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent; import java.util.Set; @@ -99,7 +99,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, private final Multiset groupCounts = new Multiset<>(); private final Multiset forumCounts = new Multiset<>(); private final Multiset blogCounts = new Multiset<>(); - private int introductionTotal = 0; + private int contactAddedTotal = 0; private int nextRequestId = 0; private ContactId blockedContact = null; private GroupId blockedGroup = null; @@ -171,7 +171,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, clearGroupMessageNotification(); clearForumPostNotification(); clearBlogPostNotification(); - clearIntroductionSuccessNotification(); + clearContactAddedNotification(); return null; }); try { @@ -206,9 +206,9 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, } @UiThread - private void clearIntroductionSuccessNotification() { - introductionTotal = 0; - notificationManager.cancel(INTRODUCTION_SUCCESS_NOTIFICATION_ID); + private void clearContactAddedNotification() { + contactAddedTotal = 0; + notificationManager.cancel(CONTACT_ADDED_NOTIFICATION_ID); } @Override @@ -230,8 +230,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, } else if (e instanceof BlogPostAddedEvent) { BlogPostAddedEvent b = (BlogPostAddedEvent) e; if (!b.isLocal()) showBlogPostNotification(b.getGroupId()); - } else if (e instanceof IntroductionSucceededEvent) { - showIntroductionNotification(); + } else if (e instanceof ContactAddedRemotelyEvent) { + showContactAddedNotification(); } } @@ -563,24 +563,24 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, } @UiThread - private void showIntroductionNotification() { - introductionTotal++; - updateIntroductionNotification(); + private void showContactAddedNotification() { + contactAddedTotal++; + updateContactAddedNotification(); } @UiThread - private void updateIntroductionNotification() { + private void updateContactAddedNotification() { BriarNotificationBuilder b = new BriarNotificationBuilder(appContext, CONTACT_CHANNEL_ID); - b.setSmallIcon(R.drawable.notification_introduction); + b.setSmallIcon(R.drawable.notification_contact_added); b.setColorRes(R.color.briar_primary); b.setContentTitle(appContext.getText(R.string.app_name)); b.setContentText(appContext.getResources().getQuantityString( - R.plurals.introduction_notification_text, introductionTotal, - introductionTotal)); + R.plurals.contact_added_notification_text, contactAddedTotal, + contactAddedTotal)); b.setNotificationCategory(CATEGORY_MESSAGE); setAlertProperties(b); - setDeleteIntent(b, INTRODUCTION_URI); + setDeleteIntent(b, CONTACT_ADDED_URI); // Touching the notification shows the contact list Intent i = new Intent(appContext, NavDrawerActivity.class); i.putExtra(INTENT_CONTACTS, true); @@ -591,14 +591,13 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, t.addNextIntent(i); b.setContentIntent(t.getPendingIntent(nextRequestId++, 0)); - notificationManager.notify(INTRODUCTION_SUCCESS_NOTIFICATION_ID, + notificationManager.notify(CONTACT_ADDED_NOTIFICATION_ID, b.build()); } @Override - public void clearAllIntroductionNotifications() { - androidExecutor.runOnUiThread( - this::clearIntroductionSuccessNotification); + public void clearAllContactAddedNotifications() { + androidExecutor.runOnUiThread(this::clearContactAddedNotification); } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/NotificationCleanupService.java b/briar-android/src/main/java/org/briarproject/briar/android/NotificationCleanupService.java index 56d216111..44698d3f5 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/NotificationCleanupService.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/NotificationCleanupService.java @@ -12,7 +12,7 @@ import static org.briarproject.briar.api.android.AndroidNotificationManager.BLOG import static org.briarproject.briar.api.android.AndroidNotificationManager.CONTACT_URI; import static org.briarproject.briar.api.android.AndroidNotificationManager.FORUM_URI; import static org.briarproject.briar.api.android.AndroidNotificationManager.GROUP_URI; -import static org.briarproject.briar.api.android.AndroidNotificationManager.INTRODUCTION_URI; +import static org.briarproject.briar.api.android.AndroidNotificationManager.CONTACT_ADDED_URI; public class NotificationCleanupService extends IntentService { @@ -46,8 +46,8 @@ public class NotificationCleanupService extends IntentService { notificationManager.clearAllForumPostNotifications(); } else if (uri.equals(BLOG_URI)) { notificationManager.clearAllBlogPostNotifications(); - } else if (uri.equals(INTRODUCTION_URI)) { - notificationManager.clearAllIntroductionNotifications(); + } else if (uri.equals(CONTACT_ADDED_URI)) { + notificationManager.clearAllContactAddedNotifications(); } } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/TestingConstants.java b/briar-android/src/main/java/org/briarproject/briar/android/TestingConstants.java index 9363db287..e84fbe7ed 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/TestingConstants.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/TestingConstants.java @@ -36,4 +36,9 @@ public interface TestingConstants { */ boolean FEATURE_FLAG_IMAGE_ATTACHMENTS = IS_DEBUG_BUILD; + /** + * Feature flag for enabling adding contacts at a distance. + */ + boolean FEATURE_FLAG_REMOTE_CONTACTS = IS_DEBUG_BUILD; + } 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 7fc324989..1c0e681ef 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,8 +15,12 @@ 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.add.remote.AddContactActivity; +import org.briarproject.briar.android.contact.add.remote.LinkExchangeFragment; import org.briarproject.briar.android.contact.ContactListFragment; import org.briarproject.briar.android.contact.ContactModule; +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.ImageActivity; @@ -168,6 +172,10 @@ public interface ActivityComponent { void inject(UnlockActivity activity); + void inject(AddContactActivity activity); + + void inject(PendingContactListActivity activity); + // Fragments void inject(AuthorNameFragment fragment); @@ -191,6 +199,10 @@ public interface ActivityComponent { void inject(KeyAgreementFragment fragment); + void inject(LinkExchangeFragment fragment); + + void inject(NicknameFragment fragment); + void inject(ContactChooserFragment fragment); void inject(ShareForumFragment fragment); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java index 64fd345e4..4e0eaf759 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java @@ -5,8 +5,6 @@ import android.content.Intent; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.UiThread; -import android.support.design.widget.Snackbar; -import android.support.v4.content.ContextCompat; import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; import android.support.v7.widget.LinearLayoutManager; @@ -34,6 +32,7 @@ import org.briarproject.briar.android.controller.handler.UiResultExceptionHandle import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.sharing.BlogSharingStatusActivity; import org.briarproject.briar.android.sharing.ShareBlogActivity; +import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.api.blog.BlogPostHeader; @@ -44,6 +43,7 @@ import javax.inject.Inject; import static android.app.Activity.RESULT_OK; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; +import static android.support.design.widget.Snackbar.LENGTH_LONG; import static android.widget.Toast.LENGTH_SHORT; import static java.util.Objects.requireNonNull; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; @@ -356,17 +356,12 @@ public class BlogFragment extends BaseFragment } private void displaySnackbar(int stringId, boolean scroll) { - Snackbar snackbar = - Snackbar.make(list, stringId, Snackbar.LENGTH_LONG); - snackbar.getView().setBackgroundResource(R.color.briar_primary); + BriarSnackbarBuilder sb = new BriarSnackbarBuilder(); if (scroll) { - View.OnClickListener onClick = v -> list.smoothScrollToPosition(0); - snackbar.setActionTextColor(ContextCompat - .getColor(getContext(), - R.color.briar_button_text_positive)); - snackbar.setAction(R.string.blogs_blog_post_scroll_to, onClick); + sb.setAction(R.string.blogs_blog_post_scroll_to, + v -> list.smoothScrollToPosition(0)); } - snackbar.show(); + sb.make(list, stringId, LENGTH_LONG).show(); } private void showDeleteDialog() { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java index 8ab591c4a..2a47ae580 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java @@ -4,15 +4,12 @@ import android.content.Intent; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.UiThread; -import android.support.design.widget.Snackbar; -import android.support.v4.content.ContextCompat; import android.support.v7.widget.LinearLayoutManager; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import org.briarproject.bramble.api.db.DbException; @@ -23,6 +20,7 @@ import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.blog.FeedController.FeedListener; import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler; import org.briarproject.briar.android.fragment.BaseFragment; +import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.api.blog.Blog; import org.briarproject.briar.api.blog.BlogPostHeader; @@ -270,16 +268,12 @@ public class FeedFragment extends BaseFragment implements int count = adapter.getItemCount(); boolean scroll = count > (lastVisible - firstVisible + 1); - Snackbar s = Snackbar.make(list, stringRes, LENGTH_LONG); - s.getView().setBackgroundResource(R.color.briar_primary); + BriarSnackbarBuilder sb = new BriarSnackbarBuilder(); if (scroll) { - OnClickListener onClick = v -> list.smoothScrollToPosition(0); - s.setActionTextColor(ContextCompat - .getColor(getContext(), - R.color.briar_button_text_positive)); - s.setAction(R.string.blogs_blog_post_scroll_to, onClick); + sb.setAction(R.string.blogs_blog_post_scroll_to, + v -> list.smoothScrollToPosition(0)); } - s.show(); + sb.make(list, stringRes, LENGTH_LONG).show(); } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java index 7e5e14be0..f1eaa533c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java @@ -3,22 +3,25 @@ package org.briarproject.briar.android.contact; import android.content.Intent; import android.os.Bundle; import android.support.annotation.UiThread; +import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.Snackbar; import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityOptionsCompat; import android.support.v4.util.Pair; import android.support.v7.widget.LinearLayoutManager; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.TextView; 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.ContactAddedEvent; +import org.briarproject.bramble.api.contact.event.ContactAddedRemotelyEvent; import org.briarproject.bramble.api.contact.event.ContactRemovedEvent; +import org.briarproject.bramble.api.contact.event.PendingContactRemovedEvent; +import org.briarproject.bramble.api.contact.event.PendingContactStateChangedEvent; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.event.Event; @@ -32,9 +35,12 @@ import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener; +import org.briarproject.briar.android.contact.add.remote.AddContactActivity; +import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity; import org.briarproject.briar.android.conversation.ConversationActivity; import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.keyagreement.ContactExchangeActivity; +import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.client.MessageTracker.GroupCount; @@ -49,20 +55,28 @@ import java.util.logging.Logger; import javax.annotation.Nullable; import javax.inject.Inject; +import io.github.kobakei.materialfabspeeddial.FabSpeedDial; +import io.github.kobakei.materialfabspeeddial.FabSpeedDial.OnMenuItemClickListener; +import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu; + import static android.os.Build.VERSION.SDK_INT; +import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE; import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAnimation; import static android.support.v4.view.ViewCompat.getTransitionName; import static java.util.Objects.requireNonNull; import static java.util.logging.Level.WARNING; +import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION; import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.now; +import static org.briarproject.briar.android.TestingConstants.FEATURE_FLAG_REMOTE_CONTACTS; import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID; import static org.briarproject.briar.android.util.UiUtils.isSamsung7; @MethodsNotNullByDefault @ParametersNotNullByDefault -public class ContactListFragment extends BaseFragment implements EventListener { +public class ContactListFragment extends BaseFragment implements EventListener, + OnMenuItemClickListener { public static final String TAG = ContactListFragment.class.getName(); private static final Logger LOG = Logger.getLogger(TAG); @@ -76,6 +90,7 @@ public class ContactListFragment extends BaseFragment implements EventListener { private ContactListAdapter adapter; private BriarRecyclerView list; + private Snackbar snackbar; // Fields that are accessed from background threads must be volatile @Inject @@ -107,7 +122,23 @@ public class ContactListFragment extends BaseFragment implements EventListener { @Nullable Bundle savedInstanceState) { requireNonNull(getActivity()).setTitle(R.string.contact_list_button); - View contentView = inflater.inflate(R.layout.list, container, false); + View contentView = inflater.inflate(R.layout.fragment_contact_list, + container, false); + + FabSpeedDial speedDial = contentView.findViewById(R.id.speedDial); + if (FEATURE_FLAG_REMOTE_CONTACTS) { + speedDial.addOnMenuItemClickListener(this); + } else { + speedDial.setMenu(new FabSpeedDialMenu(contentView.getContext())); + speedDial.addOnStateChangeListener(open -> { + if (open) { + Intent intent = new Intent(getContext(), + ContactExchangeActivity.class); + startActivity(intent); + speedDial.closeMenu(); + } + }); + } OnContactClickListener onContactClickListener = (view, item) -> { @@ -146,26 +177,28 @@ public class ContactListFragment extends BaseFragment implements EventListener { list.setEmptyText(getString(R.string.no_contacts)); list.setEmptyAction(getString(R.string.no_contacts_action)); + snackbar = new BriarSnackbarBuilder() + .setAction(R.string.show, v -> + startActivity(new Intent(getContext(), + PendingContactListActivity.class))) + .make(contentView, R.string.pending_contact_requests_snackbar, + LENGTH_INDEFINITE); + return contentView; } @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.contact_list_actions, menu); - super.onCreateOptionsMenu(menu, inflater); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle presses on the action bar items - switch (item.getItemId()) { - case R.id.action_add_contact: + public void onMenuItemClick(FloatingActionButton fab, @Nullable TextView v, + int itemId) { + switch (itemId) { + case R.id.action_add_contact_nearby: Intent intent = new Intent(getContext(), ContactExchangeActivity.class); startActivity(intent); - return true; - default: - return super.onOptionsItemSelected(item); + return; + case R.id.action_add_contact_remotely: + startActivity( + new Intent(getContext(), AddContactActivity.class)); } } @@ -174,11 +207,26 @@ public class ContactListFragment extends BaseFragment implements EventListener { super.onStart(); eventBus.addListener(this); notificationManager.clearAllContactNotifications(); - notificationManager.clearAllIntroductionNotifications(); + notificationManager.clearAllContactAddedNotifications(); loadContacts(); + checkForPendingContacts(); list.startPeriodicUpdate(); } + private void checkForPendingContacts() { + listener.runOnDbThread(() -> { + try { + if (contactManager.getPendingContacts().isEmpty()) { + runOnUiThreadUnlessDestroyed(() -> snackbar.dismiss()); + } else { + runOnUiThreadUnlessDestroyed(() -> snackbar.show()); + } + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + @Override public void onStop() { super.onStop(); @@ -245,6 +293,16 @@ public class ContactListFragment extends BaseFragment implements EventListener { (ConversationMessageReceivedEvent) e; ConversationMessageHeader h = p.getMessageHeader(); updateItem(p.getContactId(), h); + } else if (e instanceof PendingContactStateChangedEvent) { + PendingContactStateChangedEvent pe = + (PendingContactStateChangedEvent) e; + // only re-check pending contacts for initial state + if (pe.getPendingContactState() == WAITING_FOR_CONNECTION) { + checkForPendingContacts(); + } + } else if (e instanceof PendingContactRemovedEvent || + e instanceof ContactAddedRemotelyEvent) { + checkForPendingContacts(); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactActivity.java new file mode 100644 index 000000000..b04358016 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactActivity.java @@ -0,0 +1,105 @@ +package org.briarproject.briar.android.contact.add.remote; + +import android.arch.lifecycle.ViewModelProvider; +import android.arch.lifecycle.ViewModelProviders; +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.view.MenuItem; +import android.widget.Toast; + +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.activity.BriarActivity; +import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import static android.content.Intent.ACTION_SEND; +import static android.content.Intent.ACTION_VIEW; +import static android.content.Intent.EXTRA_TEXT; +import static android.widget.Toast.LENGTH_LONG; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class AddContactActivity extends BriarActivity implements + BaseFragmentListener { + + @Inject + ViewModelProvider.Factory viewModelFactory; + private AddContactViewModel viewModel; + + @Override + public void injectActivity(ActivityComponent component) { + component.inject(this); + } + + @Override + public void onCreate(@Nullable Bundle state) { + super.onCreate(state); + setContentView(R.layout.activity_fragment_container); + + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setDisplayHomeAsUpEnabled(true); + } + + viewModel = ViewModelProviders.of(this, viewModelFactory) + .get(AddContactViewModel.class); + viewModel.getRemoteLinkEntered().observeEvent(this, entered -> { + if (entered) { + NicknameFragment f = new NicknameFragment(); + showNextFragment(f); + } + }); + + Intent i = getIntent(); + if (i != null) { + onNewIntent(i); + setIntent(null); // don't keep the intent for configuration changes + } + + if (state == null) { + showInitialFragment(new LinkExchangeFragment()); + } + } + + @Override + protected void onNewIntent(Intent i) { + super.onNewIntent(i); + String action = i.getAction(); + if (ACTION_SEND.equals(action) || ACTION_VIEW.equals(action)) { + String text = i.getStringExtra(EXTRA_TEXT); + String uri = i.getDataString(); + if (text != null) handleIncomingLink(text); + else if (uri != null) handleIncomingLink(uri); + } + } + + private void handleIncomingLink(String link) { + if (link.equals(viewModel.getHandshakeLink().getValue())) { + Toast.makeText(this, R.string.intent_own_link, LENGTH_LONG) + .show(); + } else if (viewModel.isValidRemoteContactLink(link)) { + viewModel.setRemoteHandshakeLink(link); + } else { + Toast.makeText(this, R.string.invalid_link, LENGTH_LONG) + .show(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactViewModel.java new file mode 100644 index 000000000..729201cfd --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactViewModel.java @@ -0,0 +1,112 @@ +package org.briarproject.briar.android.contact.add.remote; + +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 android.support.annotation.Nullable; + +import org.briarproject.bramble.api.FormatException; +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.nullsafety.NotNullByDefault; +import org.briarproject.briar.android.viewmodel.LiveEvent; +import org.briarproject.briar.android.viewmodel.MutableLiveEvent; + +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.api.contact.ContactManager.LINK_REGEX; +import static org.briarproject.bramble.util.LogUtils.logException; + +@NotNullByDefault +public class AddContactViewModel extends AndroidViewModel { + + private final static Logger LOG = + getLogger(AddContactViewModel.class.getName()); + + private final ContactManager contactManager; + @DatabaseExecutor + private final Executor dbExecutor; + + private final MutableLiveData handshakeLink = + new MutableLiveData<>(); + private final MutableLiveEvent remoteLinkEntered = + new MutableLiveEvent<>(); + private final MutableLiveData addContactResult = + new MutableLiveData<>(); + @Nullable + private String remoteHandshakeLink; + + @Inject + public AddContactViewModel(@NonNull Application application, + ContactManager contactManager, + @DatabaseExecutor Executor dbExecutor) { + super(application); + this.contactManager = contactManager; + this.dbExecutor = dbExecutor; + loadHandshakeLink(); + } + + private void loadHandshakeLink() { + dbExecutor.execute(() -> { + try { + handshakeLink.postValue(contactManager.getHandshakeLink()); + } catch (DbException e) { + logException(LOG, WARNING, e); + // the UI should stay disable in this case, + // leaving the user unable to proceed + } + }); + } + + LiveData getHandshakeLink() { + return handshakeLink; + } + + @Nullable + String getRemoteHandshakeLink() { + return remoteHandshakeLink; + } + + void setRemoteHandshakeLink(String link) { + remoteHandshakeLink = link; + } + + boolean isValidRemoteContactLink(@Nullable CharSequence link) { + return link != null && LINK_REGEX.matcher(link).find(); + } + + LiveEvent getRemoteLinkEntered() { + return remoteLinkEntered; + } + + void onRemoteLinkEntered() { + if (remoteHandshakeLink == null) throw new IllegalStateException(); + remoteLinkEntered.setEvent(true); + } + + void addContact(String nickname) { + if (remoteHandshakeLink == null) throw new IllegalStateException(); + dbExecutor.execute(() -> { + try { + contactManager.addPendingContact(remoteHandshakeLink, nickname); + addContactResult.postValue(true); + } catch (DbException | FormatException e) { + logException(LOG, WARNING, e); + addContactResult.postValue(false); + } + }); + } + + public LiveData getAddContactResult() { + return addContactResult; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/LinkExchangeFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/LinkExchangeFragment.java new file mode 100644 index 000000000..60450c1f5 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/LinkExchangeFragment.java @@ -0,0 +1,164 @@ +package org.briarproject.briar.android.contact.add.remote; + +import android.arch.lifecycle.ViewModelProvider; +import android.arch.lifecycle.ViewModelProviders; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.os.Bundle; +import android.support.design.widget.TextInputEditText; +import android.support.design.widget.TextInputLayout; +import android.support.v4.app.ShareCompat.IntentBuilder; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +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 java.util.regex.Matcher; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import static android.content.Context.CLIPBOARD_SERVICE; +import static android.widget.Toast.LENGTH_SHORT; +import static java.util.Objects.requireNonNull; +import static org.briarproject.bramble.api.contact.ContactManager.LINK_REGEX; +import static org.briarproject.briar.android.util.UiUtils.observeOnce; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class LinkExchangeFragment extends BaseFragment { + + private static final String TAG = LinkExchangeFragment.class.getName(); + + @Inject + ViewModelProvider.Factory viewModelFactory; + + private AddContactViewModel viewModel; + + private ClipboardManager clipboard; + private TextInputLayout linkInputLayout; + private TextInputEditText linkInput; + + @Override + public String getUniqueTag() { + return TAG; + } + + @Override + public void injectFragment(ActivityComponent component) { + component.inject(this); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + if (getActivity() == null || getContext() == null) return null; + + viewModel = ViewModelProviders.of(getActivity(), viewModelFactory) + .get(AddContactViewModel.class); + + View v = inflater.inflate(R.layout.fragment_link_exchange, + container, false); + + linkInputLayout = v.findViewById(R.id.linkInputLayout); + linkInput = v.findViewById(R.id.linkInput); + if (viewModel.getRemoteHandshakeLink() != null) { + // This can happen if the link was set via an incoming Intent + linkInput.setText(viewModel.getRemoteHandshakeLink()); + } + + clipboard = (ClipboardManager) requireNonNull( + getContext().getSystemService(CLIPBOARD_SERVICE)); + + Button pasteButton = v.findViewById(R.id.pasteButton); + pasteButton.setOnClickListener(view -> { + ClipData clipData = clipboard.getPrimaryClip(); + if (clipData != null && clipData.getItemCount() > 0) + linkInput.setText(clipData.getItemAt(0).getText()); + }); + + observeOnce(viewModel.getHandshakeLink(), this, + this::onHandshakeLinkLoaded); + + return v; + } + + private void onHandshakeLinkLoaded(String link) { + View v = requireNonNull(getView()); + + TextView linkView = v.findViewById(R.id.linkView); + linkView.setText(link); + + Button copyButton = v.findViewById(R.id.copyButton); + ClipData clip = ClipData.newPlainText( + getString(R.string.link_clip_label), link); + copyButton.setOnClickListener(view -> { + clipboard.setPrimaryClip(clip); + Toast.makeText(getContext(), R.string.link_copied_toast, + LENGTH_SHORT).show(); + }); + copyButton.setEnabled(true); + + Button shareButton = v.findViewById(R.id.shareButton); + shareButton.setOnClickListener(view -> + IntentBuilder.from(requireActivity()) + .setText(link) + .setType("text/plain") + .startChooser()); + shareButton.setEnabled(true); + + Button continueButton = v.findViewById(R.id.addButton); + continueButton.setOnClickListener(view -> onContinueButtonClicked()); + continueButton.setEnabled(true); + } + + /** + * Requires {@link AddContactViewModel#getHandshakeLink()} to be loaded. + */ + @Nullable + private String getRemoteHandshakeLinkOrNull() { + CharSequence link = linkInput.getText(); + if (link == null || link.length() == 0) { + linkInputLayout.setError(getString(R.string.missing_link)); + linkInput.requestFocus(); + return null; + } + + Matcher matcher = LINK_REGEX.matcher(link); + if (matcher.find()) { + String linkWithoutSchema = matcher.group(2); + // Check also if this is our own link. This was loaded already, + // because it enables the Continue button which is the only caller. + if (("briar://" + linkWithoutSchema) + .equals(viewModel.getHandshakeLink().getValue())) { + linkInputLayout.setError(getString(R.string.own_link_error)); + linkInput.requestFocus(); + return null; + } + linkInputLayout.setError(null); + return linkWithoutSchema; + } + linkInputLayout.setError(getString(R.string.invalid_link)); + linkInput.requestFocus(); + return null; + } + + private void onContinueButtonClicked() { + String link = getRemoteHandshakeLinkOrNull(); + if (link == null) return; // invalid link + + viewModel.setRemoteHandshakeLink(link); + viewModel.onRemoteLinkEntered(); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/NicknameFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/NicknameFragment.java new file mode 100644 index 000000000..fc1ad3469 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/NicknameFragment.java @@ -0,0 +1,121 @@ +package org.briarproject.briar.android.contact.add.remote; + +import android.arch.lifecycle.ViewModelProvider; +import android.arch.lifecycle.ViewModelProviders; +import android.content.Intent; +import android.os.Bundle; +import android.support.design.widget.TextInputEditText; +import android.support.design.widget.TextInputLayout; +import android.text.Editable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.Toast; + +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 javax.annotation.Nullable; +import javax.inject.Inject; + +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; +import static android.widget.Toast.LENGTH_LONG; +import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; +import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class NicknameFragment extends BaseFragment { + + private static final String TAG = NicknameFragment.class.getName(); + + @Inject + ViewModelProvider.Factory viewModelFactory; + + private AddContactViewModel viewModel; + + private TextInputLayout contactNameLayout; + private TextInputEditText contactNameInput; + private Button addButton; + private ProgressBar progressBar; + + @Override + public String getUniqueTag() { + return TAG; + } + + @Override + public void injectFragment(ActivityComponent component) { + component.inject(this); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + if (getActivity() == null || getContext() == null) return null; + + View v = inflater.inflate(R.layout.fragment_nickname, + container, false); + + viewModel = ViewModelProviders.of(getActivity(), viewModelFactory) + .get(AddContactViewModel.class); + + contactNameLayout = v.findViewById(R.id.contactNameLayout); + contactNameInput = v.findViewById(R.id.contactNameInput); + + addButton = v.findViewById(R.id.addButton); + addButton.setOnClickListener(view -> onAddButtonClicked()); + + progressBar = v.findViewById(R.id.progressBar); + + return v; + } + + @Nullable + private String getNicknameOrNull() { + Editable name = contactNameInput.getText(); + if (name == null || name.toString().trim().length() == 0) { + contactNameLayout.setError(getString(R.string.nickname_missing)); + contactNameInput.requestFocus(); + return null; + } + if (utf8IsTooLong(name.toString(), MAX_AUTHOR_NAME_LENGTH)) { + contactNameLayout.setError(getString(R.string.name_too_long)); + contactNameInput.requestFocus(); + return null; + } + contactNameLayout.setError(null); + return name.toString().trim(); + } + + private void onAddButtonClicked() { + String name = getNicknameOrNull(); + if (name == null) return; // invalid nickname + + addButton.setVisibility(INVISIBLE); + progressBar.setVisibility(VISIBLE); + + viewModel.getAddContactResult().observe(this, success -> { + if (success == null) return; + if (success) { + Intent intent = new Intent(getActivity(), + PendingContactListActivity.class); + startActivity(intent); + } else { + Toast.makeText(getContext(), R.string.adding_contact_error, + LENGTH_LONG).show(); + } + finish(); + }); + viewModel.addContact(name); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactListActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactListActivity.java new file mode 100644 index 000000000..c731a70f9 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactListActivity.java @@ -0,0 +1,105 @@ +package org.briarproject.briar.android.contact.add.remote; + +import android.arch.lifecycle.ViewModelProvider; +import android.arch.lifecycle.ViewModelProviders; +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.LinearLayoutManager; +import android.view.MenuItem; + +import org.briarproject.bramble.api.contact.PendingContact; +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.activity.BriarActivity; +import org.briarproject.briar.android.view.BriarRecyclerView; + +import java.util.Collection; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class PendingContactListActivity extends BriarActivity + implements PendingContactListener { + + @Inject + ViewModelProvider.Factory viewModelFactory; + + private PendingContactListViewModel viewModel; + private PendingContactListAdapter adapter; + private BriarRecyclerView list; + + @Override + public void injectActivity(ActivityComponent component) { + component.inject(this); + } + + @Override + public void onCreate(@Nullable Bundle state) { + super.onCreate(state); + + setContentView(R.layout.list); + + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setDisplayHomeAsUpEnabled(true); + } + + viewModel = ViewModelProviders.of(this, viewModelFactory) + .get(PendingContactListViewModel.class); + viewModel.getPendingContacts() + .observe(this, this::onPendingContactsChanged); + + adapter = new PendingContactListAdapter(this, this, PendingContact.class); + list = findViewById(R.id.list); + list.setEmptyText(R.string.no_pending_contacts); + list.setLayoutManager(new LinearLayoutManager(this)); + list.setAdapter(adapter); + list.showProgressBar(); + } + + @Override + public void onStart() { + super.onStart(); + list.startPeriodicUpdate(); + } + + @Override + protected void onStop() { + super.onStop(); + list.stopPeriodicUpdate(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public void onFailedPendingContactRemoved(PendingContact pendingContact) { + viewModel.removePendingContact(pendingContact.getId()); + } + + private void onPendingContactsChanged(Collection contacts) { + if (contacts.isEmpty()) { + if (adapter.isEmpty()) { + list.showData(); // hides progress bar, shows empty text + } else { + // all previous contacts have been removed, so we can finish + supportFinishAfterTransition(); + } + } else { + adapter.setItems(contacts); + } + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactListAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactListAdapter.java new file mode 100644 index 000000000..f2a25d6c7 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactListAdapter.java @@ -0,0 +1,59 @@ +package org.briarproject.briar.android.contact.add.remote; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.briarproject.bramble.api.contact.PendingContact; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.util.BriarAdapter; + +@NotNullByDefault +class PendingContactListAdapter extends + BriarAdapter { + + private final PendingContactListener listener; + + PendingContactListAdapter(Context ctx, PendingContactListener listener, + Class c) { + super(ctx, c); + this.listener = listener; + } + + @Override + public PendingContactViewHolder onCreateViewHolder(ViewGroup viewGroup, + int i) { + View v = LayoutInflater.from(viewGroup.getContext()).inflate( + R.layout.list_item_pending_contact, viewGroup, false); + return new PendingContactViewHolder(v, listener); + } + + @Override + public void onBindViewHolder( + PendingContactViewHolder pendingContactViewHolder, int i) { + pendingContactViewHolder.bind(items.get(i)); + } + + @Override + public int compare(PendingContact item1, PendingContact item2) { + return (int) (item1.getTimestamp() - item2.getTimestamp()); + } + + @Override + public boolean areContentsTheSame(PendingContact item1, + PendingContact item2) { + return item1.getId().equals(item2.getId()) && + item1.getAlias().equals(item2.getAlias()) && + item1.getTimestamp() == item2.getTimestamp() && + item1.getState() == item2.getState(); + } + + @Override + public boolean areItemsTheSame(PendingContact item1, + PendingContact item2) { + return item1.getId().equals(item2.getId()); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactListViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactListViewModel.java new file mode 100644 index 000000000..c0772517f --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactListViewModel.java @@ -0,0 +1,97 @@ +package org.briarproject.briar.android.contact.add.remote; + +import android.app.Application; +import android.arch.lifecycle.AndroidViewModel; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.MutableLiveData; + +import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.contact.PendingContact; +import org.briarproject.bramble.api.contact.PendingContactId; +import org.briarproject.bramble.api.contact.event.ContactAddedRemotelyEvent; +import org.briarproject.bramble.api.contact.event.PendingContactRemovedEvent; +import org.briarproject.bramble.api.contact.event.PendingContactStateChangedEvent; +import org.briarproject.bramble.api.db.DatabaseExecutor; +import org.briarproject.bramble.api.db.DbException; +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.nullsafety.NotNullByDefault; + +import java.util.Collection; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.LogUtils.logException; + +@NotNullByDefault +public class PendingContactListViewModel extends AndroidViewModel + implements EventListener { + + private final Logger LOG = + getLogger(PendingContactListViewModel.class.getName()); + + @DatabaseExecutor + private final Executor dbExecutor; + private final ContactManager contactManager; + private final EventBus eventBus; + + private final MutableLiveData> pendingContacts = + new MutableLiveData<>(); + + @Inject + public PendingContactListViewModel(Application application, + @DatabaseExecutor Executor dbExecutor, + ContactManager contactManager, EventBus eventBus) { + super(application); + this.dbExecutor = dbExecutor; + this.contactManager = contactManager; + this.eventBus = eventBus; + this.eventBus.addListener(this); + loadPendingContacts(); + } + + @Override + protected void onCleared() { + super.onCleared(); + eventBus.removeListener(this); + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof ContactAddedRemotelyEvent || + e instanceof PendingContactStateChangedEvent || + e instanceof PendingContactRemovedEvent) { + loadPendingContacts(); + } + } + + private void loadPendingContacts() { + dbExecutor.execute(() -> { + try { + pendingContacts.postValue(contactManager.getPendingContacts()); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + LiveData> getPendingContacts() { + return pendingContacts; + } + + void removePendingContact(PendingContactId id) { + dbExecutor.execute(() -> { + try { + contactManager.removePendingContact(id); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactListener.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactListener.java new file mode 100644 index 000000000..b9090224a --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactListener.java @@ -0,0 +1,9 @@ +package org.briarproject.briar.android.contact.add.remote; + +import org.briarproject.bramble.api.contact.PendingContact; + +interface PendingContactListener { + + void onFailedPendingContactRemoved(PendingContact pendingContact); + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactViewHolder.java new file mode 100644 index 000000000..ac3b2a917 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactViewHolder.java @@ -0,0 +1,77 @@ +package org.briarproject.briar.android.contact.add.remote; + +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import org.briarproject.bramble.api.contact.PendingContact; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.view.TextAvatarView; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static org.briarproject.briar.android.util.UiUtils.formatDate; + +@NotNullByDefault +class PendingContactViewHolder extends ViewHolder { + + private final PendingContactListener listener; + private final TextAvatarView avatar; + private final TextView name; + private final TextView time; + private final TextView status; + private final Button removeButton; + + PendingContactViewHolder(View v, PendingContactListener listener) { + super(v); + avatar = v.findViewById(R.id.avatar); + name = v.findViewById(R.id.name); + time = v.findViewById(R.id.time); + status = v.findViewById(R.id.status); + removeButton = v.findViewById(R.id.removeButton); + this.listener = listener; + } + + public void bind(PendingContact item) { + avatar.setText(item.getAlias()); + avatar.setBackgroundBytes(item.getId().getBytes()); + name.setText(item.getAlias()); + time.setText(formatDate(time.getContext(), item.getTimestamp())); + removeButton.setOnClickListener(v -> { + listener.onFailedPendingContactRemoved(item); + removeButton.setEnabled(false); + }); + + int color = ContextCompat + .getColor(status.getContext(), R.color.briar_green); + int buttonVisibility = GONE; + switch (item.getState()) { + case WAITING_FOR_CONNECTION: + color = ContextCompat + .getColor(status.getContext(), R.color.briar_yellow); + status.setText(R.string.waiting_for_contact_to_come_online); + break; + case CONNECTED: + status.setText(R.string.connecting); + break; + case ADDING_CONTACT: + status.setText(R.string.adding_contact); + break; + case FAILED: + color = ContextCompat + .getColor(status.getContext(), R.color.briar_red); + status.setText(R.string.adding_contact_failed); + buttonVisibility = VISIBLE; + break; + default: + throw new IllegalStateException(); + } + status.setTextColor(color); + removeButton.setVisibility(buttonVisibility); + removeButton.setEnabled(true); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 3fb0db38c..9caf6aa64 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -59,6 +59,7 @@ import org.briarproject.briar.android.conversation.ConversationVisitor.TextCache import org.briarproject.briar.android.forum.ForumActivity; import org.briarproject.briar.android.introduction.IntroductionActivity; import org.briarproject.briar.android.privategroup.conversation.GroupActivity; +import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.android.view.ImagePreview; import org.briarproject.briar.android.view.TextAttachmentController; @@ -296,10 +297,10 @@ public class ConversationActivity extends BriarActivity super.onActivityResult(request, result, data); if (request == REQUEST_INTRODUCTION && result == RESULT_OK) { - Snackbar snackbar = Snackbar.make(list, R.string.introduction_sent, - Snackbar.LENGTH_SHORT); - snackbar.getView().setBackgroundResource(R.color.briar_primary); - snackbar.show(); + new BriarSnackbarBuilder() + .make(list, R.string.introduction_sent, + Snackbar.LENGTH_SHORT) + .show(); } else if (request == REQUEST_ATTACH_IMAGE && result == RESULT_OK) { // remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS ((TextAttachmentController) sendController).onImageReceived(data); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java index 8b5a15be2..0df8f832f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java @@ -9,7 +9,6 @@ import android.os.Bundle; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.design.widget.AppBarLayout; -import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentStatePagerAdapter; @@ -32,6 +31,7 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.BriarActivity; +import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.PullDownLayout; import java.text.SimpleDateFormat; @@ -308,9 +308,10 @@ public class ImageActivity extends BriarActivity R.string.save_image_error : R.string.save_image_success; int colorRes = error ? R.color.briar_red : R.color.briar_primary; - Snackbar s = Snackbar.make(layout, stringRes, LENGTH_LONG); - s.getView().setBackgroundResource(colorRes); - s.show(); + new BriarSnackbarBuilder() + .setBackgroundColor(colorRes) + .make(layout, stringRes, LENGTH_LONG) + .show(); viewModel.onSaveStateSeen(); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListFragment.java index dd4516f6e..b5d86fde1 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListFragment.java @@ -4,7 +4,6 @@ import android.content.Intent; import android.os.Bundle; import android.support.annotation.UiThread; import android.support.design.widget.Snackbar; -import android.support.v4.content.ContextCompat; import android.support.v7.widget.LinearLayoutManager; import android.view.LayoutInflater; import android.view.Menu; @@ -27,6 +26,7 @@ import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.fragment.BaseEventFragment; import org.briarproject.briar.android.sharing.ForumInvitationActivity; +import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.client.MessageTracker.GroupCount; @@ -105,11 +105,9 @@ public class ForumListFragment extends BaseEventFragment implements list.setLayoutManager(new LinearLayoutManager(getActivity())); list.setAdapter(adapter); - snackbar = Snackbar.make(list, "", LENGTH_INDEFINITE); - snackbar.getView().setBackgroundResource(R.color.briar_primary); - snackbar.setAction(R.string.show, this); - snackbar.setActionTextColor(ContextCompat - .getColor(getActivity(), R.color.briar_button_text_positive)); + snackbar = new BriarSnackbarBuilder() + .setAction(R.string.show, this) + .make(list, "", LENGTH_INDEFINITE); return contentView; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListFragment.java index e82306370..f6a1f3a0f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListFragment.java @@ -4,7 +4,6 @@ import android.content.Intent; import android.os.Bundle; import android.support.annotation.UiThread; import android.support.design.widget.Snackbar; -import android.support.v4.content.ContextCompat; import android.support.v7.widget.LinearLayoutManager; import android.view.LayoutInflater; import android.view.Menu; @@ -27,6 +26,7 @@ import org.briarproject.briar.android.privategroup.creation.CreateGroupActivity; import org.briarproject.briar.android.privategroup.invitation.GroupInvitationActivity; import org.briarproject.briar.android.privategroup.list.GroupListController.GroupListListener; import org.briarproject.briar.android.privategroup.list.GroupViewHolder.OnGroupRemoveClickListener; +import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.api.privategroup.GroupMessageHeader; @@ -82,11 +82,9 @@ public class GroupListFragment extends BaseFragment implements list.setLayoutManager(new LinearLayoutManager(getContext())); list.setAdapter(adapter); - snackbar = Snackbar.make(list, "", LENGTH_INDEFINITE); - snackbar.getView().setBackgroundResource(R.color.briar_primary); - snackbar.setAction(R.string.show, this); - snackbar.setActionTextColor(ContextCompat - .getColor(getActivity(), R.color.briar_button_text_positive)); + snackbar = new BriarSnackbarBuilder() + .setAction(R.string.show, this) + .make(list, "", LENGTH_INDEFINITE); return v; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java index 37942f3ef..f74626b67 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java @@ -26,6 +26,7 @@ import org.briarproject.briar.android.controller.handler.UiResultExceptionHandle import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener; import org.briarproject.briar.android.threaded.ThreadListController.ThreadListDataSource; import org.briarproject.briar.android.threaded.ThreadListController.ThreadListListener; +import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.android.view.KeyboardAwareLinearLayout; import org.briarproject.briar.android.view.TextInputView; @@ -41,7 +42,6 @@ import java.util.logging.Logger; import javax.annotation.Nullable; import javax.inject.Inject; -import static android.support.design.widget.Snackbar.make; import static android.support.v7.widget.RecyclerView.NO_POSITION; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; @@ -324,9 +324,9 @@ public abstract class ThreadListActivity extends LiveData> { + + public void observeEvent(LifecycleOwner owner, + LiveEventHandler handler) { + LiveEventObserver observer = new LiveEventObserver<>(handler); + super.observe(owner, observer); + } + + public static class ConsumableEvent { + private final T content; + private boolean consumed = false; + + public ConsumableEvent(T content) { + this.content = content; + } + + @Nullable + public T getContentIfNotConsumed() { + if (consumed) return null; + else { + consumed = true; + return content; + } + } + } + + @Immutable + public static class LiveEventObserver + implements Observer> { + private final LiveEventHandler handler; + + public LiveEventObserver(LiveEventHandler handler) { + this.handler = handler; + } + + @Override + public void onChanged(@Nullable ConsumableEvent consumableEvent) { + if (consumableEvent != null) { + T content = consumableEvent.getContentIfNotConsumed(); + if (content != null) handler.onEventUnconsumedContent(content); + } + } + + } + + public interface LiveEventHandler { + void onEventUnconsumedContent(T t); + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/MutableLiveEvent.java b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/MutableLiveEvent.java new file mode 100644 index 000000000..dac33ecfc --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/MutableLiveEvent.java @@ -0,0 +1,15 @@ +package org.briarproject.briar.android.viewmodel; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +@NotNullByDefault +public class MutableLiveEvent extends LiveEvent { + + public void postEvent(T value) { + super.postValue(new ConsumableEvent<>(value)); + } + + public void setEvent(T value) { + super.setValue(new ConsumableEvent<>(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 index e525fd059..d35f2bb5b 100644 --- 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 @@ -3,6 +3,8 @@ package org.briarproject.briar.android.viewmodel; import android.arch.lifecycle.ViewModel; import android.arch.lifecycle.ViewModelProvider; +import org.briarproject.briar.android.contact.add.remote.AddContactViewModel; +import org.briarproject.briar.android.contact.add.remote.PendingContactListViewModel; import org.briarproject.briar.android.conversation.ConversationViewModel; import org.briarproject.briar.android.conversation.ImageViewModel; @@ -27,6 +29,18 @@ public abstract class ViewModelModule { abstract ViewModel bindImageViewModel( ImageViewModel imageViewModel); + @Binds + @IntoMap + @ViewModelKey(AddContactViewModel.class) + abstract ViewModel bindAddContactViewModel( + AddContactViewModel addContactViewModel); + + @Binds + @IntoMap + @ViewModelKey(PendingContactListViewModel.class) + abstract ViewModel bindPendingRequestsViewModel( + PendingContactListViewModel pendingContactListViewModel); + @Binds @Singleton abstract ViewModelProvider.Factory bindViewModelFactory( diff --git a/briar-android/src/main/java/org/briarproject/briar/api/android/AndroidNotificationManager.java b/briar-android/src/main/java/org/briarproject/briar/api/android/AndroidNotificationManager.java index e02a9832f..0a4951155 100644 --- a/briar-android/src/main/java/org/briarproject/briar/api/android/AndroidNotificationManager.java +++ b/briar-android/src/main/java/org/briarproject/briar/api/android/AndroidNotificationManager.java @@ -30,7 +30,7 @@ public interface AndroidNotificationManager { int GROUP_MESSAGE_NOTIFICATION_ID = 5; int FORUM_POST_NOTIFICATION_ID = 6; int BLOG_POST_NOTIFICATION_ID = 7; - int INTRODUCTION_SUCCESS_NOTIFICATION_ID = 8; + int CONTACT_ADDED_NOTIFICATION_ID = 8; // Channel IDs String CONTACT_CHANNEL_ID = "contacts"; @@ -48,7 +48,7 @@ public interface AndroidNotificationManager { String GROUP_URI = "content://org.briarproject.briar/group"; String FORUM_URI = "content://org.briarproject.briar/forum"; String BLOG_URI = "content://org.briarproject.briar/blog"; - String INTRODUCTION_URI = "content://org.briarproject.briar/introduction"; + String CONTACT_ADDED_URI = "content://org.briarproject.briar/contact/added"; // Actions for pending intents String ACTION_DISMISS_REMINDER = "dismissReminder"; @@ -73,7 +73,7 @@ public interface AndroidNotificationManager { void clearAllBlogPostNotifications(); - void clearAllIntroductionNotifications(); + void clearAllContactAddedNotifications(); void showSignInNotification(); diff --git a/briar-android/src/main/res/drawable/bubble_accent.xml b/briar-android/src/main/res/drawable/bubble_accent.xml new file mode 100644 index 000000000..959efaa82 --- /dev/null +++ b/briar-android/src/main/res/drawable/bubble_accent.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/briar-android/src/main/res/drawable/bubble_completed.xml b/briar-android/src/main/res/drawable/bubble_completed.xml new file mode 100644 index 000000000..799a93c93 --- /dev/null +++ b/briar-android/src/main/res/drawable/bubble_completed.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/briar-android/src/main/res/drawable/ic_call_made.xml b/briar-android/src/main/res/drawable/ic_call_made.xml new file mode 100644 index 000000000..1c2334060 --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_call_made.xml @@ -0,0 +1,9 @@ + + + diff --git a/briar-android/src/main/res/drawable/ic_call_received.xml b/briar-android/src/main/res/drawable/ic_call_received.xml new file mode 100644 index 000000000..7528c847b --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_call_received.xml @@ -0,0 +1,10 @@ + + + diff --git a/briar-android/src/main/res/drawable/ic_content_copy.xml b/briar-android/src/main/res/drawable/ic_content_copy.xml new file mode 100644 index 000000000..5f2c3adda --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_content_copy.xml @@ -0,0 +1,10 @@ + + + diff --git a/briar-android/src/main/res/drawable/ic_content_paste.xml b/briar-android/src/main/res/drawable/ic_content_paste.xml new file mode 100644 index 000000000..07a6a9b90 --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_content_paste.xml @@ -0,0 +1,10 @@ + + + diff --git a/briar-android/src/main/res/drawable/ic_link_menu.xml b/briar-android/src/main/res/drawable/ic_link_menu.xml new file mode 100644 index 000000000..a866c4d9c --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_link_menu.xml @@ -0,0 +1,9 @@ + + + diff --git a/briar-android/src/main/res/drawable/ic_nearby.xml b/briar-android/src/main/res/drawable/ic_nearby.xml new file mode 100644 index 000000000..9e16444a3 --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_nearby.xml @@ -0,0 +1,9 @@ + + + diff --git a/briar-android/src/main/res/drawable/ic_nickname.xml b/briar-android/src/main/res/drawable/ic_nickname.xml new file mode 100644 index 000000000..f7e69d377 --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_nickname.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + diff --git a/briar-android/src/main/res/drawable/ic_person.xml b/briar-android/src/main/res/drawable/ic_person.xml new file mode 100644 index 000000000..d7366bda0 --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_person.xml @@ -0,0 +1,5 @@ + + + diff --git a/briar-android/src/main/res/drawable/notification_introduction.xml b/briar-android/src/main/res/drawable/notification_contact_added.xml similarity index 100% rename from briar-android/src/main/res/drawable/notification_introduction.xml rename to briar-android/src/main/res/drawable/notification_contact_added.xml diff --git a/briar-android/src/main/res/drawable/social_share_blue.xml b/briar-android/src/main/res/drawable/social_share_blue.xml new file mode 100644 index 000000000..d9b30d492 --- /dev/null +++ b/briar-android/src/main/res/drawable/social_share_blue.xml @@ -0,0 +1,10 @@ + + + diff --git a/briar-android/src/main/res/layout/fragment_contact_list.xml b/briar-android/src/main/res/layout/fragment_contact_list.xml new file mode 100644 index 000000000..327dc493c --- /dev/null +++ b/briar-android/src/main/res/layout/fragment_contact_list.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/briar-android/src/main/res/layout/fragment_link_exchange.xml b/briar-android/src/main/res/layout/fragment_link_exchange.xml new file mode 100644 index 000000000..339fff186 --- /dev/null +++ b/briar-android/src/main/res/layout/fragment_link_exchange.xml @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + + + +