[android] finalize list of pending contacts and add test code

parent 66cdf4f5
......@@ -85,7 +85,7 @@ public interface ContactManager {
* Removes a {@link PendingContact} that is in state
* {@link PendingContactState FAILED}.
*/
void removePendingContact(PendingContact pendingContact);
void removePendingContact(PendingContact pendingContact) throws DbException;
/**
* Returns the contact with the given ID.
......
......@@ -5,6 +5,7 @@ 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.ContactAddedRemotelyEvent;
import org.briarproject.bramble.api.contact.event.PendingContactStateChangedEvent;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DatabaseComponent;
......@@ -19,6 +20,7 @@ import org.briarproject.bramble.api.identity.AuthorInfo;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.Scheduler;
import org.briarproject.bramble.api.transport.KeyManager;
import java.util.ArrayList;
......@@ -27,12 +29,18 @@ import java.util.List;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject;
import static java.lang.System.currentTimeMillis;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.contact.PendingContactState.ADDING_CONTACT;
import static org.briarproject.bramble.api.contact.PendingContactState.CONNECTED;
import static org.briarproject.bramble.api.contact.PendingContactState.FAILED;
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;
......@@ -48,10 +56,12 @@ class ContactManagerImpl implements ContactManager {
private static final String REMOTE_CONTACT_LINK =
"briar://" + getRandomBase32String(LINK_LENGTH);
// TODO remove
// TODO replace with real implementation
private final List<PendingContact> pendingContacts = new ArrayList<>();
@DatabaseExecutor
private final Executor dbExecutor;
@Scheduler
private final ScheduledExecutorService scheduler;
private final DatabaseComponent db;
private final KeyManager keyManager;
......@@ -61,11 +71,13 @@ class ContactManagerImpl implements ContactManager {
@Inject
ContactManagerImpl(DatabaseComponent db,
@DatabaseExecutor Executor dbExecutor, KeyManager keyManager,
IdentityManager identityManager) {
IdentityManager identityManager, @Scheduler
ScheduledExecutorService scheduler) {
this.db = db;
this.dbExecutor = dbExecutor;
this.keyManager = keyManager;
this.identityManager = identityManager;
this.scheduler = scheduler;
hooks = new CopyOnWriteArrayList<>();
}
......@@ -113,7 +125,7 @@ class ContactManagerImpl implements ContactManager {
return REMOTE_CONTACT_LINK;
}
// TODO remove
// TODO replace with real implementation
@SuppressWarnings("SameParameterValue")
private static String getRandomBase32String(int length) {
Random random = new Random();
......@@ -134,23 +146,78 @@ class ContactManagerImpl implements ContactManager {
@Override
public void addRemoteContactRequest(String link, String alias) {
// TODO replace with real implementation
PendingContactId id = new PendingContactId(
link.substring(0, PendingContactId.LENGTH).getBytes());
PendingContact pendingContact =
new PendingContact(id, new byte[MAX_PUBLIC_KEY_LENGTH],
alias, WAITING_FOR_CONNECTION, currentTimeMillis());
dbExecutor.execute(() -> {
try {
Thread.sleep(2000);
Thread.sleep(1500);
} 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 {
getLogger("TMP").warning("WAITING_FOR_CONNECTION");
pendingContacts.add(pendingContact);
Event e = new PendingContactStateChangedEvent(id,
WAITING_FOR_CONNECTION);
db.transaction(true, txn -> txn.attach(e));
} catch (DbException ignored) {
}
});
scheduler.schedule(() -> dbExecutor.execute(() -> {
getLogger("TMP").warning("CONNECTED");
pendingContacts.remove(pendingContact);
PendingContact updated = new PendingContact(id,
pendingContact.getPublicKey(), alias, CONNECTED,
pendingContact.getTimestamp());
pendingContacts.add(updated);
Event e = new PendingContactStateChangedEvent(id, CONNECTED);
try {
db.transaction(true, txn -> txn.attach(e));
} catch (DbException ignored) {
}
}), 20, SECONDS);
scheduler.schedule(() -> dbExecutor.execute(() -> {
getLogger("TMP").warning("ADDING_CONTACT");
pendingContacts.remove(pendingContact);
PendingContact updated = new PendingContact(id,
pendingContact.getPublicKey(), alias, ADDING_CONTACT,
pendingContact.getTimestamp());
pendingContacts.add(updated);
Event e =
new PendingContactStateChangedEvent(id, ADDING_CONTACT);
try {
db.transaction(true, txn -> txn.attach(e));
} catch (DbException ignored) {
}
}), 40, SECONDS);
scheduler.schedule(() -> dbExecutor.execute(() -> {
pendingContacts.remove(pendingContact);
Event e;
try {
if (true || new Random().nextBoolean()) {
getLogger("TMP").warning("FAILED");
e = new PendingContactStateChangedEvent(id, FAILED);
PendingContact updated = new PendingContact(id,
pendingContact.getPublicKey(), alias, FAILED,
pendingContact.getTimestamp());
pendingContacts.add(updated);
} else {
getLogger("TMP").warning("ADDED");
ContactId cid = new ContactId(Integer.MAX_VALUE);
AuthorId aid = identityManager.getLocalAuthor().getId();
Contact c = new Contact(cid, null, aid, alias,
new byte[MAX_PUBLIC_KEY_LENGTH], true);
e = new ContactAddedRemotelyEvent(c);
}
db.transaction(true, txn -> txn.attach(e));
} catch (DbException ignored) {
}
}), 60, SECONDS);
}
@Override
......
......@@ -24,6 +24,7 @@ import org.junit.Test;
import java.util.Collection;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
......@@ -59,8 +60,10 @@ public class ContactManagerImplTest extends BrambleMockTestCase {
public ContactManagerImplTest() {
Executor dbExecutor = new ImmediateExecutor();
ScheduledExecutorService scheduler =
context.mock(ScheduledExecutorService.class);
contactManager = new ContactManagerImpl(db, dbExecutor, keyManager,
identityManager);
identityManager, scheduler);
}
@Test
......
......@@ -13,17 +13,12 @@ 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;
......
package org.briarproject.briar.android.contact.add.remote;
import org.briarproject.bramble.api.contact.PendingContact;
interface PendingContactListener {
void onFailedPendingContactRemoved(PendingContact pendingContact);
}
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.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;
......@@ -28,13 +23,12 @@ import javax.inject.Inject;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class PendingRequestsActivity extends BriarActivity
implements EventListener {
implements PendingContactListener {
@Inject
ContactManager contactManager;
@Inject
EventBus eventBus;
ViewModelProvider.Factory viewModelFactory;
private PendingRequestsViewModel viewModel;
private PendingRequestsAdapter adapter;
private BriarRecyclerView list;
......@@ -54,33 +48,29 @@ public class PendingRequestsActivity extends BriarActivity
ab.setDisplayHomeAsUpEnabled(true);
}
adapter = new PendingRequestsAdapter(this, PendingContact.class);
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(PendingRequestsViewModel.class);
viewModel.getPendingContacts()
.observe(this, this::onPendingContactsChanged);
adapter = new PendingRequestsAdapter(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();
eventBus.addListener(this);
list.startPeriodicUpdate();
runOnDbThread(() -> {
try {
Collection<PendingContact> contacts =
contactManager.getPendingContacts();
addPendingContacts(contacts);
} catch (DbException e) {
e.printStackTrace();
}
});
}
@Override
protected void onStop() {
super.onStop();
list.stopPeriodicUpdate();
adapter.clear();
}
@Override
......@@ -95,31 +85,23 @@ public class PendingRequestsActivity extends BriarActivity
}
@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();
}
});
}
public void onFailedPendingContactRemoved(PendingContact pendingContact) {
adapter.remove(pendingContact);
viewModel.removePendingContact(pendingContact);
}
private void addPendingContacts(Collection<PendingContact> contacts) {
runOnUiThreadUnlessDestroyed(() -> {
if (contacts.isEmpty()) {
list.showData();
private void onPendingContactsChanged(Collection<PendingContact> contacts) {
if (contacts.isEmpty()) {
if (!adapter.isEmpty()) {
// all previous contacts have been removed, so we can finish
supportFinishAfterTransition();
} else {
adapter.addAll(contacts);
adapter.clear();
list.showData();
}
});
} else {
adapter.setItems(contacts);
}
}
}
......@@ -5,7 +5,6 @@ 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;
......@@ -15,8 +14,12 @@ import org.briarproject.briar.android.util.BriarAdapter;
public class PendingRequestsAdapter extends
BriarAdapter<PendingContact, PendingRequestsViewHolder> {
public PendingRequestsAdapter(Context ctx, Class<PendingContact> c) {
private final PendingContactListener listener;
public PendingRequestsAdapter(Context ctx, PendingContactListener listener,
Class<PendingContact> c) {
super(ctx, c);
this.listener = listener;
}
@Override
......@@ -24,7 +27,7 @@ public class PendingRequestsAdapter extends
ViewGroup viewGroup, int i) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate(
R.layout.list_item_pending_contact, viewGroup, false);
return new PendingRequestsViewHolder(v);
return new PendingRequestsViewHolder(v, listener);
}
@Override
......@@ -52,14 +55,4 @@ public class PendingRequestsAdapter extends
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;
}
}
}
}
......@@ -3,6 +3,7 @@ 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;
......@@ -10,33 +11,41 @@ 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 android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.briar.android.util.UiUtils.formatDate;
@NotNullByDefault
public class PendingRequestsViewHolder 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 button;
public PendingRequestsViewHolder(View v) {
public PendingRequestsViewHolder(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);
button = v.findViewById(R.id.button);
this.listener = listener;
}
public void bind(PendingContact item) {
avatar.setText(item.getAlias());
avatar.setBackgroundBytes(toUtf8(item.getAlias() + item.getTimestamp()));
avatar.setBackgroundBytes(item.getId().getBytes());
name.setText(item.getAlias());
time.setText(formatDate(time.getContext(), item.getTimestamp()));
button.setOnClickListener(
v -> listener.onFailedPendingContactRemoved(item));
int color = ContextCompat
.getColor(status.getContext(), R.color.briar_green);
int buttonVisibility = GONE;
switch (item.getState()) {
case WAITING_FOR_CONNECTION:
color = ContextCompat
......@@ -50,15 +59,16 @@ public class PendingRequestsViewHolder extends ViewHolder {
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);
buttonVisibility = VISIBLE;
break;
default:
throw new IllegalStateException();
}
status.setTextColor(color);
button.setVisibility(buttonVisibility);
}
}
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.event.ContactAddedRemotelyEvent;
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 PendingRequestsViewModel extends AndroidViewModel
implements EventListener {
private final Logger LOG =
getLogger(PendingRequestsViewModel.class.getName());
@DatabaseExecutor
private final Executor dbExecutor;
private final ContactManager contactManager;
private final EventBus eventBus;
private final MutableLiveData<Collection<PendingContact>> pendingContacts =
new MutableLiveData<>();
@Inject
public PendingRequestsViewModel(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) {
loadPendingContacts();
}
}
private void loadPendingContacts() {
dbExecutor.execute(() -> {
try {
pendingContacts.postValue(contactManager.getPendingContacts());
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
LiveData<Collection<PendingContact>> getPendingContacts() {
return pendingContacts;
}
void removePendingContact(PendingContact pendingContact) {
dbExecutor.execute(() -> {
try {
contactManager.removePendingContact(pendingContact);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
loadPendingContacts();
}
}
......@@ -4,6 +4,7 @@ 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.PendingRequestsViewModel;
import org.briarproject.briar.android.conversation.ConversationViewModel;
import org.briarproject.briar.android.conversation.ImageViewModel;
......@@ -34,6 +35,12 @@ public abstract class ViewModelModule {
abstract ViewModel bindAddContactViewModel(
AddContactViewModel addContactViewModel);
@Binds
@IntoMap
@ViewModelKey(PendingRequestsViewModel.class)
abstract ViewModel bindPendingRequestsViewModel(
PendingRequestsViewModel pendingRequestsViewModel);
@Binds
@Singleton
abstract ViewModelProvider.Factory bindViewModelFactory(
......
......@@ -147,6 +147,7 @@
android:text="@string/share_button"
app:layout_constraintBottom_toBottomOf="@id/copyButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@id/copyButton"
app:layout_constraintTop_toTopOf="@id/copyButton"/>
......
......@@ -11,11 +11,10 @@
android:id="@+id/avatar"
android:layout_width="@dimen/listitem_picture_frame_size"
android:layout_height="@dimen/listitem_picture_frame_size"
android:layout_marginBottom="8dp"
android:layout_marginBottom="@dimen/listitem_horizontal_margin"
android:layout_marginLeft="@dimen/listitem_horizontal_margin"
android:layout_marginStart="@dimen/listitem_horizontal_margin"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toTopOf="@+id/divider"