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..33bace51e 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 @@ -10,12 +10,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,7 +60,7 @@ public interface ContactManager { /** * Returns the static link that needs to be sent to the contact to be added. */ - String getRemoteContactLink(); + String getRemoteContactLink() throws DbException; /** * Returns true if the given link is syntactically valid. @@ -69,12 +74,12 @@ public interface ContactManager { * @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 addRemoteContactRequest(String link, String alias); /** * Returns a list of {@link PendingContact}s. */ - Collection getPendingContacts(); + Collection getPendingContacts() throws DbException; /** * Removes a {@link PendingContact} that is in state 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..87109efad 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 @@ -5,11 +5,14 @@ import org.briarproject.bramble.api.contact.ContactId; 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.PendingContactStateChangedEvent; import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.identity.Author; import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.identity.AuthorInfo; @@ -18,17 +21,18 @@ import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.transport.KeyManager; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Random; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.regex.Pattern; +import java.util.concurrent.Executor; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import javax.inject.Inject; -import static java.util.Collections.emptyList; +import static java.lang.System.currentTimeMillis; 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; @@ -42,11 +46,12 @@ 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 + "})"); + // TODO remove + private final List pendingContacts = new ArrayList<>(); + @DatabaseExecutor + private final Executor dbExecutor; private final DatabaseComponent db; private final KeyManager keyManager; @@ -54,9 +59,11 @@ class ContactManagerImpl implements ContactManager { private final List hooks; @Inject - ContactManagerImpl(DatabaseComponent db, KeyManager keyManager, + ContactManagerImpl(DatabaseComponent db, + @DatabaseExecutor Executor dbExecutor, KeyManager keyManager, IdentityManager identityManager) { this.db = db; + this.dbExecutor = dbExecutor; this.keyManager = keyManager; this.identityManager = identityManager; hooks = new CopyOnWriteArrayList<>(); @@ -99,9 +106,14 @@ class ContactManagerImpl implements ContactManager { @Override public String getRemoteContactLink() { // TODO replace with real implementation + try { + Thread.sleep(1500); + } catch (InterruptedException ignored) { + } return REMOTE_CONTACT_LINK; } + // TODO remove @SuppressWarnings("SameParameterValue") private static String getRandomBase32String(int length) { Random random = new Random(); @@ -120,22 +132,37 @@ class ContactManagerImpl implements ContactManager { } @Override - public PendingContact addRemoteContactRequest(String link, String alias) { + public void addRemoteContactRequest(String link, String alias) { // 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()); + dbExecutor.execute(() -> { + try { + Thread.sleep(2000); + } catch (InterruptedException ignored) { + } + PendingContactId id = new PendingContactId(link.getBytes()); + PendingContact pendingContact = + new PendingContact(id, new byte[MAX_PUBLIC_KEY_LENGTH], + alias, WAITING_FOR_CONNECTION, currentTimeMillis()); + pendingContacts.add(pendingContact); + Event e = new PendingContactStateChangedEvent(id, + WAITING_FOR_CONNECTION); + try { + db.transaction(true, txn -> txn.attach(e)); + } catch (DbException ignored) { + } + }); } @Override public Collection getPendingContacts() { // TODO replace with real implementation - return emptyList(); + return pendingContacts; } @Override public void removePendingContact(PendingContact pendingContact) { // TODO replace with real implementation + pendingContacts.remove(pendingContact); } @Override diff --git a/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java index d1e269264..06d0124b6 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java @@ -16,12 +16,14 @@ import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.transport.KeyManager; import org.briarproject.bramble.test.BrambleMockTestCase; import org.briarproject.bramble.test.DbExpectations; +import org.briarproject.bramble.test.ImmediateExecutor; import org.jmock.Expectations; import org.jmock.Mockery; import org.junit.Test; import java.util.Collection; import java.util.Random; +import java.util.concurrent.Executor; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; @@ -56,8 +58,9 @@ public class ContactManagerImplTest extends BrambleMockTestCase { private final ContactId contactId = contact.getId(); public ContactManagerImplTest() { - contactManager = - new ContactManagerImpl(db, keyManager, identityManager); + Executor dbExecutor = new ImmediateExecutor(); + contactManager = new ContactManagerImpl(db, dbExecutor, keyManager, + identityManager); } @Test 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..7595a77d8 100644 --- a/briar-android/src/main/AndroidManifest.xml +++ b/briar-android/src/main/AndroidManifest.xml @@ -425,5 +425,28 @@ android:launchMode="singleTask" android:theme="@style/BriarTheme.NoActionBar"/> + + + + + + + + + + + + + + + + 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..169113c92 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.PendingRequestsActivity; 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(PendingRequestsActivity 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/contact/ContactListFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java index 7e5e14be0..348efa677 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,24 @@ 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.content.ContextCompat; 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.ContactRemovedEvent; +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,6 +34,8 @@ 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.PendingRequestsActivity; import org.briarproject.briar.android.conversation.ConversationActivity; import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.keyagreement.ContactExchangeActivity; @@ -49,11 +53,16 @@ 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 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; @@ -62,7 +71,8 @@ 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 +86,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 +118,12 @@ 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 speedDialView = contentView.findViewById(R.id.speedDial); + speedDialView.addOnMenuItemClickListener(this); OnContactClickListener onContactClickListener = (view, item) -> { @@ -146,26 +162,30 @@ public class ContactListFragment extends BaseFragment implements EventListener { list.setEmptyText(getString(R.string.no_contacts)); list.setEmptyAction(getString(R.string.no_contacts_action)); + // TODO UiUtils helper method? + snackbar = Snackbar.make(contentView, + R.string.pending_contact_requests_snackbar, LENGTH_INDEFINITE); + snackbar.getView().setBackgroundResource(R.color.briar_primary); + snackbar.setAction(R.string.show, v -> startActivity( + new Intent(getContext(), PendingRequestsActivity.class))); + snackbar.setActionTextColor(ContextCompat + .getColor(getContext(), R.color.briar_button_text_positive)); + 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)); } } @@ -176,9 +196,24 @@ public class ContactListFragment extends BaseFragment implements EventListener { notificationManager.clearAllContactNotifications(); notificationManager.clearAllIntroductionNotifications(); 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(); @@ -232,6 +267,7 @@ public class ContactListFragment extends BaseFragment implements EventListener { if (e instanceof ContactAddedEvent) { LOG.info("Contact added, reloading"); loadContacts(); + checkForPendingContacts(); } else if (e instanceof ContactConnectedEvent) { setConnected(((ContactConnectedEvent) e).getContactId(), true); } else if (e instanceof ContactDisconnectedEvent) { @@ -245,6 +281,13 @@ 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(); + } } } 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..c732660a3 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactActivity.java @@ -0,0 +1,99 @@ +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; + + @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); + } + + AddContactViewModel viewModel = + ViewModelProviders.of(this, viewModelFactory) + .get(AddContactViewModel.class); + viewModel.getRemoteLinkEntered().observe(this, entered -> { + if (entered != null && entered) { + NicknameFragment f = new NicknameFragment(); + showNextFragment(f); + } + }); + + Intent i = getIntent(); + if (i != null) { + String action = i.getAction(); + if (ACTION_SEND.equals(action) || ACTION_VIEW.equals(action)) { + String text = i.getStringExtra(EXTRA_TEXT); + if (text != null) { + if (viewModel.isValidRemoteContactLink(text)) { + viewModel.setRemoteContactLink(text); + } else { + Toast.makeText(this, R.string.invalid_link, LENGTH_LONG) + .show(); + } + } + String uri = i.getDataString(); + if (uri != null) { + if (viewModel.isValidRemoteContactLink(uri)) { + viewModel.setRemoteContactLink(uri); + } else { + Toast.makeText(this, R.string.invalid_link, LENGTH_LONG) + .show(); + } + } + } + } + if (state == null) { + showInitialFragment(new LinkExchangeFragment()); + } + } + + @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..d9af1c9f4 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactViewModel.java @@ -0,0 +1,89 @@ +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.contact.ContactManager; +import org.briarproject.bramble.api.db.DatabaseExecutor; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static java.util.logging.Logger.getLogger; + +@NotNullByDefault +public class AddContactViewModel extends AndroidViewModel { + + private static Logger LOG = getLogger(AddContactViewModel.class.getName()); + + private final ContactManager contactManager; + @DatabaseExecutor + private final Executor dbExecutor; + + private final MutableLiveData ourLink = new MutableLiveData<>(); + private final MutableLiveData remoteLinkEntered = + new MutableLiveData<>(); + @Nullable + private String remoteContactLink; + + @Inject + public AddContactViewModel(@NonNull Application application, + ContactManager contactManager, + @DatabaseExecutor Executor dbExecutor) { + super(application); + this.contactManager = contactManager; + this.dbExecutor = dbExecutor; + loadOurLink(); + } + + private void loadOurLink() { + dbExecutor.execute(() -> { + try { + ourLink.postValue(contactManager.getRemoteContactLink()); + } catch (DbException e) { + throw new AssertionError(e); + } + }); + } + + LiveData getOurLink() { + return ourLink; + } + + void setRemoteContactLink(String link) { + remoteContactLink = link; + } + + @Nullable + String getRemoteContactLink() { + return remoteContactLink; + } + + boolean isValidRemoteContactLink(@Nullable CharSequence link) { + return link != null && + contactManager.isValidRemoteContactLink(link.toString()); + } + + LiveData getRemoteLinkEntered() { + return remoteLinkEntered; + } + + void onRemoteLinkEntered() { + if (remoteContactLink == null) throw new IllegalStateException(); + remoteLinkEntered.setValue(true); + } + + void addContact(String nickname) { + if (remoteContactLink == null) throw new IllegalStateException(); + contactManager.addRemoteContactRequest(remoteContactLink, nickname); + } + +} 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..8f5333722 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/LinkExchangeFragment.java @@ -0,0 +1,168 @@ +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.text.Editable; +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.getRemoteContactLink() != null) { + linkInput.setText(viewModel.getRemoteContactLink()); + } + + clipboard = (ClipboardManager) requireNonNull( + getContext().getSystemService(CLIPBOARD_SERVICE)); + + Button pasteButton = v.findViewById(R.id.pasteButton); + pasteButton.setOnClickListener(view -> { + ClipData clipData = clipboard.getPrimaryClip(); + if (clipData != null) + linkInput.setText(clipData.getItemAt(0).getText()); + }); + + observeOnce(viewModel.getOurLink(), this, this::onOwnLinkLoaded); + + return v; + } + + private void onOwnLinkLoaded(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(requireNonNull(getActivity())) + .setText(link) + .setType("text/plain") + .startChooser()); + shareButton.setEnabled(true); + + Button continueButton = v.findViewById(R.id.addButton); + continueButton.setOnClickListener(view -> onContinueButtonClicked()); + continueButton.setEnabled(true); + } + + private boolean isInputError() { + Editable linkText = linkInput.getText(); + boolean briarLink = viewModel.isValidRemoteContactLink(linkText); + if (!briarLink) { + if (linkText == null || linkText.length() == 0) { + linkInputLayout.setError(getString(R.string.missing_link)); + } else { + linkInputLayout.setError(getString(R.string.invalid_link)); + } + linkInput.requestFocus(); + return true; + } else linkInputLayout.setError(null); + String link = getLink(); + boolean isOurLink = link != null && + ("briar://" + link).equals(viewModel.getOurLink().getValue()); + if (isOurLink) { + linkInputLayout.setError(getString(R.string.own_link_error)); + linkInput.requestFocus(); + return true; + } else linkInputLayout.setError(null); + return false; + } + + @Nullable + private String getLink() { + CharSequence link = linkInput.getText(); + if (link == null) return null; + Matcher matcher = LINK_REGEX.matcher(link); + if (matcher.matches()) // needs to be called before groups become available + return matcher.group(2); + else + return null; + } + + private void onContinueButtonClicked() { + if (isInputError()) return; + + String linkText = getLink(); + if (linkText == null) throw new AssertionError(); + viewModel.setRemoteContactLink(linkText); + + 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..6bd420004 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/NicknameFragment.java @@ -0,0 +1,98 @@ +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.annotation.MainThread; +import android.support.annotation.UiThread; +import android.support.design.widget.TextInputEditText; +import android.support.design.widget.TextInputLayout; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +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 java.util.Objects.requireNonNull; + +@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; + + @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); + + Button addButton = v.findViewById(R.id.addButton); + addButton.setOnClickListener(view -> onAddButtonClicked()); + + contactNameLayout = v.findViewById(R.id.contactNameLayout); + contactNameInput = v.findViewById(R.id.contactNameInput); + + return v; + } + + @MainThread + @UiThread + private boolean isInputError() { + boolean validContactName = contactNameInput.getText() != null && + contactNameInput.getText().toString().trim().length() > 0; + if (!validContactName) { + contactNameLayout.setError(getString(R.string.nickname_missing)); + contactNameInput.requestFocus(); + return true; + } else contactNameLayout.setError(null); + return false; + } + + private void onAddButtonClicked() { + if (isInputError()) return; + + String name = requireNonNull(contactNameInput.getText()).toString(); + viewModel.addContact(name); + + Intent intent = + new Intent(getActivity(), PendingRequestsActivity.class); + startActivity(intent); + finish(); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingRequestsActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingRequestsActivity.java new file mode 100644 index 000000000..dbf0d7aba --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingRequestsActivity.java @@ -0,0 +1,125 @@ +package org.briarproject.briar.android.contact.add.remote; + +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.Contact; +import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.contact.PendingContact; +import org.briarproject.bramble.api.contact.event.ContactAddedEvent; +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.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 PendingRequestsActivity extends BriarActivity + implements EventListener { + + @Inject + ContactManager contactManager; + @Inject + EventBus eventBus; + + private PendingRequestsAdapter 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); + } + + adapter = new PendingRequestsAdapter(this, PendingContact.class); + list = findViewById(R.id.list); + list.setLayoutManager(new LinearLayoutManager(this)); + list.setAdapter(adapter); + } + + @Override + public void onStart() { + super.onStart(); + eventBus.addListener(this); + list.startPeriodicUpdate(); + runOnDbThread(() -> { + try { + Collection contacts = + contactManager.getPendingContacts(); + addPendingContacts(contacts); + } catch (DbException e) { + e.printStackTrace(); + } + }); + } + + @Override + protected void onStop() { + super.onStop(); + list.stopPeriodicUpdate(); + adapter.clear(); + } + + @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 eventOccurred(Event e) { + if (e instanceof ContactAddedEvent) { + runOnDbThread(() -> { + try { + Contact contact = contactManager + .getContact(((ContactAddedEvent) e).getContactId()); + runOnUiThreadUnlessDestroyed(() -> { + adapter.remove(contact); + if (adapter.isEmpty()) finish(); + }); + } catch (DbException e1) { + e1.printStackTrace(); + } + }); + } + } + + private void addPendingContacts(Collection contacts) { + runOnUiThreadUnlessDestroyed(() -> { + if (contacts.isEmpty()) { + list.showData(); + } else { + adapter.addAll(contacts); + } + }); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingRequestsAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingRequestsAdapter.java new file mode 100644 index 000000000..53b9bb1e8 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingRequestsAdapter.java @@ -0,0 +1,65 @@ +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.Contact; +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 +public class PendingRequestsAdapter extends + BriarAdapter { + + public PendingRequestsAdapter(Context ctx, Class c) { + super(ctx, c); + } + + @Override + public PendingRequestsViewHolder onCreateViewHolder( + ViewGroup viewGroup, int i) { + View v = LayoutInflater.from(viewGroup.getContext()).inflate( + R.layout.list_item_pending_contact, viewGroup, false); + return new PendingRequestsViewHolder(v); + } + + @Override + public void onBindViewHolder( + PendingRequestsViewHolder pendingRequestsViewHolder, int i) { + pendingRequestsViewHolder.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.getAlias().equals(item2.getAlias()) && + item1.getTimestamp() == item2.getTimestamp(); + } + + @Override + public boolean areItemsTheSame(PendingContact item1, + PendingContact item2) { + return item1.getAlias().equals(item2.getAlias()) && + item1.getTimestamp() == item2.getTimestamp(); + } + + // TODO use PendingContactId + public void remove(Contact contact) { + for (int i = 0; i < items.size(); i++) { + if (items.get(i).getAlias().equals(contact.getAuthor().getName())) { + items.removeItemAt(i); + return; + } + } + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingRequestsViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingRequestsViewHolder.java new file mode 100644 index 000000000..4bacdd533 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingRequestsViewHolder.java @@ -0,0 +1,64 @@ +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.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 org.briarproject.bramble.util.StringUtils.toUtf8; +import static org.briarproject.briar.android.util.UiUtils.formatDate; + +@NotNullByDefault +public class PendingRequestsViewHolder extends ViewHolder { + + private final TextAvatarView avatar; + private final TextView name; + private final TextView time; + private final TextView status; + + public PendingRequestsViewHolder(View v) { + 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); + } + + public void bind(PendingContact item) { + avatar.setText(item.getAlias()); + avatar.setBackgroundBytes(toUtf8(item.getAlias() + item.getTimestamp())); + name.setText(item.getAlias()); + time.setText(formatDate(time.getContext(), item.getTimestamp())); + + int color = ContextCompat + .getColor(status.getContext(), R.color.briar_green); + 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: + // TODO add remove button + color = ContextCompat + .getColor(status.getContext(), R.color.briar_red); + status.setText(R.string.adding_contact_failed); + break; + default: + throw new IllegalStateException(); + } + status.setTextColor(color); + } + +} 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..712ec64c2 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,7 @@ 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.conversation.ConversationViewModel; import org.briarproject.briar.android.conversation.ImageViewModel; @@ -27,6 +28,12 @@ public abstract class ViewModelModule { abstract ViewModel bindImageViewModel( ImageViewModel imageViewModel); + @Binds + @IntoMap + @ViewModelKey(AddContactViewModel.class) + abstract ViewModel bindAddContactViewModel( + AddContactViewModel addContactViewModel); + @Binds @Singleton abstract ViewModelProvider.Factory bindViewModelFactory( 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/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..e2bf15d19 --- /dev/null +++ b/briar-android/src/main/res/layout/fragment_link_exchange.xml @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + +