From 99d3374d35b5027850fdc31c8a2620fb5f83fbf7 Mon Sep 17 00:00:00 2001 From: Jake McGinty Date: Tue, 15 Sep 2015 15:28:27 -0700 Subject: [PATCH] migrate conversation list to RecyclerView fixes #2488 fixes #2333 // FREEBIE --- res/drawable/conversation_item_background.xml | 5 +- .../conversation_item_background_dark.xml | 5 - res/layout/conversation_fragment.xml | 22 +-- res/layout/conversation_item_received.xml | 6 +- res/layout/conversation_item_sent.xml | 3 +- res/values/themes.xml | 2 - .../securesms/BindableConversationItem.java | 17 ++ .../securesms/ConversationActivity.java | 5 - .../securesms/ConversationAdapter.java | 171 +++++++++--------- .../securesms/ConversationFragment.java | 127 ++++++------- .../securesms/ConversationItem.java | 117 +++++------- .../securesms/ConversationListAdapter.java | 6 +- .../securesms/ConversationListFragment.java | 6 - .../securesms/ConversationListItem.java | 6 +- .../securesms/ConversationUpdateItem.java | 21 ++- .../securesms/ImageMediaAdapter.java | 3 +- .../securesms/MessageDetailsActivity.java | 14 +- .../database/CursorRecyclerViewAdapter.java | 22 ++- .../securesms/database/MmsSmsDatabase.java | 2 +- .../thoughtcrime/securesms/util/ViewUtil.java | 11 ++ 20 files changed, 291 insertions(+), 280 deletions(-) delete mode 100644 res/drawable/conversation_item_background_dark.xml create mode 100644 src/org/thoughtcrime/securesms/BindableConversationItem.java diff --git a/res/drawable/conversation_item_background.xml b/res/drawable/conversation_item_background.xml index fb3ecc7153..7014300913 100644 --- a/res/drawable/conversation_item_background.xml +++ b/res/drawable/conversation_item_background.xml @@ -1,5 +1,4 @@ - - + + \ No newline at end of file diff --git a/res/drawable/conversation_item_background_dark.xml b/res/drawable/conversation_item_background_dark.xml deleted file mode 100644 index d7fb6e05f6..0000000000 --- a/res/drawable/conversation_item_background_dark.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/res/layout/conversation_fragment.xml b/res/layout/conversation_fragment.xml index 8cd2df13d6..5f4a3d3806 100644 --- a/res/layout/conversation_fragment.xml +++ b/res/layout/conversation_fragment.xml @@ -5,18 +5,12 @@ android:layout_height="match_parent" android:orientation="vertical"> - + - \ No newline at end of file + diff --git a/res/layout/conversation_item_received.xml b/res/layout/conversation_item_received.xml index 97ef8db754..4f38aaf108 100644 --- a/res/layout/conversation_item_received.xml +++ b/res/layout/conversation_item_received.xml @@ -4,11 +4,9 @@ android:layout_height="wrap_content" android:paddingRight="10dip" android:orientation="vertical" - android:background="?conversation_item_background" + android:background="@drawable/conversation_item_background" xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - xmlns:dots="http://schemas.android.com/apk/res-auto" - xmlns:app="http://schemas.android.com/apk/res-auto"> + xmlns:tools="http://schemas.android.com/tools"> + android:background="@drawable/conversation_item_background"> @drawable/quick_camera_light - @drawable/conversation_item_background @drawable/conversation_item_sent_indicator_text_shape @drawable/ic_info_outline_light @@ -204,7 +203,6 @@ #99ffffff - @drawable/conversation_item_background_dark #ff333333 #ffffffff #aaeeeeee diff --git a/src/org/thoughtcrime/securesms/BindableConversationItem.java b/src/org/thoughtcrime/securesms/BindableConversationItem.java new file mode 100644 index 0000000000..971907c8a3 --- /dev/null +++ b/src/org/thoughtcrime/securesms/BindableConversationItem.java @@ -0,0 +1,17 @@ +package org.thoughtcrime.securesms; + +import android.support.annotation.NonNull; + +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.database.model.MessageRecord; + +import java.util.Locale; +import java.util.Set; + +public interface BindableConversationItem extends Unbindable { + void bind(@NonNull MasterSecret masterSecret, + @NonNull MessageRecord messageRecord, + @NonNull Locale locale, + @NonNull Set batchSelected, + boolean groupThread, boolean pushDestination); +} diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index 0e9e9b2686..2ec67386a4 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -1368,11 +1368,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity public void onFocusChange(View v, boolean hasFocus) {} } - @Override - public void setComposeText(String text) { - this.composeText.setText(text); - } - @Override public void setThreadId(long threadId) { this.threadId = threadId; diff --git a/src/org/thoughtcrime/securesms/ConversationAdapter.java b/src/org/thoughtcrime/securesms/ConversationAdapter.java index c6cc4cb44b..ed636731e8 100644 --- a/src/org/thoughtcrime/securesms/ConversationAdapter.java +++ b/src/org/thoughtcrime/securesms/ConversationAdapter.java @@ -18,13 +18,18 @@ package org.thoughtcrime.securesms; import android.content.Context; import android.database.Cursor; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; import android.view.ViewGroup; -import android.widget.AbsListView; -import android.support.v4.widget.CursorAdapter; import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.MmsSmsDatabase; @@ -39,7 +44,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; -import org.thoughtcrime.securesms.ConversationFragment.SelectionClickListener; +import org.thoughtcrime.securesms.util.ViewUtil; /** * A cursor adapter for a conversation thread. Ultimately @@ -49,7 +54,9 @@ import org.thoughtcrime.securesms.ConversationFragment.SelectionClickListener; * @author Moxie Marlinspike * */ -public class ConversationAdapter extends CursorAdapter implements AbsListView.RecyclerListener { +public class ConversationAdapter + extends CursorRecyclerViewAdapter +{ private static final int MAX_CACHE_SIZE = 40; private final Map> messageRecordCache = @@ -61,46 +68,46 @@ public class ConversationAdapter extends CursorAdapter implements AbsListView.Re private final Set batchSelected = Collections.synchronizedSet(new HashSet()); - private final SelectionClickListener selectionClickListener; - private final Context context; + private final ItemClickListener clickListener; private final MasterSecret masterSecret; private final Locale locale; private final boolean groupThread; private final boolean pushDestination; + private final MmsSmsDatabase db; private final LayoutInflater inflater; - public ConversationAdapter(Context context, MasterSecret masterSecret, Locale locale, - SelectionClickListener selectionClickListener, boolean groupThread, - boolean pushDestination) - { - super(context, null, 0); - this.context = context; - this.masterSecret = masterSecret; - this.locale = locale; - this.selectionClickListener = selectionClickListener; - this.groupThread = groupThread; - this.pushDestination = pushDestination; - this.inflater = LayoutInflater.from(context); + protected static class ViewHolder extends RecyclerView.ViewHolder { + public ViewHolder(final @NonNull V itemView) { + super(itemView); + } + + @SuppressWarnings("unchecked") + public V getView() { + return (V)itemView; + } } - @Override - public void bindView(View view, Context context, Cursor cursor) { - long id = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID)); - String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT)); - MessageRecord messageRecord = getMessageRecord(id, cursor, type); + public interface ItemClickListener { + void onItemClick(ConversationItem item); + void onItemLongClick(ConversationItem item); + } - switch (getItemViewType(cursor)) { - case MESSAGE_TYPE_INCOMING: - case MESSAGE_TYPE_OUTGOING: - ((ConversationItem) view).set(masterSecret, messageRecord, locale, batchSelected, - selectionClickListener, groupThread, pushDestination); - break; - case MESSAGE_TYPE_UPDATE: - ((ConversationUpdateItem)view).set(messageRecord); - break; - default: - throw new AssertionError("Unknown type!"); - } + public ConversationAdapter(@NonNull Context context, + @NonNull MasterSecret masterSecret, + @NonNull Locale locale, + @Nullable ItemClickListener clickListener, + @Nullable Cursor cursor, + boolean groupThread, + boolean pushDestination) + { + super(context, cursor); + this.masterSecret = masterSecret; + this.locale = locale; + this.clickListener = clickListener; + this.groupThread = groupThread; + this.pushDestination = pushDestination; + this.inflater = LayoutInflater.from(context); + this.db = DatabaseFactory.getMmsSmsDatabase(context); } @Override @@ -109,40 +116,50 @@ public class ConversationAdapter extends CursorAdapter implements AbsListView.Re super.changeCursor(cursor); } - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - View view; - - int type = getItemViewType(cursor); - - switch (type) { - case ConversationAdapter.MESSAGE_TYPE_OUTGOING: - view = inflater.inflate(R.layout.conversation_item_sent, parent, false); - break; - case ConversationAdapter.MESSAGE_TYPE_INCOMING: - view = inflater.inflate(R.layout.conversation_item_received, parent, false); - break; - case ConversationAdapter.MESSAGE_TYPE_UPDATE: - view = inflater.inflate(R.layout.conversation_item_update, parent, false); - break; - default: throw new IllegalArgumentException("unsupported item view type given to ConversationAdapter"); + @Override public void onBindViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) { + long id = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID)); + String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT)); + MessageRecord messageRecord = getMessageRecord(id, cursor, type); + + viewHolder.getView().bind(masterSecret, messageRecord, locale, batchSelected, groupThread, pushDestination); + } + + @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + final V itemView = ViewUtil.inflate(inflater, parent, getLayoutForViewType(viewType)); + if (viewType == MESSAGE_TYPE_INCOMING || viewType == MESSAGE_TYPE_OUTGOING) { + itemView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + if (clickListener != null) clickListener.onItemClick((ConversationItem)itemView); + } + }); + itemView.setOnLongClickListener(new OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + if (clickListener != null) clickListener.onItemLongClick((ConversationItem)itemView); + return true; + } + }); } - return view; + return new ViewHolder(itemView); } - @Override - public int getViewTypeCount() { - return 3; + @Override public void onViewRecycled(ViewHolder holder) { + holder.getView().unbind(); } - @Override - public int getItemViewType(int position) { - Cursor cursor = (Cursor)getItem(position); - return getItemViewType(cursor); + private @LayoutRes int getLayoutForViewType(int viewType) { + switch (viewType) { + case ConversationAdapter.MESSAGE_TYPE_OUTGOING: return R.layout.conversation_item_sent; + case ConversationAdapter.MESSAGE_TYPE_INCOMING: return R.layout.conversation_item_received; + case ConversationAdapter.MESSAGE_TYPE_UPDATE: return R.layout.conversation_item_update; + default: throw new IllegalArgumentException("unsupported item view type given to ConversationAdapter"); + } } - private int getItemViewType(Cursor cursor) { + @Override + public int getItemViewType(@NonNull Cursor cursor) { long id = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.ID)); String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT)); MessageRecord messageRecord = getMessageRecord(id, cursor, type); @@ -153,43 +170,33 @@ public class ConversationAdapter extends CursorAdapter implements AbsListView.Re } private MessageRecord getMessageRecord(long messageId, Cursor cursor, String type) { - SoftReference reference = messageRecordCache.get(type + messageId); - + final SoftReference reference = messageRecordCache.get(type + messageId); if (reference != null) { - MessageRecord record = reference.get(); - - if (record != null) - return record; + final MessageRecord record = reference.get(); + if (record != null) return record; } - MmsSmsDatabase.Reader reader = DatabaseFactory.getMmsSmsDatabase(context) - .readerFor(cursor, masterSecret); - - MessageRecord messageRecord = reader.getCurrent(); - + final MessageRecord messageRecord = db.readerFor(cursor, masterSecret).getCurrent(); messageRecordCache.put(type + messageId, new SoftReference<>(messageRecord)); return messageRecord; } public void close() { - this.getCursor().close(); + getCursor().close(); } - public void toggleBatchSelected(MessageRecord messageRecord) { - if (batchSelected.contains(messageRecord)) { - batchSelected.remove(messageRecord); - } else { + public void toggleSelection(MessageRecord messageRecord) { + if (!batchSelected.remove(messageRecord)) { batchSelected.add(messageRecord); } } - public Set getBatchSelected() { - return batchSelected; + public void clearSelection() { + batchSelected.clear(); } - @Override - public void onMovedToScrapHeap(View view) { - ((Unbindable) view).unbind(); + public Set getSelectedItems() { + return Collections.unmodifiableSet(new HashSet<>(batchSelected)); } } diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java index 05c1a26053..43c27040e1 100644 --- a/src/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationFragment.java @@ -8,12 +8,13 @@ import android.database.Cursor; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; -import android.support.v4.app.ListFragment; +import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; -import android.support.v4.widget.CursorAdapter; import android.support.v7.app.AppCompatActivity; import android.support.v7.view.ActionMode; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; import android.text.ClipboardManager; import android.text.TextUtils; import android.util.Log; @@ -24,12 +25,11 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.Window; -import android.widget.AdapterView; -import android.widget.ListView; import android.widget.Toast; import com.afollestad.materialdialogs.AlertDialogWrapper; +import org.thoughtcrime.securesms.ConversationAdapter.ItemClickListener; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsSmsDatabase; @@ -45,21 +45,22 @@ import org.thoughtcrime.securesms.util.FutureTaskListener; import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask; import org.thoughtcrime.securesms.util.SaveAttachmentTask; import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment; -import org.thoughtcrime.securesms.util.ServiceUtil; +import org.thoughtcrime.securesms.util.ViewUtil; import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; import java.util.List; import java.util.Locale; +import java.util.Set; -public class ConversationFragment extends ListFragment +public class ConversationFragment extends Fragment implements LoaderManager.LoaderCallbacks { private static final String TAG = ConversationFragment.class.getSimpleName(); - private final ActionModeCallback actionModeCallback = new ActionModeCallback(); - private final SelectionClickListener selectionClickListener = new ConversationFragmentSelectionClickListener(); + private final ActionModeCallback actionModeCallback = new ActionModeCallback(); + private final ItemClickListener selectionClickListener = new ConversationFragmentItemClickListener(); private ConversationFragmentListener listener; @@ -68,6 +69,7 @@ public class ConversationFragment extends ListFragment private long threadId; private ActionMode actionMode; private Locale locale; + private RecyclerView list; @Override public void onCreate(Bundle icicle) { @@ -78,16 +80,24 @@ public class ConversationFragment extends ListFragment @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { - return inflater.inflate(R.layout.conversation_fragment, container, false); + final View view = inflater.inflate(R.layout.conversation_fragment, container, false); + list = ViewUtil.findById(view, android.R.id.list); + return view; } @Override public void onActivityCreated(Bundle bundle) { super.onActivityCreated(bundle); + final LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity()); + layoutManager.setOrientation(LinearLayoutManager.VERTICAL); + layoutManager.setReverseLayout(true); + list.setHasFixedSize(false); + list.setScrollContainer(true); + list.setLayoutManager(layoutManager); + initializeResources(); initializeListAdapter(); - initializeContextualActionBar(); } @Override @@ -100,8 +110,8 @@ public class ConversationFragment extends ListFragment public void onResume() { super.onResume(); - if (getListAdapter() != null) { - ((ConversationAdapter) getListAdapter()).notifyDataSetChanged(); + if (list.getAdapter() != null) { + list.getAdapter().notifyDataSetChanged(); } } @@ -125,21 +135,15 @@ public class ConversationFragment extends ListFragment private void initializeListAdapter() { if (this.recipients != null && this.threadId != -1) { - this.setListAdapter(new ConversationAdapter(getActivity(), masterSecret, locale, selectionClickListener, - (!this.recipients.isSingleRecipient()) || this.recipients.isGroupRecipient(), - DirectoryHelper.isPushDestination(getActivity(), this.recipients))); - getListView().setRecyclerListener((ConversationAdapter)getListAdapter()); + list.setAdapter(new ConversationAdapter(getActivity(), masterSecret, locale, selectionClickListener, null, + (!this.recipients.isSingleRecipient()) || this.recipients.isGroupRecipient(), + DirectoryHelper.isPushDestination(getActivity(), this.recipients))); getLoaderManager().restartLoader(0, null, this); } } - private void initializeContextualActionBar() { - getListView().setOnItemClickListener(selectionClickListener); - getListView().setOnItemLongClickListener(selectionClickListener); - } - private void setCorrectMenuVisibility(Menu menu) { - List messageRecords = getSelectedMessageRecords(); + Set messageRecords = getListAdapter().getSelectedItems(); if (actionMode != null && messageRecords.size() == 0) { actionMode.finish(); @@ -152,7 +156,7 @@ public class ConversationFragment extends ListFragment menu.findItem(R.id.menu_context_save_attachment).setVisible(false); menu.findItem(R.id.menu_context_resend).setVisible(false); } else { - MessageRecord messageRecord = messageRecords.get(0); + MessageRecord messageRecord = messageRecords.iterator().next(); menu.findItem(R.id.menu_context_resend).setVisible(messageRecord.isFailed()); menu.findItem(R.id.menu_context_save_attachment).setVisible(messageRecord.isMms() && @@ -165,17 +169,17 @@ public class ConversationFragment extends ListFragment } } + private ConversationAdapter getListAdapter() { + return (ConversationAdapter) list.getAdapter(); + } + private MessageRecord getSelectedMessageRecord() { - List messageRecords = getSelectedMessageRecords(); + Set messageRecords = getListAdapter().getSelectedItems(); - if (messageRecords.size() == 1) return messageRecords.get(0); + if (messageRecords.size() == 1) return messageRecords.iterator().next(); else throw new AssertionError(); } - private List getSelectedMessageRecords() { - return new LinkedList<>(((ConversationAdapter)getListAdapter()).getBatchSelected()); - } - public void reload(Recipients recipients, long threadId) { this.recipients = recipients; @@ -186,17 +190,18 @@ public class ConversationFragment extends ListFragment } public void scrollToBottom() { - final ListView list = getListView(); list.post(new Runnable() { @Override public void run() { - list.setSelection(getListAdapter().getCount() - 1); + list.stopScroll(); + list.smoothScrollToPosition(0); } }); } - private void handleCopyMessage(final List messageRecords) { - Collections.sort(messageRecords, new Comparator() { + private void handleCopyMessage(final Set messageRecords) { + List messageList = new LinkedList<>(messageRecords); + Collections.sort(messageList, new Comparator() { @Override public int compare(MessageRecord lhs, MessageRecord rhs) { if (lhs.getDateReceived() < rhs.getDateReceived()) return -1; @@ -209,7 +214,7 @@ public class ConversationFragment extends ListFragment ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); boolean first = true; - for (MessageRecord messageRecord : messageRecords) { + for (MessageRecord messageRecord : messageList) { String body = messageRecord.getDisplayBody().toString(); if (body != null) { @@ -225,7 +230,7 @@ public class ConversationFragment extends ListFragment clipboard.setText(result); } - private void handleDeleteMessages(final List messageRecords) { + private void handleDeleteMessages(final Set messageRecords) { AlertDialogWrapper.Builder builder = new AlertDialogWrapper.Builder(getActivity()); builder.setTitle(R.string.ConversationFragment_confirm_message_delete); builder.setIconAttribute(R.attr.dialog_alert_icon); @@ -319,53 +324,41 @@ public class ConversationFragment extends ListFragment @Override public void onLoadFinished(Loader arg0, Cursor cursor) { - if (getListAdapter() != null) { - ((CursorAdapter) getListAdapter()).changeCursor(cursor); + if (list.getAdapter() != null) { + getListAdapter().changeCursor(cursor); } } @Override public void onLoaderReset(Loader arg0) { - if (getListAdapter() != null) { - ((CursorAdapter) getListAdapter()).changeCursor(null); + if (list.getAdapter() != null) { + getListAdapter().changeCursor(null); } } public interface ConversationFragmentListener { - public void setComposeText(String text); - - public void setThreadId(long threadId); + void setThreadId(long threadId); } - public interface SelectionClickListener extends - AdapterView.OnItemLongClickListener, AdapterView.OnItemClickListener {} + private class ConversationFragmentItemClickListener implements ItemClickListener { - private class ConversationFragmentSelectionClickListener - implements SelectionClickListener - { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - if (actionMode != null && view instanceof ConversationItem) { - MessageRecord messageRecord = ((ConversationItem)view).getMessageRecord(); - ((ConversationAdapter) getListAdapter()).toggleBatchSelected(messageRecord); - ((ConversationAdapter) getListAdapter()).notifyDataSetChanged(); + @Override public void onItemClick(ConversationItem item) { + if (actionMode != null) { + MessageRecord messageRecord = item.getMessageRecord(); + ((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord); + list.getAdapter().notifyDataSetChanged(); setCorrectMenuVisibility(actionMode.getMenu()); } } - @Override - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - if (actionMode == null && view instanceof ConversationItem) { - MessageRecord messageRecord = ((ConversationItem)view).getMessageRecord(); - ((ConversationAdapter) getListAdapter()).toggleBatchSelected(messageRecord); - ((ConversationAdapter) getListAdapter()).notifyDataSetChanged(); + @Override public void onItemLongClick(ConversationItem item) { + if (actionMode == null) { + ((ConversationAdapter) list.getAdapter()).toggleSelection(item.getMessageRecord()); + list.getAdapter().notifyDataSetChanged(); actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback); - return true; } - - return false; } } @@ -395,8 +388,8 @@ public class ConversationFragment extends ListFragment @Override public void onDestroyActionMode(ActionMode mode) { - ((ConversationAdapter)getListAdapter()).getBatchSelected().clear(); - ((ConversationAdapter)getListAdapter()).notifyDataSetChanged(); + ((ConversationAdapter)list.getAdapter()).clearSelection(); + list.getAdapter().notifyDataSetChanged(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { getActivity().getWindow().setStatusBarColor(statusBarColor); @@ -409,11 +402,11 @@ public class ConversationFragment extends ListFragment public boolean onActionItemClicked(ActionMode mode, MenuItem item) { switch(item.getItemId()) { case R.id.menu_context_copy: - handleCopyMessage(getSelectedMessageRecords()); + handleCopyMessage(getListAdapter().getSelectedItems()); actionMode.finish(); return true; case R.id.menu_context_delete_message: - handleDeleteMessages(getSelectedMessageRecords()); + handleDeleteMessages(getListAdapter().getSelectedItems()); actionMode.finish(); return true; case R.id.menu_context_details: @@ -436,5 +429,5 @@ public class ConversationFragment extends ListFragment return false; } - }; + } } diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java index 44201532b1..78a3e850fb 100644 --- a/src/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/ConversationItem.java @@ -23,10 +23,10 @@ import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.NonNull; import android.text.TextUtils; +import android.text.util.Linkify; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; @@ -40,7 +40,6 @@ import android.widget.Toast; import com.afollestad.materialdialogs.AlertDialogWrapper; -import org.thoughtcrime.securesms.ConversationFragment.SelectionClickListener; import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.crypto.MasterSecret; @@ -74,7 +73,9 @@ import java.util.Set; * */ -public class ConversationItem extends LinearLayout implements Recipient.RecipientModifiedListener, Unbindable { +public class ConversationItem extends LinearLayout + implements Recipient.RecipientModifiedListener, BindableConversationItem +{ private final static String TAG = ConversationItem.class.getSimpleName(); private MessageRecord messageRecord; @@ -97,16 +98,13 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien private View pendingIndicator; private ImageView pendingApprovalIndicator; - private StatusManager statusManager; - private Set batchSelected; - private SelectionClickListener selectionClickListener; - private ThumbnailView mediaThumbnail; - private Button mmsDownloadButton; - private TextView mmsDownloadingLabel; + private StatusManager statusManager; + private Set batchSelected; + private ThumbnailView mediaThumbnail; + private Button mmsDownloadButton; + private TextView mmsDownloadingLabel; - private int defaultBubbleColor; - private Drawable selectedBackground; - private Drawable normalBackground; + private int defaultBubbleColor; private final MmsDownloadClickListener mmsDownloadClickListener = new MmsDownloadClickListener(); private final MmsPreferencesClickListener mmsPreferencesClickListener = new MmsPreferencesClickListener(); @@ -114,9 +112,8 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien private final Context context; public ConversationItem(Context context) { - super(context); - this.context = context; - } + this(context, null); + } public ConversationItem(Context context, AttributeSet attrs) { super(context, attrs); @@ -131,7 +128,7 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien ViewGroup pendingIndicatorStub = (ViewGroup) findViewById(R.id.pending_indicator_stub); if (pendingIndicatorStub != null) { - LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + LayoutInflater inflater = LayoutInflater.from(context); if (Build.VERSION.SDK_INT >= 11) inflater.inflate(R.layout.conversation_item_pending_v11, pendingIndicatorStub, true); else inflater.inflate(R.layout.conversation_item_pending, pendingIndicatorStub, true); } @@ -154,33 +151,33 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien this.statusManager = new StatusManager(pendingIndicator, sentIndicator, deliveredIndicator, failedIndicator, pendingApprovalIndicator); setOnClickListener(clickListener); + PassthroughClickListener passthroughClickListener = new PassthroughClickListener(); if (mmsDownloadButton != null) mmsDownloadButton.setOnClickListener(mmsDownloadClickListener); - if (mediaThumbnail != null) { - mediaThumbnail.setThumbnailClickListener(new ThumbnailClickListener()); - mediaThumbnail.setOnLongClickListener(new MultiSelectLongClickListener()); - mediaThumbnail.setDownloadClickListener(new ThumbnailDownloadClickListener()); - } + mediaThumbnail.setThumbnailClickListener(new ThumbnailClickListener()); + mediaThumbnail.setDownloadClickListener(new ThumbnailDownloadClickListener()); + mediaThumbnail.setOnLongClickListener(passthroughClickListener); + bodyText.setOnLongClickListener(passthroughClickListener); + bodyText.setOnClickListener(passthroughClickListener); } - public void set(@NonNull MasterSecret masterSecret, - @NonNull MessageRecord messageRecord, - @NonNull Locale locale, - @NonNull Set batchSelected, - @NonNull SelectionClickListener selectionClickListener, - boolean groupThread, boolean pushDestination) + @Override + public void bind(@NonNull MasterSecret masterSecret, + @NonNull MessageRecord messageRecord, + @NonNull Locale locale, + @NonNull Set batchSelected, + boolean groupThread, boolean pushDestination) { this.masterSecret = masterSecret; this.messageRecord = messageRecord; this.locale = locale; this.batchSelected = batchSelected; - this.selectionClickListener = selectionClickListener; this.groupThread = groupThread; this.pushDestination = pushDestination; this.recipient = messageRecord.getIndividualRecipient(); this.recipient.addListener(this); - setSelectionBackgroundDrawables(messageRecord); + setSelectionState(messageRecord); setBodyText(messageRecord); setBubbleState(messageRecord, recipient); setStatusIcons(messageRecord); @@ -198,8 +195,6 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien final TypedArray attrs = context.obtainStyledAttributes(attributes); defaultBubbleColor = attrs.getColor(0, Color.WHITE); - selectedBackground = attrs.getDrawable(1); - normalBackground = attrs.getDrawable(2); attrs.recycle(); } @@ -227,12 +222,11 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien } } - private void setSelectionBackgroundDrawables(MessageRecord messageRecord) { - if (batchSelected.contains(messageRecord)) { - setBackgroundDrawable(selectedBackground); - } else { - setBackgroundDrawable(normalBackground); - } + private void setSelectionState(MessageRecord messageRecord) { + setSelected(batchSelected.contains(messageRecord)); + mediaThumbnail.setClickable(batchSelected.isEmpty()); + mediaThumbnail.setLongClickable(batchSelected.isEmpty()); + bodyText.setAutoLinkMask(batchSelected.isEmpty() ? Linkify.ALL : 0); } private boolean isCaptionlessMms(MessageRecord messageRecord) { @@ -255,11 +249,6 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien bodyText.setText(messageRecord.getDisplayBody()); bodyText.setVisibility(View.VISIBLE); } - - if (bodyText.isClickable() && bodyText.isFocusable()) { - bodyText.setOnLongClickListener(new MultiSelectLongClickListener()); - bodyText.setOnClickListener(new MultiSelectLongClickListener()); - } } private void setMediaAttributes(MessageRecord messageRecord) { @@ -334,12 +323,9 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien private void setEvents(MessageRecord messageRecord) { setClickable(batchSelected.isEmpty() && - (messageRecord.isFailed() || + (messageRecord.isFailed() || messageRecord.isPendingInsecureSmsFallback() || messageRecord.isBundleKeyExchange())); - if (messageRecord.isFailed()) { - setOnLongClickListener(new MultiSelectLongClickListener()); - } } private void setGroupMessageStatus(MessageRecord messageRecord, Recipient recipient) { @@ -410,7 +396,7 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien } private class ThumbnailDownloadClickListener implements ThumbnailView.ThumbnailClickListener { - @Override public void onClick(View v, Slide slide) { + @Override public void onClick(View v, final Slide slide) { DatabaseFactory.getPartDatabase(context).setTransferState(messageRecord.getId(), slide.getPart().getPartId(), PartDatabase.TRANSFER_PROGRESS_STARTED); } } @@ -430,10 +416,9 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien } public void onClick(final View v, final Slide slide) { - if (!batchSelected.isEmpty()) { - selectionClickListener.onItemClick(null, ConversationItem.this, -1, -1); - } else if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && - slide.getThumbnailUri() != null) + if (batchSelected.isEmpty() && + MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && + slide.getThumbnailUri() != null) { Intent intent = new Intent(context, MediaPreviewActivity.class); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); @@ -483,11 +468,22 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien } } + private class PassthroughClickListener implements View.OnLongClickListener, View.OnClickListener { + + @Override public boolean onLongClick(View v) { + performLongClick(); + return true; + } + + @Override public void onClick(View v) { + performClick(); + } + } private class ClickListener implements View.OnClickListener { public void onClick(View v) { - if (messageRecord.isFailed() && !batchSelected.isEmpty()) { - selectionClickListener.onItemClick(null, ConversationItem.this, -1, -1); - } else if(messageRecord.isFailed()) { + if (!batchSelected.isEmpty()) return; + + if (messageRecord.isFailed()) { Intent intent = new Intent(context, MessageDetailsActivity.class); intent.putExtra(MessageDetailsActivity.MASTER_SECRET_EXTRA, masterSecret); intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, messageRecord.getId()); @@ -502,19 +498,6 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien } } - private class MultiSelectLongClickListener implements OnLongClickListener, OnClickListener { - @Override - public boolean onLongClick(View view) { - selectionClickListener.onItemLongClick(null, ConversationItem.this, -1, -1); - return true; - } - - @Override - public void onClick(View view) { - selectionClickListener.onItemClick(null, ConversationItem.this, -1, -1); - } - } - private void handleMessageApproval() { final int title; final int message; diff --git a/src/org/thoughtcrime/securesms/ConversationListAdapter.java b/src/org/thoughtcrime/securesms/ConversationListAdapter.java index 1e728e4482..0fb908b7e9 100644 --- a/src/org/thoughtcrime/securesms/ConversationListAdapter.java +++ b/src/org/thoughtcrime/securesms/ConversationListAdapter.java @@ -100,8 +100,12 @@ public class ConversationListAdapter extends CursorRecyclerViewAdapter batchSelected, + boolean groupThread, boolean pushDestination) + { + bind(messageRecord); + } + + private void bind(@NonNull MessageRecord messageRecord) { this.messageRecord = messageRecord; this.sender = messageRecord.getIndividualRecipient(); @@ -73,7 +88,7 @@ public class ConversationUpdateItem extends LinearLayout Util.runOnMain(new Runnable() { @Override public void run() { - set(messageRecord); + bind(messageRecord); } }); } diff --git a/src/org/thoughtcrime/securesms/ImageMediaAdapter.java b/src/org/thoughtcrime/securesms/ImageMediaAdapter.java index 6691399db0..61d7565a42 100644 --- a/src/org/thoughtcrime/securesms/ImageMediaAdapter.java +++ b/src/org/thoughtcrime/securesms/ImageMediaAdapter.java @@ -19,6 +19,7 @@ package org.thoughtcrime.securesms; import android.content.Context; import android.content.Intent; import android.database.Cursor; +import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.view.LayoutInflater; @@ -64,7 +65,7 @@ public class ImageMediaAdapter extends CursorRecyclerViewAdapter { } @Override - public void onBindViewHolder(final ViewHolder viewHolder, final Cursor cursor) { + public void onBindViewHolder(final ViewHolder viewHolder, final @NonNull Cursor cursor) { final ThumbnailView imageView = viewHolder.imageView; final ImageRecord imageRecord = ImageRecord.from(cursor); diff --git a/src/org/thoughtcrime/securesms/MessageDetailsActivity.java b/src/org/thoughtcrime/securesms/MessageDetailsActivity.java index dad593a094..132e989220 100644 --- a/src/org/thoughtcrime/securesms/MessageDetailsActivity.java +++ b/src/org/thoughtcrime/securesms/MessageDetailsActivity.java @@ -28,7 +28,6 @@ import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.ListView; import android.widget.TextView; @@ -173,8 +172,8 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity toFromRes = R.string.message_details_header__from; } toFrom.setText(toFromRes); - conversationItem.set(masterSecret, messageRecord, dynamicLanguage.getCurrentLocale(), - new HashSet(), new NullSelectionListener(), + conversationItem.bind(masterSecret, messageRecord, dynamicLanguage.getCurrentLocale(), + new HashSet(), recipients != messageRecord.getRecipients(), DirectoryHelper.isPushDestination(this, recipients)); recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, masterSecret, messageRecord, @@ -306,13 +305,4 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity } } } - - private static class NullSelectionListener implements ConversationFragment.SelectionClickListener { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) {} - @Override - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - return false; - } - } } diff --git a/src/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java b/src/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java index b24e587fae..18e09beca5 100644 --- a/src/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java +++ b/src/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java @@ -19,6 +19,7 @@ package org.thoughtcrime.securesms.database; import android.content.Context; import android.database.Cursor; import android.database.DataSetObserver; +import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; /** @@ -90,17 +91,34 @@ public abstract class CursorRecyclerViewAdapter T inflate(@NonNull LayoutInflater inflater, + @NonNull ViewGroup parent, + @LayoutRes int layoutResId) + { + return (T)(inflater.inflate(layoutResId, parent, false)); + } }