UI for introducing two contacts to each other

Show system notification for successful introductions
This commit is contained in:
Torsten Grote
2016-01-19 13:28:59 -02:00
parent 7c687736df
commit 5ea7ff2857
65 changed files with 2398 additions and 271 deletions

View File

@@ -12,6 +12,9 @@ import org.briarproject.android.forum.ReadForumPostActivity;
import org.briarproject.android.forum.ShareForumActivity;
import org.briarproject.android.forum.WriteForumPostActivity;
import org.briarproject.android.identity.CreateIdentityActivity;
import org.briarproject.android.introduction.ContactChooserFragment;
import org.briarproject.android.introduction.IntroductionActivity;
import org.briarproject.android.introduction.IntroductionMessageFragment;
import org.briarproject.android.invitation.AddContactActivity;
import org.briarproject.android.keyagreement.ChooseIdentityFragment;
import org.briarproject.android.keyagreement.KeyAgreementActivity;
@@ -80,6 +83,12 @@ public interface AndroidComponent extends CoreEagerSingletons {
void inject(ShowQrCodeFragment fragment);
void inject(IntroductionActivity activity);
void inject(ContactChooserFragment fragment);
void inject(IntroductionMessageFragment fragment);
// Eager singleton load
void inject(AppModule.EagerSingletons init);
}

View File

@@ -14,10 +14,14 @@ import org.briarproject.android.api.AndroidExecutor;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.contact.ConversationActivity;
import org.briarproject.android.forum.ForumActivity;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.db.DatabaseExecutor;
import org.briarproject.api.db.DbException;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.IntroductionRequestReceivedEvent;
import org.briarproject.api.event.IntroductionResponseReceivedEvent;
import org.briarproject.api.event.IntroductionSucceededEvent;
import org.briarproject.api.event.MessageValidatedEvent;
import org.briarproject.api.event.SettingsUpdatedEvent;
import org.briarproject.api.forum.ForumManager;
@@ -57,6 +61,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
private static final int PRIVATE_MESSAGE_NOTIFICATION_ID = 3;
private static final int FORUM_POST_NOTIFICATION_ID = 4;
private static final int INTRODUCTION_SUCCESS_NOTIFICATION_ID = 5;
private static final String CONTACT_URI =
"content://org.briarproject/contact";
private static final String FORUM_URI =
@@ -111,6 +116,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
public Void call() {
clearPrivateMessageNotification();
clearForumPostNotification();
clearIntroductionSuccessNotification();
return null;
}
});
@@ -135,6 +141,12 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
nm.cancel(FORUM_POST_NOTIFICATION_ID);
}
private void clearIntroductionSuccessNotification() {
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.cancel(INTRODUCTION_SUCCESS_NOTIFICATION_ID);
}
public void eventOccurred(Event e) {
if (e instanceof SettingsUpdatedEvent) {
SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
@@ -148,6 +160,25 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
else if (c.equals(forumManager.getClientId()))
showForumPostNotification(m.getMessage().getGroupId());
}
} else if (e instanceof IntroductionRequestReceivedEvent) {
try {
GroupId group = messagingManager.getConversationId(
((IntroductionRequestReceivedEvent) e).getContactId());
showPrivateMessageNotification(group);
} catch (DbException ex) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, ex.toString(), ex);
}
} else if (e instanceof IntroductionResponseReceivedEvent) {
try {
GroupId group = messagingManager.getConversationId(
((IntroductionResponseReceivedEvent) e).getContactId());
showPrivateMessageNotification(group);
} catch (DbException ex) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, ex.toString(), ex);
}
} else if (e instanceof IntroductionSucceededEvent) {
Contact c = ((IntroductionSucceededEvent) e).getContact();
showIntroductionSucceededNotification(c);
}
}
@@ -335,4 +366,35 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
}
});
}
private void showIntroductionSucceededNotification(final Contact c) {
androidExecutor.execute(new Runnable() {
public void run() {
NotificationCompat.Builder b =
new NotificationCompat.Builder(appContext);
b.setSmallIcon(R.drawable.introduction_notification);
b.setContentTitle(appContext
.getString(R.string.introduction_success_title));
b.setContentText(appContext
.getString(R.string.introduction_success_text,
c.getAuthor().getName()));
b.setDefaults(getDefaults());
b.setAutoCancel(true);
Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(NavDrawerActivity.INTENT_CONTACTS, true);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(NavDrawerActivity.class);
t.addNextIntent(i);
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.notify(INTRODUCTION_SUCCESS_NOTIFICATION_ID, b.build());
}
});
}
}

View File

@@ -54,7 +54,7 @@ public abstract class BaseActivity extends AppCompatActivity {
((InputMethodManager) o).showSoftInput(view, SHOW_IMPLICIT);
}
protected void hideSoftKeyboard(View view) {
public void hideSoftKeyboard(View view) {
IBinder token = view.getWindowToken();
Object o = getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).hideSoftInputFromWindow(token, 0);

View File

@@ -1,8 +1,12 @@
package org.briarproject.android.contact;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.os.Build;
import android.support.v4.content.ContextCompat;
import android.support.v7.util.SortedList;
import android.support.v7.widget.RecyclerView;
import android.text.format.DateUtils;
@@ -14,9 +18,8 @@ import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.identity.Author;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.identity.AuthorId;
import java.util.List;
@@ -53,13 +56,23 @@ public class ContactListAdapter
@Override
public int compare(ContactListItem c1,
ContactListItem c2) {
// sort items by time
// and do not take unread messages into account
long time1 = c1.getTimestamp();
long time2 = c2.getTimestamp();
if (time1 < time2) return 1;
if (time1 > time2) return -1;
return 0;
int authorCompare = 0;
if (chooser) {
authorCompare = c1.getLocalAuthor().getName()
.compareTo(
c2.getLocalAuthor().getName());
}
if (authorCompare == 0) {
// sort items by time
// and do not take unread messages into account
long time1 = c1.getTimestamp();
long time2 = c2.getTimestamp();
if (time1 < time2) return 1;
if (time1 > time2) return -1;
return 0;
} else {
return authorCompare;
}
}
@Override
@@ -86,10 +99,16 @@ public class ContactListAdapter
return true;
}
});
private final OnItemClickListener listener;
private final boolean chooser;
private Context ctx;
private AuthorId localAuthorId;
public ContactListAdapter(Context context) {
public ContactListAdapter(Context context, OnItemClickListener listener,
boolean chooser) {
ctx = context;
this.listener = listener;
this.chooser = chooser;
}
@Override
@@ -103,12 +122,11 @@ public class ContactListAdapter
@Override
public void onBindViewHolder(final ContactHolder ui, final int position) {
final ContactListItem item = getItem(position);
Resources res = ctx.getResources();
int unread = item.getUnreadCount();
if (unread > 0) {
if (!chooser && unread > 0) {
ui.layout.setBackgroundColor(
res.getColor(R.color.unread_background));
ContextCompat.getColor(ctx, R.color.unread_background));
}
if (item.isConnected()) {
@@ -121,27 +139,37 @@ public class ContactListAdapter
ui.avatar.setImageDrawable(
new IdenticonDrawable(author.getId().getBytes()));
String contactName = author.getName();
if (unread > 0) {
if (!chooser && unread > 0) {
// TODO show these in a bubble on top of the avatar
ui.name.setText(contactName + " (" + unread + ")");
} else {
ui.name.setText(contactName);
}
if (chooser) {
ui.identity.setText(item.getLocalAuthor().getName());
} else {
ui.identity.setVisibility(View.GONE);
}
if (item.isEmpty()) {
ui.date.setText(R.string.no_private_messages);
} else {
// TODO show this as X units ago
long timestamp = item.getTimestamp();
ui.date.setText(
DateUtils.getRelativeTimeSpanString(ctx, timestamp));
}
if (chooser && !item.getLocalAuthor().getId().equals(localAuthorId)) {
grayOutItem(ui);
}
ui.layout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
GroupId groupId = item.getGroupId();
Intent i = new Intent(ctx, ConversationActivity.class);
i.putExtra("briar.GROUP_ID", groupId.getBytes());
ctx.startActivity(i);
listener.onItemClick(ui.avatar, item);
}
});
}
@@ -151,6 +179,34 @@ public class ContactListAdapter
return contacts.size();
}
/**
* Set the identity from whose perspective the contact shall be chosen.
* This is only used if chooser is true.
* @param authorId The ID of the local Author
*/
public void setLocalAuthor(AuthorId authorId) {
localAuthorId = authorId;
notifyDataSetChanged();
}
private void grayOutItem(final ContactHolder ui) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
float alpha = 0.25f;
ui.bulb.setAlpha(alpha);
ui.avatar.setAlpha(alpha);
ui.name.setAlpha(alpha);
ui.date.setAlpha(alpha);
ui.identity.setAlpha(alpha);
} else {
ColorFilter colorFilter = new PorterDuffColorFilter(Color.GRAY,
PorterDuff.Mode.MULTIPLY);
ui.bulb.setColorFilter(colorFilter);
ui.avatar.setColorFilter(colorFilter);
ui.name.setEnabled(false);
ui.date.setEnabled(false);
}
}
public ContactListItem getItem(int position) {
if (position == INVALID_POSITION || contacts.size() <= position) {
return null; // Not found
@@ -162,10 +218,6 @@ public class ContactListAdapter
contacts.updateItemAt(position, item);
}
public int findItemPosition(ContactListItem item) {
return contacts.indexOf(item);
}
public int findItemPosition(ContactId c) {
int count = getItemCount();
for (int i = 0; i < count; i++) {
@@ -202,6 +254,7 @@ public class ContactListAdapter
public ImageView bulb;
public ImageView avatar;
public TextView name;
public TextView identity;
public TextView date;
public ContactHolder(View v) {
@@ -211,7 +264,13 @@ public class ContactListAdapter
bulb = (ImageView) v.findViewById(R.id.bulbView);
avatar = (ImageView) v.findViewById(R.id.avatarView);
name = (TextView) v.findViewById(R.id.nameView);
identity = (TextView) v.findViewById(R.id.identityView);
date = (TextView) v.findViewById(R.id.dateView);
}
}
public interface OnItemClickListener {
void onItemClick(View view, ContactListItem item);
}
}

View File

@@ -1,9 +1,12 @@
package org.briarproject.android.contact;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.view.LayoutInflater;
import android.view.View;
@@ -11,23 +14,26 @@ import android.view.ViewGroup;
import org.briarproject.R;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.BriarApplication;
import org.briarproject.android.fragment.BaseEventFragment;
import org.briarproject.android.keyagreement.KeyAgreementActivity;
import org.briarproject.android.util.BriarRecyclerView;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.NoSuchContactException;
import org.briarproject.api.event.ContactAddedEvent;
import org.briarproject.api.event.ContactConnectedEvent;
import org.briarproject.api.event.ContactDisconnectedEvent;
import org.briarproject.api.event.ContactRemovedEvent;
import org.briarproject.api.event.ContactStatusChangedEvent;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.MessageValidatedEvent;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.introduction.IntroductionManager;
import org.briarproject.api.introduction.IntroductionMessage;
import org.briarproject.api.messaging.MessagingManager;
import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.plugins.ConnectionRegistry;
@@ -74,8 +80,12 @@ public class ContactListFragment extends BaseEventFragment {
@Inject
protected volatile ContactManager contactManager;
@Inject
protected volatile IdentityManager identityManager;
@Inject
protected volatile MessagingManager messagingManager;
@Inject
protected volatile IntroductionManager introductionManager;
@Inject
protected volatile EventBus eventBus;
@Override
@@ -91,7 +101,31 @@ public class ContactListFragment extends BaseEventFragment {
inflater.inflate(R.layout.activity_contact_list, container,
false);
adapter = new ContactListAdapter(getContext());
ContactListAdapter.OnItemClickListener onItemClickListener =
new ContactListAdapter.OnItemClickListener() {
@Override
public void onItemClick(View view, ContactListItem item) {
GroupId groupId = item.getGroupId();
Intent i = new Intent(getActivity(),
ConversationActivity.class);
i.putExtra("briar.GROUP_ID", groupId.getBytes());
if (Build.VERSION.SDK_INT >= 16) {
ActivityOptionsCompat options =
ActivityOptionsCompat.
makeSceneTransitionAnimation(
getActivity(),
view, "avatar");
getActivity().startActivity(i, options.toBundle());
} else {
startActivity(i);
}
}
};
adapter = new ContactListAdapter(getContext(), onItemClickListener,
false);
list = (BriarRecyclerView) contentView.findViewById(R.id.contactList);
list.setLayoutManager(new LinearLayoutManager(getContext()));
list.setAdapter(adapter);
@@ -135,12 +169,14 @@ public class ContactListFragment extends BaseEventFragment {
ContactId id = c.getId();
GroupId groupId =
messagingManager.getConversationId(id);
Collection<PrivateMessageHeader> headers =
messagingManager.getMessageHeaders(id);
Collection<ConversationItem> messages =
getMessages(id);
boolean connected =
connectionRegistry.isConnected(c.getId());
contacts.add(new ContactListItem(c, connected,
groupId, headers));
LocalAuthor localAuthor = identityManager
.getLocalAuthor(c.getLocalAuthorId());
contacts.add(new ContactListItem(c, localAuthor,
connected, groupId, messages));
} catch (NoSuchContactException e) {
// Continue
}
@@ -169,7 +205,12 @@ public class ContactListFragment extends BaseEventFragment {
public void eventOccurred(Event e) {
if (e instanceof ContactAddedEvent) {
LOG.info("Contact added, reloading");
if(((ContactAddedEvent) e).isActive()) {
LOG.info("Contact added as active, reloading");
loadContacts();
}
} else if (e instanceof ContactStatusChangedEvent) {
LOG.info("Contact Status changed, reloading");
loadContacts();
} else if (e instanceof ContactConnectedEvent) {
setConnected(((ContactConnectedEvent) e).getContactId(), true);
@@ -181,7 +222,8 @@ public class ContactListFragment extends BaseEventFragment {
} else if (e instanceof MessageValidatedEvent) {
MessageValidatedEvent m = (MessageValidatedEvent) e;
ClientId c = m.getClientId();
if (m.isValid() && c.equals(messagingManager.getClientId())) {
if (m.isValid() && (c.equals(messagingManager.getClientId()) ||
c.equals(introductionManager.getClientId()))) {
LOG.info("Message added, reloading");
reloadConversation(m.getMessage().getGroupId());
}
@@ -192,14 +234,10 @@ public class ContactListFragment extends BaseEventFragment {
listener.runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
ContactId c = messagingManager.getContactId(g);
Collection<PrivateMessageHeader> headers =
messagingManager.getMessageHeaders(c);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Partial load took " + duration + " ms");
updateItem(c, headers);
Collection<ConversationItem> messages =
getMessages(c);
updateItem(c, messages);
} catch (NoSuchContactException e) {
LOG.info("Contact removed");
} catch (DbException e) {
@@ -211,13 +249,13 @@ public class ContactListFragment extends BaseEventFragment {
}
private void updateItem(final ContactId c,
final Collection<PrivateMessageHeader> headers) {
final Collection<ConversationItem> messages) {
listener.runOnUiThread(new Runnable() {
public void run() {
int position = adapter.findItemPosition(c);
ContactListItem item = adapter.getItem(position);
if (item != null) {
item.setHeaders(headers);
item.setMessages(messages);
adapter.updateItem(position, item);
}
}
@@ -246,4 +284,35 @@ public class ContactListFragment extends BaseEventFragment {
}
});
}
/** This needs to be called from the DbThread */
private Collection<ConversationItem> getMessages(ContactId id)
throws DbException {
long now = System.currentTimeMillis();
Collection<ConversationItem> messages =
new ArrayList<ConversationItem>();
Collection<PrivateMessageHeader> headers =
messagingManager.getMessageHeaders(id);
for (PrivateMessageHeader h : headers) {
messages.add(new ConversationMessageItem(h));
}
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading message headers took " + duration + " ms");
Collection<IntroductionMessage> introductions =
introductionManager
.getIntroductionMessages(id);
for (IntroductionMessage m : introductions) {
messages.add(ConversationItem.from(m));
}
duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading introduction messages took " + duration + " ms");
return messages;
}
}

View File

@@ -1,44 +1,55 @@
package org.briarproject.android.contact;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sync.GroupId;
import java.util.Collection;
import static org.briarproject.android.contact.ConversationItem.IncomingItem;
// This class is not thread-safe
class ContactListItem {
public class ContactListItem {
private final Contact contact;
private final LocalAuthor localAuthor;
private final GroupId groupId;
private boolean connected, empty;
private long timestamp;
private int unread;
ContactListItem(Contact contact, boolean connected, GroupId groupId,
Collection<PrivateMessageHeader> headers) {
public ContactListItem(Contact contact, LocalAuthor localAuthor,
boolean connected,
GroupId groupId,
Collection<ConversationItem> messages) {
this.contact = contact;
this.localAuthor = localAuthor;
this.groupId = groupId;
this.connected = connected;
setHeaders(headers);
setMessages(messages);
}
void setHeaders(Collection<PrivateMessageHeader> headers) {
empty = headers.isEmpty();
void setMessages(Collection<ConversationItem> messages) {
empty = messages.isEmpty();
timestamp = 0;
unread = 0;
if (!empty) {
for (PrivateMessageHeader h : headers) {
if (h.getTimestamp() > timestamp) timestamp = h.getTimestamp();
if (!h.isRead()) unread++;
for (ConversationItem i : messages) {
if (i.getTime() > timestamp) timestamp = i.getTime();
if (i instanceof IncomingItem && !((IncomingItem) i).isRead())
unread++;
}
}
}
Contact getContact() {
public Contact getContact() {
return contact;
}
public LocalAuthor getLocalAuthor() {
return localAuthor;
}
GroupId getGroupId() {
return groupId;
}

View File

@@ -2,11 +2,15 @@ package org.briarproject.android.contact;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.Toolbar;
import android.util.SparseArray;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -14,14 +18,17 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.R;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.BriarActivity;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.introduction.IntroductionActivity;
import org.briarproject.android.util.BriarRecyclerView;
import org.briarproject.api.FormatException;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager;
@@ -35,9 +42,16 @@ import org.briarproject.api.event.ContactRemovedEvent;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.IntroductionRequestReceivedEvent;
import org.briarproject.api.event.IntroductionResponseReceivedEvent;
import org.briarproject.api.event.MessageValidatedEvent;
import org.briarproject.api.event.MessagesAckedEvent;
import org.briarproject.api.event.MessagesSentEvent;
import org.briarproject.api.introduction.IntroductionManager;
import org.briarproject.api.introduction.IntroductionMessage;
import org.briarproject.api.introduction.IntroductionRequest;
import org.briarproject.api.introduction.IntroductionResponse;
import org.briarproject.api.introduction.SessionId;
import org.briarproject.api.messaging.MessagingManager;
import org.briarproject.api.messaging.PrivateMessage;
import org.briarproject.api.messaging.PrivateMessageFactory;
@@ -61,21 +75,31 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import de.hdodenhof.circleimageview.CircleImageView;
import im.delight.android.identicons.IdenticonDrawable;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.contact.ConversationItem.OutgoingItem;
import static org.briarproject.android.contact.ConversationItem.IncomingItem;
public class ConversationActivity extends BriarActivity
implements EventListener, OnClickListener {
implements EventListener, OnClickListener,
ConversationAdapter.IntroductionHandler {
private static final Logger LOG =
Logger.getLogger(ConversationActivity.class.getName());
private static final int INTRODUCTION_REQUEST_CODE = 0;
@Inject protected AndroidNotificationManager notificationManager;
@Inject protected ConnectionRegistry connectionRegistry;
@Inject @CryptoExecutor protected Executor cryptoExecutor;
private Map<MessageId, byte[]> bodyCache = new HashMap<MessageId, byte[]>();
private ConversationAdapter adapter = null;
private CircleImageView toolbarAvatar;
private ImageView toolbarStatus;
private TextView toolbarTitle;
private BriarRecyclerView list = null;
private EditText content = null;
private ImageButton sendButton = null;
@@ -85,6 +109,7 @@ public class ConversationActivity extends BriarActivity
@Inject protected volatile MessagingManager messagingManager;
@Inject protected volatile EventBus eventBus;
@Inject protected volatile PrivateMessageFactory privateMessageFactory;
@Inject protected volatile IntroductionManager introductionManager;
private volatile GroupId groupId = null;
private volatile ContactId contactId = null;
private volatile String contactName = null;
@@ -102,7 +127,21 @@ public class ConversationActivity extends BriarActivity
setContentView(R.layout.activity_conversation);
adapter = new ConversationAdapter(this);
// Custom Toolbar
final Toolbar tb = (Toolbar) findViewById(R.id.toolbar);
toolbarAvatar = (CircleImageView) tb.findViewById(R.id.contactAvatar);
toolbarStatus = (ImageView) tb.findViewById(R.id.contactStatus);
toolbarTitle = (TextView) tb.findViewById(R.id.contactName);
setSupportActionBar(tb);
final ActionBar ab = getSupportActionBar();
if (ab != null) {
ab.setDisplayShowHomeEnabled(true);
ab.setDisplayHomeAsUpEnabled(true);
ab.setDisplayShowCustomEnabled(true);
ab.setDisplayShowTitleEnabled(false);
}
adapter = new ConversationAdapter(this, this);
list = (BriarRecyclerView) findViewById(R.id.conversationView);
list.setLayoutManager(new LinearLayoutManager(this));
list.setAdapter(adapter);
@@ -125,8 +164,7 @@ public class ConversationActivity extends BriarActivity
eventBus.addListener(this);
notificationManager.blockNotification(groupId);
notificationManager.clearPrivateMessageNotification(groupId);
loadContactDetails();
loadHeaders();
loadData();
}
@Override
@@ -141,12 +179,10 @@ public class ConversationActivity extends BriarActivity
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu items for use in the action bar
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.contact_actions, menu);
inflater.inflate(R.menu.conversation_actions, menu);
// Adapt icon color to dark action bar
menu.findItem(R.id.action_social_remove_person).getIcon().setColorFilter(
getResources().getColor(R.color.action_bar_text),
PorterDuff.Mode.SRC_IN);
hideIntroductionActionWhenOneContact(
menu.findItem(R.id.action_introduction));
return super.onCreateOptionsMenu(menu);
}
@@ -155,6 +191,20 @@ public class ConversationActivity extends BriarActivity
public boolean onOptionsItemSelected(final MenuItem item) {
// Handle presses on the action bar items
switch (item.getItemId()) {
case android.R.id.home:
supportFinishAfterTransition();
return true;
case R.id.action_introduction:
if (contactId == null) return false;
Intent intent = new Intent(this, IntroductionActivity.class);
intent.putExtra(IntroductionActivity.CONTACT_ID,
contactId.getInt());
ActivityOptionsCompat options = ActivityOptionsCompat
.makeCustomAnimation(this, android.R.anim.slide_in_left,
android.R.anim.slide_out_right);
ActivityCompat.startActivityForResult(this, intent,
INTRODUCTION_REQUEST_CODE, options.toBundle());
return true;
case R.id.action_social_remove_person:
askToRemoveContact();
return true;
@@ -163,20 +213,37 @@ public class ConversationActivity extends BriarActivity
}
}
private void loadContactDetails() {
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
if (requestCode == INTRODUCTION_REQUEST_CODE) {
if (resultCode == RESULT_OK) {
loadData();
}
}
}
private void loadData() {
runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
contactId = messagingManager.getContactId(groupId);
Contact contact = contactManager.getContact(contactId);
contactName = contact.getAuthor().getName();
contactIdenticonKey = contact.getAuthor().getId().getBytes();
if (contactId == null)
contactId = messagingManager.getContactId(groupId);
if (contactName == null || contactIdenticonKey == null) {
Contact contact = contactManager.getContact(contactId);
contactName = contact.getAuthor().getName();
contactIdenticonKey =
contact.getAuthor().getId().getBytes();
}
connected = connectionRegistry.isConnected(contactId);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading contact took " + duration + " ms");
displayContactDetails();
// Load the messages here to make sure we have a contactId
loadMessages();
} catch (NoSuchContactException e) {
finishOnUiThread();
} catch (DbException e) {
@@ -190,31 +257,42 @@ public class ConversationActivity extends BriarActivity
private void displayContactDetails() {
runOnUiThread(new Runnable() {
public void run() {
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(contactName);
if (connected) {
actionBar.setSubtitle(getString(R.string.online));
} else {
actionBar.setSubtitle(getString(R.string.offline));
}
toolbarAvatar.setImageDrawable(
new IdenticonDrawable(contactIdenticonKey));
toolbarTitle.setText(contactName);
if (connected) {
toolbarStatus.setImageDrawable(ContextCompat
.getDrawable(ConversationActivity.this,
R.drawable.contact_online));
toolbarStatus
.setContentDescription(getString(R.string.online));
} else {
toolbarStatus.setImageDrawable(ContextCompat
.getDrawable(ConversationActivity.this,
R.drawable.contact_offline));
toolbarStatus
.setContentDescription(getString(R.string.offline));
}
adapter.setIdenticonKey(contactIdenticonKey);
adapter.setIdenticonKey(contactIdenticonKey, contactName);
}
});
}
private void loadHeaders() {
private void loadMessages() {
runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
Collection<PrivateMessageHeader> headers =
messagingManager.getMessageHeaders(contactId);
Collection<IntroductionMessage> introductions =
introductionManager
.getIntroductionMessages(contactId);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading headers took " + duration + " ms");
displayHeaders(headers);
displayMessages(headers, introductions);
} catch (NoSuchContactException e) {
finishOnUiThread();
} catch (DbException e) {
@@ -225,23 +303,37 @@ public class ConversationActivity extends BriarActivity
});
}
private void displayHeaders(
final Collection<PrivateMessageHeader> headers) {
private void displayMessages(final Collection<PrivateMessageHeader> headers,
final Collection<IntroductionMessage> introductions) {
runOnUiThread(new Runnable() {
public void run() {
sendButton.setEnabled(true);
if (headers.isEmpty()) {
if (headers.isEmpty() && introductions.isEmpty()) {
// we have no messages,
// so let the list know to hide progress bar
list.showData();
} else {
for (PrivateMessageHeader h : headers) {
ConversationItem item = new ConversationItem(h);
ConversationMessageItem item =
new ConversationMessageItem(h);
byte[] body = bodyCache.get(h.getId());
if (body == null) loadMessageBody(h);
else item.setBody(body);
adapter.add(item);
}
for (IntroductionMessage m : introductions) {
ConversationItem item;
if (m instanceof IntroductionRequest) {
item = ConversationItem
.from((IntroductionRequest) m);
} else {
item = ConversationItem
.from(ConversationActivity.this,
contactName,
(IntroductionResponse) m);
}
adapter.add(item);
}
// Scroll to the bottom
list.scrollToPosition(adapter.getItemCount() - 1);
}
@@ -273,27 +365,42 @@ public class ConversationActivity extends BriarActivity
runOnUiThread(new Runnable() {
public void run() {
bodyCache.put(m, body);
int count = adapter.getItemCount();
for (int i = 0; i < count; i++) {
ConversationItem item = adapter.getItem(i);
if (item.getHeader().getId().equals(m)) {
SparseArray<ConversationMessageItem> messages =
adapter.getPrivateMessages();
for (int i = 0; i < messages.size(); i++) {
ConversationMessageItem item = messages.valueAt(i);
if (item.getId().equals(m)) {
item.setBody(body);
adapter.notifyItemChanged(i);
// Scroll to the bottom
list.scrollToPosition(count - 1);
adapter.notifyItemChanged(messages.keyAt(i));
return;
}
}
// Scroll to the bottom
list.scrollToPosition(adapter.getItemCount() - 1);
}
});
}
private void addIntroduction(final ConversationItem item) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (adapter != null) {
adapter.add(item);
// Scroll to the bottom
list.scrollToPosition(adapter.getItemCount() - 1);
}
}
});
}
private void markMessagesRead() {
List<MessageId> unread = new ArrayList<MessageId>();
int count = adapter.getItemCount();
for (int i = 0; i < count; i++) {
PrivateMessageHeader h = adapter.getItem(i).getHeader();
if (!h.isRead()) unread.add(h.getId());
SparseArray<IncomingItem> list =
adapter.getIncomingMessages();
for (int i = 0; i < list.size(); i++) {
IncomingItem item = list.valueAt(i);
if (!item.isRead()) unread.add(item.getId());
}
if (unread.isEmpty()) return;
if (LOG.isLoggable(INFO))
@@ -307,6 +414,8 @@ public class ConversationActivity extends BriarActivity
try {
long now = System.currentTimeMillis();
for (MessageId m : unread)
// not really clean, but the messaging manager can
// handle introduction messages as well
messagingManager.setReadFlag(m, true);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
@@ -331,7 +440,7 @@ public class ConversationActivity extends BriarActivity
if (m.isValid() && m.getMessage().getGroupId().equals(groupId)) {
LOG.info("Message added, reloading");
// Mark new incoming messages as read directly
if (m.isLocal()) loadHeaders();
if (m.isLocal()) loadMessages();
else markMessageReadIfNew(m.getMessage());
}
} else if (e instanceof MessagesSentEvent) {
@@ -360,6 +469,23 @@ public class ConversationActivity extends BriarActivity
connected = false;
displayContactDetails();
}
} else if (e instanceof IntroductionRequestReceivedEvent) {
IntroductionRequestReceivedEvent event =
(IntroductionRequestReceivedEvent) e;
if (event.getContactId().equals(contactId)) {
IntroductionRequest ir = event.getIntroductionRequest();
ConversationItem item = new ConversationIntroductionInItem(ir);
addIntroduction(item);
}
} else if (e instanceof IntroductionResponseReceivedEvent) {
IntroductionResponseReceivedEvent event =
(IntroductionResponseReceivedEvent) e;
if (event.getContactId().equals(contactId)) {
IntroductionResponse ir = event.getIntroductionResponse();
ConversationItem item =
ConversationItem.from(this, contactName, ir);
addIntroduction(item);
}
}
}
@@ -369,10 +495,13 @@ public class ConversationActivity extends BriarActivity
ConversationItem item = adapter.getLastItem();
if (item != null) {
// Mark the message read if it's the newest message
long lastMsgTime = item.getHeader().getTimestamp();
long lastMsgTime = item.getTime();
long newMsgTime = m.getTimestamp();
if (newMsgTime > lastMsgTime) markNewMessageRead(m);
else loadHeaders();
else loadMessages();
} else {
// mark the message as read as well if it is the first one
markNewMessageRead(m);
}
}
});
@@ -383,7 +512,7 @@ public class ConversationActivity extends BriarActivity
public void run() {
try {
messagingManager.setReadFlag(m.getId(), true);
loadHeaders();
loadMessages();
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
@@ -397,13 +526,14 @@ public class ConversationActivity extends BriarActivity
runOnUiThread(new Runnable() {
public void run() {
Set<MessageId> messages = new HashSet<MessageId>(messageIds);
int count = adapter.getItemCount();
for (int i = 0; i < count; i++) {
ConversationItem item = adapter.getItem(i);
if (messages.contains(item.getHeader().getId())) {
SparseArray<OutgoingItem> list =
adapter.getOutgoingMessages();
for (int i = 0; i < list.size(); i++) {
OutgoingItem item = list.valueAt(i);
if (messages.contains(item.getId())) {
item.setSent(sent);
item.setSeen(seen);
adapter.notifyItemChanged(i);
adapter.notifyItemChanged(list.keyAt(i));
}
}
}
@@ -424,7 +554,7 @@ public class ConversationActivity extends BriarActivity
private long getMinTimestampForNewMessage() {
// Don't use an earlier timestamp than the newest message
ConversationItem item = adapter.getLastItem();
return item == null ? 0 : item.getHeader().getTimestamp() + 1;
return item == null ? 0 : item.getTime() + 1;
}
private void createMessage(final byte[] body, final long timestamp) {
@@ -466,7 +596,8 @@ public class ConversationActivity extends BriarActivity
}
};
AlertDialog.Builder builder =
new AlertDialog.Builder(ConversationActivity.this);
new AlertDialog.Builder(ConversationActivity.this,
R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.dialog_title_delete_contact));
builder.setMessage(getString(R.string.dialog_message_delete_contact));
builder.setPositiveButton(android.R.string.ok, okListener);
@@ -500,4 +631,66 @@ public class ConversationActivity extends BriarActivity
}
});
}
private void hideIntroductionActionWhenOneContact(final MenuItem item) {
runOnDbThread(new Runnable() {
public void run() {
try {
if (contactManager.getActiveContacts().size() < 2) {
hideIntroductionAction(item);
}
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void hideIntroductionAction(final MenuItem item) {
runOnUiThread(new Runnable() {
@Override
public void run() {
item.setVisible(false);
}
});
}
@Override
public void respondToIntroduction(final SessionId sessionId, final boolean accept) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
if (accept) {
introductionManager.acceptIntroduction(sessionId);
} else {
introductionManager.declineIntroduction(sessionId);
}
loadMessages();
} catch (DbException e) {
introductionResponseError();
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch (FormatException e) {
introductionResponseError();
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void introductionResponseError() {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(ConversationActivity.this,
R.string.introduction_response_error,
Toast.LENGTH_SHORT).show();
}
});
}
}

View File

@@ -4,29 +4,37 @@ import android.content.Context;
import android.support.v7.util.SortedList;
import android.support.v7.widget.RecyclerView;
import android.text.format.DateUtils;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.introduction.IntroductionRequest;
import org.briarproject.api.introduction.SessionId;
import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.util.StringUtils;
import im.delight.android.identicons.IdenticonDrawable;
import static android.support.v7.util.SortedList.INVALID_POSITION;
import static android.support.v7.widget.RecyclerView.ViewHolder;
import static org.briarproject.android.contact.ConversationItem.INTRODUCTION_IN;
import static org.briarproject.android.contact.ConversationItem.INTRODUCTION_OUT;
import static org.briarproject.android.contact.ConversationItem.MSG_IN;
import static org.briarproject.android.contact.ConversationItem.MSG_IN_UNREAD;
import static org.briarproject.android.contact.ConversationItem.MSG_OUT;
import static org.briarproject.android.contact.ConversationItem.NOTICE_IN;
import static org.briarproject.android.contact.ConversationItem.NOTICE_OUT;
import static org.briarproject.android.contact.ConversationItem.OutgoingItem;
import static org.briarproject.android.contact.ConversationItem.IncomingItem;
class ConversationAdapter extends
RecyclerView.Adapter<ConversationAdapter.MessageHolder> {
class ConversationAdapter extends RecyclerView.Adapter {
private static final int MSG_OUT = 0;
private static final int MSG_IN = 1;
private static final int MSG_IN_UNREAD = 2;
private final SortedList<ConversationItem> messages =
private final SortedList<ConversationItem> items =
new SortedList<ConversationItem>(ConversationItem.class,
new SortedList.Callback<ConversationItem>() {
@Override
@@ -52,8 +60,8 @@ class ConversationAdapter extends
@Override
public int compare(ConversationItem c1,
ConversationItem c2) {
long time1 = c1.getHeader().getTimestamp();
long time2 = c2.getHeader().getTimestamp();
long time1 = c1.getTime();
long time2 = c2.getTime();
if (time1 < time2) return -1;
if (time1 > time2) return 1;
return 0;
@@ -62,8 +70,7 @@ class ConversationAdapter extends
@Override
public boolean areItemsTheSame(ConversationItem c1,
ConversationItem c2) {
return c1.getHeader().getId()
.equals(c2.getHeader().getId());
return c1.getId().equals(c2.getId());
}
@Override
@@ -73,67 +80,113 @@ class ConversationAdapter extends
}
});
private Context ctx;
private IntroductionHandler intro;
private byte[] identiconKey;
private String contactName;
public ConversationAdapter(Context context) {
public ConversationAdapter(Context context,
IntroductionHandler introductionHandler) {
ctx = context;
intro = introductionHandler;
setHasStableIds(true);
}
public void setIdenticonKey(byte[] key) {
public void setIdenticonKey(byte[] key, String contactName) {
this.identiconKey = key;
this.contactName = contactName;
// FIXME this breaks the progress animation because it is called early before data is loaded
notifyDataSetChanged();
}
@Override
public int getItemViewType(int position) {
// return different type for incoming and outgoing (local) messages
PrivateMessageHeader header = getItem(position).getHeader();
if (header.isLocal()) {
return MSG_OUT;
} else if (header.isRead()) {
return MSG_IN;
} else {
return MSG_IN_UNREAD;
}
public long getItemId(int position) {
return getItem(position).getId().hashCode();
}
@Override
public MessageHolder onCreateViewHolder(ViewGroup viewGroup, int type) {
public int getItemViewType(int position) {
return getItem(position).getType();
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) {
View v;
// outgoing message (local)
if (type == MSG_OUT) {
v = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.list_item_msg_out, viewGroup, false);
return new MessageHolder(v, type);
}
else if (type == INTRODUCTION_IN) {
v = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.list_item_introduction_in, viewGroup, false);
return new IntroductionHolder(v, type);
}
else if (type == INTRODUCTION_OUT) {
v = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.list_item_introduction_out, viewGroup, false);
return new IntroductionHolder(v, type);
}
else if (type == NOTICE_IN) {
v = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.list_item_notice_in, viewGroup, false);
return new NoticeHolder(v, type);
}
else if (type == NOTICE_OUT) {
v = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.list_item_notice_out, viewGroup, false);
return new NoticeHolder(v, type);
}
// incoming message (non-local)
else {
v = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.list_item_msg_in, viewGroup, false);
return new MessageHolder(v, type);
}
return new MessageHolder(v, type);
}
@Override
public void onBindViewHolder(final MessageHolder ui, final int position) {
public void onBindViewHolder(final ViewHolder ui, final int position) {
ConversationItem item = getItem(position);
if (item instanceof ConversationMessageItem) {
bindMessage((MessageHolder) ui, (ConversationMessageItem) item,
position);
} else if (item instanceof ConversationIntroductionOutItem) {
bindIntroduction((IntroductionHolder) ui,
(ConversationIntroductionOutItem) item, position);
} else if (item instanceof ConversationIntroductionInItem) {
bindIntroduction((IntroductionHolder) ui,
(ConversationIntroductionInItem) item, position);
} else if (item instanceof ConversationNoticeOutItem) {
bindNotice((NoticeHolder) ui, (ConversationNoticeOutItem) item,
position);
} else if (item instanceof ConversationNoticeInItem) {
bindNotice((NoticeHolder) ui, (ConversationNoticeInItem) item,
position);
} else {
throw new IllegalArgumentException("Unhandled Conversation Item");
}
}
private void bindMessage(final MessageHolder ui,
ConversationMessageItem item, final int position) {
PrivateMessageHeader header = item.getHeader();
if (header.isLocal()) {
if (item.getType() == MSG_OUT) {
if (item.isSeen()) {
ui.status.setImageResource(R.drawable.message_delivered);
ui.status.setImageResource(R.drawable.message_delivered_white);
} else if (item.isSent()) {
ui.status.setImageResource(R.drawable.message_sent);
ui.status.setImageResource(R.drawable.message_sent_white);
} else {
ui.status.setImageResource(R.drawable.message_stored);
ui.status.setImageResource(R.drawable.message_stored_white);
}
} else {
if (identiconKey != null)
ui.avatar.setImageDrawable(
new IdenticonDrawable(identiconKey));
if (!header.isRead()) {
int left = ui.layout.getPaddingLeft();
ui.avatar.setImageDrawable(new IdenticonDrawable(identiconKey));
if (item.getType() == MSG_IN_UNREAD) {
// TODO implement new unread message highlight according to #232
/* int left = ui.layout.getPaddingLeft();
int top = ui.layout.getPaddingTop();
int right = ui.layout.getPaddingRight();
int bottom = ui.layout.getPaddingBottom();
@@ -144,6 +197,7 @@ class ConversationAdapter extends
// re-apply the previous padding due to bug in some Android versions
// see: https://code.google.com/p/android/issues/detail?id=17885
ui.layout.setPadding(left, top, right, bottom);
*/
}
}
@@ -159,42 +213,178 @@ class ConversationAdapter extends
ui.date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp));
}
private void bindIntroduction(final IntroductionHolder ui,
final ConversationIntroductionInItem item, final int position) {
final IntroductionRequest ir = item.getIntroductionRequest();
final String message = ir.getMessage();
if (StringUtils.isNullOrEmpty(message)) {
ui.messageLayout.setVisibility(View.GONE);
} else {
ui.messageLayout.setVisibility(View.VISIBLE);
if (item.getType() == INTRODUCTION_IN && identiconKey != null) {
ui.message.avatar.setImageDrawable(
new IdenticonDrawable(identiconKey));
}
ui.message.body.setText(message);
ui.message.date.setText(
DateUtils.getRelativeTimeSpanString(ctx, item.getTime()));
}
// Outgoing Introduction Request
if (item instanceof ConversationIntroductionOutItem) {
ui.text.setText(ctx.getString(R.string.introduction_request_sent,
contactName, ir.getName()));
ConversationIntroductionOutItem i =
(ConversationIntroductionOutItem) item;
if (i.isSeen()) {
ui.status.setImageResource(R.drawable.message_delivered);
ui.message.status.setImageResource(R.drawable.message_delivered_white);
} else if (i.isSent()) {
ui.status.setImageResource(R.drawable.message_sent);
ui.message.status.setImageResource(R.drawable.message_sent_white);
} else {
ui.status.setImageResource(R.drawable.message_stored);
ui.message.status.setImageResource(R.drawable.message_stored_white);
}
}
// Incoming Introduction Request (Answered)
else if (item.wasAnswered()) {
ui.text.setText(ctx.getString(
R.string.introduction_request_answered_received,
contactName, ir.getName()));
ui.acceptButton.setVisibility(View.GONE);
ui.declineButton.setVisibility(View.GONE);
}
// Incoming Introduction Request (Not Answered)
else {
if (item.getIntroductionRequest().doesExist()) {
ui.text.setText(ctx.getString(
R.string.introduction_request_exists_received,
contactName, ir.getName()));
} else {
ui.text.setText(
ctx.getString(R.string.introduction_request_received,
contactName, ir.getName()));
}
ui.acceptButton.setVisibility(View.VISIBLE);
ui.acceptButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
intro.respondToIntroduction(ir.getSessionId(), true);
item.setAnswered(true);
notifyItemChanged(position);
}
});
ui.declineButton.setVisibility(View.VISIBLE);
ui.declineButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
intro.respondToIntroduction(ir.getSessionId(), false);
item.setAnswered(true);
notifyItemChanged(position);
}
});
}
ui.date.setText(
DateUtils.getRelativeTimeSpanString(ctx, item.getTime()));
}
private void bindNotice(final NoticeHolder ui,
final ConversationNoticeItem item, final int position) {
ui.text.setText(item.getText());
ui.date.setText(
DateUtils.getRelativeTimeSpanString(ctx, item.getTime()));
if (item instanceof ConversationNoticeOutItem) {
ConversationNoticeOutItem n = (ConversationNoticeOutItem) item;
if (n.isSeen()) {
ui.status.setImageResource(R.drawable.message_delivered);
} else if (n.isSent()) {
ui.status.setImageResource(R.drawable.message_sent);
} else {
ui.status.setImageResource(R.drawable.message_stored);
}
}
}
@Override
public int getItemCount() {
return messages.size();
return items.size();
}
public ConversationItem getItem(int position) {
if (position == INVALID_POSITION || messages.size() <= position) {
if (position == INVALID_POSITION || items.size() <= position) {
return null; // Not found
}
return messages.get(position);
return items.get(position);
}
public ConversationItem getLastItem() {
if (messages.size() > 0) {
return messages.get(messages.size() - 1);
if (items.size() > 0) {
return items.get(items.size() - 1);
} else {
return null;
}
}
public SparseArray<IncomingItem> getIncomingMessages() {
SparseArray<IncomingItem> messages =
new SparseArray<IncomingItem>();
for (int i = 0; i < items.size(); i++) {
ConversationItem item = items.get(i);
if (item instanceof IncomingItem) {
messages.put(i, (IncomingItem) item);
}
}
return messages;
}
public SparseArray<OutgoingItem> getOutgoingMessages() {
SparseArray<OutgoingItem> messages =
new SparseArray<OutgoingItem>();
for (int i = 0; i < items.size(); i++) {
ConversationItem item = items.get(i);
if (item instanceof OutgoingItem) {
messages.put(i, (OutgoingItem) item);
}
}
return messages;
}
public SparseArray<ConversationMessageItem> getPrivateMessages() {
SparseArray<ConversationMessageItem> messages =
new SparseArray<ConversationMessageItem>();
for (int i = 0; i < items.size(); i++) {
ConversationItem item = items.get(i);
if (item instanceof ConversationMessageItem) {
messages.put(i, (ConversationMessageItem) item);
}
}
return messages;
}
public void add(final ConversationItem message) {
this.messages.add(message);
this.items.add(message);
}
public void clear() {
this.messages.beginBatchedUpdates();
this.items.beginBatchedUpdates();
while(messages.size() != 0) {
messages.removeItemAt(0);
while(items.size() != 0) {
items.removeItemAt(0);
}
this.messages.endBatchedUpdates();
this.items.endBatchedUpdates();
}
// TODO: Does this class need to be public?
public static class MessageHolder extends RecyclerView.ViewHolder {
protected class MessageHolder extends RecyclerView.ViewHolder {
public ViewGroup layout;
public TextView body;
@@ -217,4 +407,59 @@ class ConversationAdapter extends
}
}
}
}
protected class IntroductionHolder extends RecyclerView.ViewHolder {
public ViewGroup layout;
public View messageLayout;
public MessageHolder message;
public TextView text;
public Button acceptButton;
public Button declineButton;
public TextView date;
public ImageView status;
public IntroductionHolder(View v, int type) {
super(v);
layout = (ViewGroup) v.findViewById(R.id.introductionLayout);
messageLayout = v.findViewById(R.id.messageLayout);
message = new MessageHolder(messageLayout,
type == INTRODUCTION_IN ? MSG_IN : MSG_OUT);
text = (TextView) v.findViewById(R.id.introductionText);
acceptButton = (Button) v.findViewById(R.id.acceptButton);
declineButton = (Button) v.findViewById(R.id.declineButton);
date = (TextView) v.findViewById(R.id.introductionTime);
if (type == INTRODUCTION_OUT) {
status = (ImageView) v.findViewById(R.id.introductionStatus);
}
}
}
protected class NoticeHolder extends RecyclerView.ViewHolder {
public ViewGroup layout;
public TextView text;
public TextView date;
public ImageView status;
public NoticeHolder(View v, int type) {
super(v);
layout = (ViewGroup) v.findViewById(R.id.noticeLayout);
text = (TextView) v.findViewById(R.id.noticeText);
date = (TextView) v.findViewById(R.id.noticeTime);
if (type == NOTICE_OUT) {
status = (ImageView) v.findViewById(R.id.noticeStatus);
}
}
}
public interface IntroductionHandler {
void respondToIntroduction(final SessionId sessionId,
final boolean accept);
}
}

View File

@@ -0,0 +1,47 @@
package org.briarproject.android.contact;
import org.briarproject.api.introduction.IntroductionRequest;
import org.briarproject.api.sync.MessageId;
public class ConversationIntroductionInItem extends ConversationItem implements
ConversationItem.IncomingItem {
private IntroductionRequest ir;
private boolean answered, read;
public ConversationIntroductionInItem(IntroductionRequest ir) {
super(ir.getMessageId(), ir.getTime());
this.ir = ir;
this.answered = ir.wasAnswered();
this.read = ir.isRead();
}
@Override
int getType() {
return INTRODUCTION_IN;
}
public IntroductionRequest getIntroductionRequest() {
return ir;
}
public boolean wasAnswered() {
return answered;
}
public void setAnswered(boolean answered) {
this.answered = answered;
}
@Override
public boolean isRead() {
return read;
}
@Override
public void setRead(boolean read) {
this.read = read;
}
}

View File

@@ -0,0 +1,48 @@
package org.briarproject.android.contact;
import org.briarproject.api.introduction.IntroductionRequest;
import org.briarproject.api.sync.MessageId;
/**
* This class is needed and can not be replaced by an ConversationNoticeOutItem,
* because it carries the optional introduction message
* to be displayed as a regular private message.
*/
public class ConversationIntroductionOutItem
extends ConversationIntroductionInItem
implements ConversationItem.OutgoingItem {
private boolean sent, seen;
public ConversationIntroductionOutItem(IntroductionRequest ir) {
super(ir);
this.sent = ir.isSent();
this.seen = ir.isSeen();
}
@Override
int getType() {
return INTRODUCTION_OUT;
}
@Override
public boolean isSent() {
return sent;
}
@Override
public void setSent(boolean sent) {
this.sent = sent;
}
@Override
public boolean isSeen() {
return seen;
}
@Override
public void setSeen(boolean seen) {
this.seen = seen;
}
}

View File

@@ -1,46 +1,105 @@
package org.briarproject.android.contact;
import org.briarproject.api.messaging.PrivateMessageHeader;
import android.content.Context;
import org.briarproject.R;
import org.briarproject.api.introduction.IntroductionMessage;
import org.briarproject.api.introduction.IntroductionRequest;
import org.briarproject.api.introduction.IntroductionResponse;
import org.briarproject.api.sync.MessageId;
// This class is not thread-safe
class ConversationItem {
public abstract class ConversationItem {
private final PrivateMessageHeader header;
private byte[] body;
private boolean sent, seen;
final static int MSG_IN = 0;
final static int MSG_IN_UNREAD = 1;
final static int MSG_OUT = 2;
final static int INTRODUCTION_IN = 3;
final static int INTRODUCTION_OUT = 4;
final static int NOTICE_IN = 5;
final static int NOTICE_OUT = 6;
ConversationItem(PrivateMessageHeader header) {
this.header = header;
body = null;
sent = header.isSent();
seen = header.isSeen();
private MessageId id;
private long time;
public ConversationItem(MessageId id, long time) {
this.id = id;
this.time = time;
}
PrivateMessageHeader getHeader() {
return header;
abstract int getType();
public MessageId getId() {
return id;
}
byte[] getBody() {
return body;
long getTime() {
return time;
}
void setBody(byte[] body) {
this.body = body;
public static ConversationItem from(IntroductionRequest ir) {
if (ir.isLocal()) {
return new ConversationIntroductionOutItem(ir);
} else {
return new ConversationIntroductionInItem(ir);
}
}
boolean isSent() {
return sent;
public static ConversationItem from(Context ctx, String contactName,
IntroductionResponse ir) {
if (ir.isLocal()) {
String text;
if (ir.wasAccepted()) {
text = ctx.getString(
R.string.introduction_response_accepted_sent,
ir.getName());
} else {
text = ctx.getString(
R.string.introduction_response_declined_sent,
ir.getName());
}
return new ConversationNoticeOutItem(ir.getMessageId(), text,
ir.getTime(), ir.isSent(), ir.isSeen());
} else {
String text;
if (ir.wasAccepted()) {
text = ctx.getString(
R.string.introduction_response_accepted_received,
contactName, ir.getName());
} else {
text = ctx.getString(
R.string.introduction_response_declined_received,
contactName, ir.getName());
}
return new ConversationNoticeInItem(ir.getMessageId(), text,
ir.getTime(), ir.isRead());
}
}
void setSent(boolean sent) {
this.sent = sent;
/** This method should not be used to get user-facing objects,
* Its purpose is to provider data for the contact list.
*/
public static ConversationItem from(IntroductionMessage im) {
if (im.isLocal())
return new ConversationNoticeOutItem(im.getMessageId(), "",
im.getTime(), false, false);
return new ConversationNoticeInItem(im.getMessageId(), "", im.getTime(),
im.isRead());
}
boolean isSeen() {
return seen;
protected interface OutgoingItem {
MessageId getId();
boolean isSent();
void setSent(boolean sent);
boolean isSeen();
void setSeen(boolean seen);
}
void setSeen(boolean seen) {
this.seen = seen;
protected interface IncomingItem {
MessageId getId();
boolean isRead();
void setRead(boolean read);
}
}

View File

@@ -0,0 +1,73 @@
package org.briarproject.android.contact;
import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.sync.MessageId;
// This class is not thread-safe
public class ConversationMessageItem extends ConversationItem implements
ConversationItem.OutgoingItem, ConversationItem.IncomingItem {
private final PrivateMessageHeader header;
private byte[] body;
private boolean sent, seen, read;
public ConversationMessageItem(PrivateMessageHeader header) {
super(header.getId(), header.getTimestamp());
this.header = header;
body = null;
sent = header.isSent();
seen = header.isSeen();
read = header.isRead();
}
@Override
int getType() {
if (getHeader().isLocal()) return MSG_OUT;
if (getHeader().isRead()) return MSG_IN;
return MSG_IN_UNREAD;
}
PrivateMessageHeader getHeader() {
return header;
}
byte[] getBody() {
return body;
}
void setBody(byte[] body) {
this.body = body;
}
@Override
public boolean isSent() {
return sent;
}
@Override
public void setSent(boolean sent) {
this.sent = sent;
}
@Override
public boolean isSeen() {
return seen;
}
@Override
public void setSeen(boolean seen) {
this.seen = seen;
}
@Override
public boolean isRead() {
return read;
}
@Override
public void setRead(boolean read) {
this.read = read;
}
}

View File

@@ -0,0 +1,32 @@
package org.briarproject.android.contact;
import org.briarproject.api.sync.MessageId;
public class ConversationNoticeInItem extends ConversationNoticeItem implements
ConversationItem.IncomingItem {
private boolean read;
public ConversationNoticeInItem(MessageId id, String text, long time,
boolean read) {
super(id, text, time);
this.read = read;
}
@Override
int getType() {
return NOTICE_IN;
}
@Override
public boolean isRead() {
return read;
}
@Override
public void setRead(boolean read) {
this.read = read;
}
}

View File

@@ -0,0 +1,19 @@
package org.briarproject.android.contact;
import org.briarproject.api.sync.MessageId;
abstract class ConversationNoticeItem extends ConversationItem {
private String text;
public ConversationNoticeItem(MessageId id, String text, long time) {
super(id, time);
this.text = text;
}
public String getText() {
return text;
}
}

View File

@@ -0,0 +1,44 @@
package org.briarproject.android.contact;
import org.briarproject.api.sync.MessageId;
public class ConversationNoticeOutItem extends ConversationNoticeItem implements
ConversationItem.OutgoingItem {
private boolean sent, seen;
public ConversationNoticeOutItem(MessageId id, String text, long time,
boolean sent, boolean seen) {
super(id, text, time);
this.sent = sent;
this.seen = seen;
}
@Override
int getType() {
return NOTICE_OUT;
}
@Override
public boolean isSent() {
return sent;
}
@Override
public void setSent(boolean sent) {
this.sent = sent;
}
@Override
public boolean isSeen() {
return seen;
}
@Override
public void setSeen(boolean seen) {
this.seen = seen;
}
}

View File

@@ -0,0 +1,243 @@
package org.briarproject.android.introduction;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import org.briarproject.R;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.contact.ContactListAdapter;
import org.briarproject.android.contact.ContactListItem;
import org.briarproject.android.contact.ConversationItem;
import org.briarproject.android.contact.ConversationMessageItem;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.util.BriarRecyclerView;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.db.DbException;
import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.introduction.IntroductionManager;
import org.briarproject.api.introduction.IntroductionMessage;
import org.briarproject.api.messaging.MessagingManager;
import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.plugins.ConnectionRegistry;
import org.briarproject.api.sync.GroupId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
public class ContactChooserFragment extends BaseFragment {
public final static String TAG = "ContactChooserFragment";
private IntroductionActivity introductionActivity;
private BriarRecyclerView list;
private ContactListAdapter adapter;
private int contactId;
private static final Logger LOG =
Logger.getLogger(ContactChooserFragment.class.getName());
// Fields that are accessed from background threads must be volatile
protected volatile Contact c1;
@Inject
protected volatile ContactManager contactManager;
@Inject
protected volatile IdentityManager identityManager;
@Inject
protected volatile MessagingManager messagingManager;
@Inject
protected volatile IntroductionManager introductionManager;
@Inject
protected volatile ConnectionRegistry connectionRegistry;
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
introductionActivity = (IntroductionActivity) context;
} catch (ClassCastException e) {
throw new java.lang.InstantiationError(
"This fragment is only meant to be attached to the IntroductionActivity");
}
}
@Override
public void injectActivity(AndroidComponent component) {
component.inject(this);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View contentView =
inflater.inflate(R.layout.introduction_contact_chooser,
container, false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setExitTransition(new Fade());
}
ContactListAdapter.OnItemClickListener onItemClickListener =
new ContactListAdapter.OnItemClickListener() {
@Override
public void onItemClick(View view, ContactListItem item) {
if (c1 == null) {
Toast.makeText(getActivity(),
R.string.introduction_error,
Toast.LENGTH_SHORT).show();
return;
}
Contact c2 = item.getContact();
if (!c1.getLocalAuthorId()
.equals(c2.getLocalAuthorId())) {
warnAboutDifferentIdentities(view, c1, c2);
} else {
introductionActivity.showMessageScreen(view, c1, c2);
}
}
};
adapter =
new ContactListAdapter(getActivity(), onItemClickListener, true);
list = (BriarRecyclerView) contentView.findViewById(R.id.contactList);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
list.setAdapter(adapter);
list.setEmptyText(getString(R.string.no_contacts));
contactId = introductionActivity.getContactId();
return contentView;
}
@Override
public void onResume() {
super.onResume();
loadContacts();
}
@Override
public String getUniqueTag() {
return TAG;
}
private void loadContacts() {
introductionActivity.runOnDbThread(new Runnable() {
public void run() {
try {
List<ContactListItem> contacts =
new ArrayList<ContactListItem>();
AuthorId localAuthorId= null;
for (Contact c : contactManager.getActiveContacts()) {
if (c.getId().getInt() == contactId) {
c1 = c;
localAuthorId = c1.getLocalAuthorId();
} else {
ContactId id = c.getId();
GroupId groupId =
messagingManager.getConversationId(id);
Collection<ConversationItem> messages =
getMessages(id);
boolean connected =
connectionRegistry.isConnected(c.getId());
LocalAuthor localAuthor = identityManager
.getLocalAuthor(c.getLocalAuthorId());
contacts.add(new ContactListItem(c, localAuthor,
connected, groupId, messages));
}
}
displayContacts(localAuthorId, contacts);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void displayContacts(final AuthorId localAuthorId,
final List<ContactListItem> contacts) {
introductionActivity.runOnUiThread(new Runnable() {
public void run() {
adapter.setLocalAuthor(localAuthorId);
adapter.clear();
if (contacts.size() == 0) list.showData();
else adapter.addAll(contacts);
}
});
}
private void warnAboutDifferentIdentities(final View view, final Contact c1,
final Contact c2) {
DialogInterface.OnClickListener okListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
introductionActivity.showMessageScreen(view, c1, c2);
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(),
R.style.BriarDialogTheme);
builder.setTitle(getString(
R.string.introduction_warn_different_identities_title));
builder.setMessage(getString(
R.string.introduction_warn_different_identities_text));
builder.setPositiveButton(R.string.dialog_button_introduce, okListener);
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
/** This needs to be called from the DbThread */
private Collection<ConversationItem> getMessages(ContactId id)
throws DbException {
long now = System.currentTimeMillis();
Collection<ConversationItem> messages =
new ArrayList<ConversationItem>();
Collection<PrivateMessageHeader> headers =
messagingManager.getMessageHeaders(id);
for (PrivateMessageHeader h : headers) {
messages.add(new ConversationMessageItem(h));
}
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading message headers took " + duration + " ms");
Collection<IntroductionMessage> introductions =
introductionManager
.getIntroductionMessages(id);
for (IntroductionMessage m : introductions) {
messages.add(ConversationItem.from(m));
}
duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading introduction messages took " + duration + " ms");
return messages;
}
}

View File

@@ -0,0 +1,107 @@
package org.briarproject.android.introduction;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.view.MenuItem;
import android.view.View;
import org.briarproject.R;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.BriarActivity;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.api.contact.Contact;
public class IntroductionActivity extends BriarActivity implements
BaseFragment.BaseFragmentListener {
public static final String CONTACT_ID = "briar.CONTACT_ID";
private int contactId;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
contactId = intent.getIntExtra(CONTACT_ID, -1);
setContentView(R.layout.activity_introduction);
if (savedInstanceState == null) {
ContactChooserFragment chooserFragment =
new ContactChooserFragment();
getSupportFragmentManager().beginTransaction()
.add(R.id.introductionContainer, chooserFragment).commit();
}
}
@Override
public void injectActivity(AndroidComponent component) {
component.inject(this);
}
@Override
public void showLoadingScreen(boolean isBlocking, int stringId) {
// this is handled by the recycler view in ContactChooserFragment
}
@Override
public void hideLoadingScreen() {
// this is handled by the recycler view in ContactChooserFragment
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
// Handle presses on the action bar items
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onBackPressed() {
FragmentManager fm = getSupportFragmentManager();
if (fm.getBackStackEntryCount() == 1) {
fm.popBackStack();
} else {
super.onBackPressed();
}
}
public int getContactId() {
return contactId;
}
public void showMessageScreen(final View view, final Contact c1,
final Contact c2) {
IntroductionMessageFragment messageFragment =
IntroductionMessageFragment
.newInstance(c1.getId().getInt(), c2.getId().getInt());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
messageFragment.setSharedElementEnterTransition(new ChangeBounds());
messageFragment.setEnterTransition(new Fade());
messageFragment.setSharedElementReturnTransition(new ChangeBounds());
}
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(android.R.anim.fade_in,
android.R.anim.fade_out,
android.R.anim.slide_in_left,
android.R.anim.slide_out_right)
.addSharedElement(view, "avatar")
.replace(R.id.introductionContainer, messageFragment,
ContactChooserFragment.TAG)
.addToBackStack(null)
.commit();
}
}

View File

@@ -0,0 +1,235 @@
package org.briarproject.android.introduction;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.R;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.api.FormatException;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.db.DbException;
import org.briarproject.api.introduction.IntroductionManager;
import java.util.logging.Logger;
import javax.inject.Inject;
import de.hdodenhof.circleimageview.CircleImageView;
import im.delight.android.identicons.IdenticonDrawable;
import static java.util.logging.Level.WARNING;
public class IntroductionMessageFragment extends BaseFragment {
private static final Logger LOG =
Logger.getLogger(IntroductionMessageFragment.class.getName());
public final static String TAG = "ContactChooserFragment";
private IntroductionActivity introductionActivity;
private ViewHolder ui;
private final static String CONTACT_ID_1 = "contact1";
private final static String CONTACT_ID_2 = "contact2";
// Fields that are accessed from background threads must be volatile
private volatile boolean introductionWasMade = false;
@Inject
protected volatile ContactManager contactManager;
@Inject
protected volatile IntroductionManager introductionManager;
public static IntroductionMessageFragment newInstance(int contactId1,
int contactId2) {
IntroductionMessageFragment f = new IntroductionMessageFragment();
Bundle args = new Bundle();
args.putInt(CONTACT_ID_1, contactId1);
args.putInt(CONTACT_ID_2, contactId2);
f.setArguments(args);
return f;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
introductionActivity = (IntroductionActivity) context;
} catch (ClassCastException e) {
throw new java.lang.InstantiationError(
"This fragment is only meant to be attached to the IntroductionActivity");
}
}
@Override
public void injectActivity(AndroidComponent component) {
component.inject(this);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// change toolbar text
ActionBar actionBar = introductionActivity.getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(R.string.introduction_message_title);
}
// inflate view
View v =
inflater.inflate(R.layout.introduction_message, container,
false);
// show progress bar until contacts have been loaded
ui = new ViewHolder(v);
ui.text.setVisibility(View.GONE);
ui.button.setEnabled(false);
// get contact IDs from fragment arguments
int contactId1 = getArguments().getInt(CONTACT_ID_1, -1);
int contactId2 = getArguments().getInt(CONTACT_ID_2, -1);
if (contactId1 == -1 || contactId2 == -1) {
throw new java.lang.InstantiationError(
"You need to use newInstance() to instantiate");
}
// get contacts and then show view
prepareToSetUpViews(contactId1, contactId2);
return v;
}
@Override
public String getUniqueTag() {
return TAG;
}
private void prepareToSetUpViews(final int contactId1,
final int contactId2) {
introductionActivity.runOnDbThread(new Runnable() {
public void run() {
try {
Contact c1 = contactManager
.getContact(new ContactId(contactId1));
Contact c2 = contactManager
.getContact(new ContactId(contactId2));
setUpViews(c1, c2);
} catch (DbException e) {
// TODO
e.printStackTrace();
}
}
});
}
private void setUpViews(final Contact c1, final Contact c2) {
introductionActivity.runOnUiThread(new Runnable() {
public void run() {
// set avatars
ui.avatar1.setImageDrawable(new IdenticonDrawable(
c1.getAuthor().getId().getBytes()));
ui.avatar2.setImageDrawable(new IdenticonDrawable(
c2.getAuthor().getId().getBytes()));
// set introduction text
ui.text.setText(String.format(
getString(R.string.introduction_message_text),
c1.getAuthor().getName(), c2.getAuthor().getName()));
// set button action
ui.button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onButtonClick(c1, c2);
}
});
// hide progress bar and show views
ui.progressBar.setVisibility(View.GONE);
ui.text.setVisibility(View.VISIBLE);
ui.button.setEnabled(true);
}
});
}
public void onButtonClick(final Contact c1, final Contact c2) {
String msg = ui.message.getText().toString();
makeIntroduction(c1, c2, msg);
}
private void makeIntroduction(final Contact c1, final Contact c2,
final String msg) {
introductionActivity.runOnDbThread(new Runnable() {
public void run() {
// prevent double introductions
if (introductionWasMade) return;
// actually make the introduction
try {
introductionManager.makeIntroduction(c1, c2, msg);
introductionWasMade = true;
postIntroduction(false);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
postIntroduction(true);
} catch (FormatException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
postIntroduction(true);
}
}
});
}
private void postIntroduction(final boolean error) {
introductionActivity.runOnUiThread(new Runnable() {
public void run() {
introductionActivity.hideSoftKeyboard(ui.message);
if (error) {
Toast.makeText(introductionActivity,
R.string.introduction_error, Toast.LENGTH_SHORT)
.show();
introductionActivity.setResult(Activity.RESULT_CANCELED);
} else {
introductionActivity.setResult(Activity.RESULT_OK);
}
introductionActivity.finish();
}
});
}
private class ViewHolder {
ProgressBar progressBar;
ViewGroup header;
CircleImageView avatar1;
CircleImageView avatar2;
TextView text;
EditText message;
Button button;
ViewHolder(View v) {
progressBar = (ProgressBar) v.findViewById(R.id.progressBar);
header = (ViewGroup) v.findViewById(R.id.introductionHeader);
avatar1 = (CircleImageView) v.findViewById(R.id.avatarContact1);
avatar2 = (CircleImageView) v.findViewById(R.id.avatarContact2);
text = (TextView) v.findViewById(R.id.introductionText);
message = (EditText) v.findViewById(R.id.introductionMessageView);
button = (Button) v.findViewById(R.id.makeIntroductionButton);
}
}
}

View File

@@ -73,15 +73,10 @@ public class BriarRecyclerView extends FrameLayout {
}
emptyObserver = new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
showData();
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
onChanged();
if (itemCount > 0) showData();
}
};
}