migrate conversation list to RecyclerView

fixes #2488
fixes #2333
// FREEBIE
This commit is contained in:
Jake McGinty 2015-09-15 15:28:27 -07:00 committed by Moxie Marlinspike
parent cbcd53a8a0
commit 99d3374d35
20 changed files with 293 additions and 282 deletions

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="true" android:state_selected="true"
android:drawable="@drawable/list_selected_holo_light" />
</selector>
<item android:drawable="@color/textsecure_primary_alpha33" android:state_selected="true" />
</selector>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="true" android:state_selected="true"
android:drawable="@drawable/list_selected_holo_dark" />
</selector>

View file

@ -5,18 +5,12 @@
android:layout_height="match_parent"
android:orientation="vertical">
<ListView android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
android:drawSelectorOnTop="false"
android:transcriptMode="normal"
android:scrollbarAlwaysDrawVerticalTrack="false"
android:scrollbarStyle="insideOverlay"
android:stackFromBottom="true"
android:fadingEdge="none"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:cacheColorHint="?conversation_background" />
<android.support.v7.widget.RecyclerView
android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
android:scrollbars="vertical"
android:cacheColorHint="?conversation_background" />
</LinearLayout>
</LinearLayout>

View file

@ -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">
<TextView android:id="@+id/group_message_status"
android:layout_width="wrap_content"

View file

@ -2,12 +2,11 @@
<org.thoughtcrime.securesms.ConversationItem
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/conversation_item"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="?conversation_item_background">
android:background="@drawable/conversation_item_background">
<RelativeLayout android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -133,7 +133,6 @@
<item name="quick_camera_icon">@drawable/quick_camera_light</item>
<item name="conversation_item_background">@drawable/conversation_item_background</item>
<item name="conversation_item_sent_indicator_text_background">@drawable/conversation_item_sent_indicator_text_shape</item>
<item name="dialog_info_icon">@drawable/ic_info_outline_light</item>
@ -204,7 +203,6 @@
<item name="conversation_group_member_name">#99ffffff</item>
<item name="conversation_item_background">@drawable/conversation_item_background_dark</item>
<item name="conversation_item_bubble_background">#ff333333</item>
<item name="conversation_item_sent_text_primary_color">#ffffffff</item>
<item name="conversation_item_sent_text_secondary_color">#aaeeeeee</item>

View file

@ -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<MessageRecord> batchSelected,
boolean groupThread, boolean pushDestination);
}

View file

@ -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;

View file

@ -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 <V extends View & BindableConversationItem>
extends CursorRecyclerViewAdapter<ConversationAdapter.ViewHolder>
{
private static final int MAX_CACHE_SIZE = 40;
private final Map<String,SoftReference<MessageRecord>> messageRecordCache =
@ -61,46 +68,46 @@ public class ConversationAdapter extends CursorAdapter implements AbsListView.Re
private final Set<MessageRecord> batchSelected = Collections.synchronizedSet(new HashSet<MessageRecord>());
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 <V extends View & BindableConversationItem> ViewHolder(final @NonNull V itemView) {
super(itemView);
}
@SuppressWarnings("unchecked")
public <V extends View & BindableConversationItem> 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;
@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);
int type = getItemViewType(cursor);
viewHolder.getView().bind(masterSecret, messageRecord, locale, batchSelected, groupThread, pushDestination);
}
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 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 void onViewRecycled(ViewHolder holder) {
holder.getView().unbind();
}
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");
}
}
@Override
public int getViewTypeCount() {
return 3;
}
@Override
public int getItemViewType(int position) {
Cursor cursor = (Cursor)getItem(position);
return getItemViewType(cursor);
}
private int getItemViewType(Cursor cursor) {
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<MessageRecord> reference = messageRecordCache.get(type + messageId);
final SoftReference<MessageRecord> 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<MessageRecord> getBatchSelected() {
return batchSelected;
public void clearSelection() {
batchSelected.clear();
}
@Override
public void onMovedToScrapHeap(View view) {
((Unbindable) view).unbind();
public Set<MessageRecord> getSelectedItems() {
return Collections.unmodifiableSet(new HashSet<>(batchSelected));
}
}

View file

@ -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<Cursor>
{
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<MessageRecord> messageRecords = getSelectedMessageRecords();
Set<MessageRecord> 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,15 +169,15 @@ public class ConversationFragment extends ListFragment
}
}
private MessageRecord getSelectedMessageRecord() {
List<MessageRecord> messageRecords = getSelectedMessageRecords();
if (messageRecords.size() == 1) return messageRecords.get(0);
else throw new AssertionError();
private ConversationAdapter getListAdapter() {
return (ConversationAdapter) list.getAdapter();
}
private List<MessageRecord> getSelectedMessageRecords() {
return new LinkedList<>(((ConversationAdapter)getListAdapter()).getBatchSelected());
private MessageRecord getSelectedMessageRecord() {
Set<MessageRecord> messageRecords = getListAdapter().getSelectedItems();
if (messageRecords.size() == 1) return messageRecords.iterator().next();
else throw new AssertionError();
}
public void reload(Recipients recipients, long threadId) {
@ -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<MessageRecord> messageRecords) {
Collections.sort(messageRecords, new Comparator<MessageRecord>() {
private void handleCopyMessage(final Set<MessageRecord> messageRecords) {
List<MessageRecord> messageList = new LinkedList<>(messageRecords);
Collections.sort(messageList, new Comparator<MessageRecord>() {
@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<MessageRecord> messageRecords) {
private void handleDeleteMessages(final Set<MessageRecord> 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<Cursor> arg0, Cursor cursor) {
if (getListAdapter() != null) {
((CursorAdapter) getListAdapter()).changeCursor(cursor);
if (list.getAdapter() != null) {
getListAdapter().changeCursor(cursor);
}
}
@Override
public void onLoaderReset(Loader<Cursor> 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;
}
};
}
}

View file

@ -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<MessageRecord> batchSelected;
private SelectionClickListener selectionClickListener;
private ThumbnailView mediaThumbnail;
private Button mmsDownloadButton;
private TextView mmsDownloadingLabel;
private StatusManager statusManager;
private Set<MessageRecord> 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<MessageRecord> batchSelected,
@NonNull SelectionClickListener selectionClickListener,
boolean groupThread, boolean pushDestination)
@Override
public void bind(@NonNull MasterSecret masterSecret,
@NonNull MessageRecord messageRecord,
@NonNull Locale locale,
@NonNull Set<MessageRecord> 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;

View file

@ -100,8 +100,12 @@ public class ConversationListAdapter extends CursorRecyclerViewAdapter<Conversat
parent, false), clickListener);
}
@Override public void onViewRecycled(ViewHolder holder) {
holder.getItem().unbind();
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, Cursor cursor) {
public void onBindViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
ThreadDatabase.Reader reader = threadDatabase.readerFor(cursor, masterCipher);
ThreadRecord record = reader.getCurrent();

View file

@ -159,12 +159,6 @@ public class ConversationListFragment extends Fragment
private void initializeListAdapter() {
list.setAdapter(new ConversationListAdapter(getActivity(), masterSecret, locale, null, this));
list.setRecyclerListener(new RecyclerListener() {
@Override
public void onViewRecycled(ViewHolder holder) {
((ConversationListItem)holder.itemView).unbind();
}
});
getLoaderManager().restartLoader(0, null, this);
}

View file

@ -49,7 +49,7 @@ import static org.thoughtcrime.securesms.util.SpanUtil.color;
*/
public class ConversationListItem extends RelativeLayout
implements Recipients.RecipientsModifiedListener
implements Recipients.RecipientsModifiedListener, Unbindable
{
private final static String TAG = ConversationListItem.class.getSimpleName();
@ -115,9 +115,9 @@ public class ConversationListItem extends RelativeLayout
this.contactPhotoImage.setAvatar(recipients, true);
}
@Override
public void unbind() {
if (this.recipients != null)
this.recipients.removeListener(this);
if (this.recipients != null) this.recipients.removeListener(this);
}
private void setBatchState(boolean batch) {

View file

@ -2,20 +2,25 @@ package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.Util;
import java.util.Locale;
import java.util.Set;
public class ConversationUpdateItem extends LinearLayout
implements Recipients.RecipientsModifiedListener, Recipient.RecipientModifiedListener, Unbindable, View.OnClickListener
implements Recipients.RecipientsModifiedListener, Recipient.RecipientModifiedListener, BindableConversationItem, View.OnClickListener
{
private static final String TAG = ConversationUpdateItem.class.getSimpleName();
@ -42,7 +47,17 @@ public class ConversationUpdateItem extends LinearLayout
setOnClickListener(this);
}
public void set(MessageRecord messageRecord) {
@Override
public void bind(@NonNull MasterSecret masterSecret,
@NonNull MessageRecord messageRecord,
@NonNull Locale locale,
@NonNull Set<MessageRecord> 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);
}
});
}

View file

@ -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<ViewHolder> {
}
@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);

View file

@ -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<MessageRecord>(), new NullSelectionListener(),
conversationItem.bind(masterSecret, messageRecord, dynamicLanguage.getCurrentLocale(),
new HashSet<MessageRecord>(),
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;
}
}
}

View file

@ -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<VH extends RecyclerView.ViewHold
: 0;
}
public abstract void onBindViewHolder(VH viewHolder, Cursor cursor);
public abstract void onBindViewHolder(VH viewHolder, @NonNull Cursor cursor);
@Override
public void onBindViewHolder(VH viewHolder, int position) {
moveToPositionOrThrow(position);
onBindViewHolder(viewHolder, cursor);
}
@Override public int getItemViewType(int position) {
moveToPositionOrThrow(position);
return getItemViewType(cursor);
}
public int getItemViewType(@NonNull Cursor cursor) {
return 0;
}
private void assertActiveCursor() {
if (!isActiveCursor()) {
throw new IllegalStateException("this should only be called when the cursor is valid");
}
}
private void moveToPositionOrThrow(final int position) {
assertActiveCursor();
if (!cursor.moveToPosition(position)) {
throw new IllegalStateException("couldn't move cursor to position " + position);
}
onBindViewHolder(viewHolder, cursor);
}
private boolean isActiveCursor() {

View file

@ -56,7 +56,7 @@ public class MmsSmsDatabase extends Database {
MmsSmsColumns.MISMATCHED_IDENTITIES,
MmsDatabase.NETWORK_FAILURE, TRANSPORT};
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;

View file

@ -16,15 +16,18 @@
*/
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.support.annotation.IdRes;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
@ -110,4 +113,12 @@ public class ViewUtil {
view.setVisibility(View.VISIBLE);
view.startAnimation(animation);
}
@SuppressWarnings("unchecked")
public static <T extends View> T inflate(@NonNull LayoutInflater inflater,
@NonNull ViewGroup parent,
@LayoutRes int layoutResId)
{
return (T)(inflater.inflate(layoutResId, parent, false));
}
}