diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 257b2d8752..32f780434a 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -173,6 +173,16 @@ + + + + + + + + + + + + + diff --git a/res/drawable-v21/conversation_list_item_background.xml b/res/drawable-v21/conversation_list_item_read_background_dark.xml similarity index 82% rename from res/drawable-v21/conversation_list_item_background.xml rename to res/drawable-v21/conversation_list_item_read_background_dark.xml index 642879178e..19f004e729 100644 --- a/res/drawable-v21/conversation_list_item_background.xml +++ b/res/drawable-v21/conversation_list_item_read_background_dark.xml @@ -5,6 +5,7 @@ + diff --git a/res/drawable-xhdpi/ic_archive_white_24dp.png b/res/drawable-xhdpi/ic_archive_white_24dp.png new file mode 100644 index 0000000000..3513bd9fef Binary files /dev/null and b/res/drawable-xhdpi/ic_archive_white_24dp.png differ diff --git a/res/drawable-xhdpi/ic_unarchive_white_24dp.png b/res/drawable-xhdpi/ic_unarchive_white_24dp.png new file mode 100644 index 0000000000..a0a1509a16 Binary files /dev/null and b/res/drawable-xhdpi/ic_unarchive_white_24dp.png differ diff --git a/res/drawable-xhdpi/ic_unarchive_white_36dp.png b/res/drawable-xhdpi/ic_unarchive_white_36dp.png new file mode 100644 index 0000000000..20d015751f Binary files /dev/null and b/res/drawable-xhdpi/ic_unarchive_white_36dp.png differ diff --git a/res/drawable-xxhdpi/ic_archive_white_24dp.png b/res/drawable-xxhdpi/ic_archive_white_24dp.png new file mode 100644 index 0000000000..00e04e42bf Binary files /dev/null and b/res/drawable-xxhdpi/ic_archive_white_24dp.png differ diff --git a/res/drawable-xxhdpi/ic_unarchive_white_24dp.png b/res/drawable-xxhdpi/ic_unarchive_white_24dp.png new file mode 100644 index 0000000000..20d015751f Binary files /dev/null and b/res/drawable-xxhdpi/ic_unarchive_white_24dp.png differ diff --git a/res/drawable-xxhdpi/ic_unarchive_white_36dp.png b/res/drawable-xxhdpi/ic_unarchive_white_36dp.png new file mode 100644 index 0000000000..fefcb36e84 Binary files /dev/null and b/res/drawable-xxhdpi/ic_unarchive_white_36dp.png differ diff --git a/res/drawable-xxxhdpi/ic_archive_white_24dp.png b/res/drawable-xxxhdpi/ic_archive_white_24dp.png new file mode 100644 index 0000000000..34cd3fd805 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_archive_white_24dp.png differ diff --git a/res/drawable-xxxhdpi/ic_archive_white_36dp.png b/res/drawable-xxxhdpi/ic_archive_white_36dp.png new file mode 100644 index 0000000000..5e1e2af823 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_archive_white_36dp.png differ diff --git a/res/drawable-xxxhdpi/ic_archive_white_48dp.png b/res/drawable-xxxhdpi/ic_archive_white_48dp.png new file mode 100644 index 0000000000..6fb64c404b Binary files /dev/null and b/res/drawable-xxxhdpi/ic_archive_white_48dp.png differ diff --git a/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png b/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png new file mode 100644 index 0000000000..a789520baa Binary files /dev/null and b/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png differ diff --git a/res/drawable-xxxhdpi/ic_unarchive_white_36dp.png b/res/drawable-xxxhdpi/ic_unarchive_white_36dp.png new file mode 100644 index 0000000000..5e4dd3ddf1 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_unarchive_white_36dp.png differ diff --git a/res/drawable/conversation_list_item_read_background.xml b/res/drawable/conversation_list_item_read_background.xml new file mode 100644 index 0000000000..45108a5b76 --- /dev/null +++ b/res/drawable/conversation_list_item_read_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/res/drawable/conversation_list_item_background.xml b/res/drawable/conversation_list_item_read_background_dark.xml similarity index 78% rename from res/drawable/conversation_list_item_background.xml rename to res/drawable/conversation_list_item_read_background_dark.xml index 8a28201a1a..c5153ed188 100644 --- a/res/drawable/conversation_list_item_background.xml +++ b/res/drawable/conversation_list_item_read_background_dark.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/res/drawable/rounded_rectangle.xml b/res/drawable/rounded_rectangle.xml new file mode 100644 index 0000000000..4af7432fd4 --- /dev/null +++ b/res/drawable/rounded_rectangle.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/res/layout/contact_selection_list_item.xml b/res/layout/contact_selection_list_item.xml index 4f9d121420..2f842b03e5 100644 --- a/res/layout/contact_selection_list_item.xml +++ b/res/layout/contact_selection_list_item.xml @@ -8,7 +8,6 @@ android:layout_height="?android:attr/listPreferredItemHeight" android:orientation="horizontal" android:gravity="center_vertical" - android:background="@drawable/conversation_list_item_background" android:paddingLeft="48dp" android:paddingRight="20dp"> @@ -18,7 +17,7 @@ android:layout_height="40dp" android:foreground="@drawable/contact_photo_background" android:cropToPadding="true" - tools:src="@color/md_material_blue_600" + tools:src="@color/blue_600" android:layout_marginRight="10dp" android:contentDescription="@string/SingleContactSelectionActivity_contact_photo" /> diff --git a/res/layout/conversation_list_fragment.xml b/res/layout/conversation_list_fragment.xml index 9dcc31fa54..961349a88b 100644 --- a/res/layout/conversation_list_fragment.xml +++ b/res/layout/conversation_list_fragment.xml @@ -1,6 +1,7 @@ - - + android:contentDescription="@string/conversation_list_fragment__fab_content_description"/> - + diff --git a/res/layout/conversation_list_item_action.xml b/res/layout/conversation_list_item_action.xml new file mode 100644 index 0000000000..77d2bc8375 --- /dev/null +++ b/res/layout/conversation_list_item_action.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/res/layout/conversation_list_item_view.xml b/res/layout/conversation_list_item_view.xml index 1965dfeda4..94624a9e04 100644 --- a/res/layout/conversation_list_item_view.xml +++ b/res/layout/conversation_list_item_view.xml @@ -4,7 +4,6 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:background="@drawable/conversation_list_item_background" android:layout_height="70dp"> + + + diff --git a/res/menu/conversation.xml b/res/menu/conversation.xml index d57a0beaf5..81587e84c1 100644 --- a/res/menu/conversation.xml +++ b/res/menu/conversation.xml @@ -7,9 +7,6 @@ - - diff --git a/res/menu/conversation_list_batch.xml b/res/menu/conversation_list_batch.xml index a61042f5d5..3df65d81de 100644 --- a/res/menu/conversation_list_batch.xml +++ b/res/menu/conversation_list_batch.xml @@ -5,10 +5,11 @@ + app:showAsAction="always" /> + android:icon="?menu_selectall_icon" + app:showAsAction="always"/> \ No newline at end of file diff --git a/res/menu/conversation_list_batch_archive.xml b/res/menu/conversation_list_batch_archive.xml new file mode 100644 index 0000000000..1c91c1b358 --- /dev/null +++ b/res/menu/conversation_list_batch_archive.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/res/menu/conversation_list_batch_unarchive.xml b/res/menu/conversation_list_batch_unarchive.xml new file mode 100644 index 0000000000..ecb7bb310b --- /dev/null +++ b/res/menu/conversation_list_batch_unarchive.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/res/values/colors.xml b/res/values/colors.xml index bcf175759f..e2de5006b3 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -24,7 +24,7 @@ #7F111111 - #ffffffff + @color/gray5 #ffffffff #ff000000 #ff333333 diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 5a99e42358..c1937aea22 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -37,6 +37,7 @@ 150dp 70dp + 16dp 135dip diff --git a/res/values/strings.xml b/res/values/strings.xml index 59f88cd492..4933f80e9a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -174,10 +174,18 @@ Deleting Deleting selected threads... + Archived conversations + UNDO + Moved conversation to inbox + Archived conversation + Moved conversations to inbox Key exchange message... + + Archived conversations (%d) + Using custom: %s Using default: %s @@ -867,6 +875,7 @@ Message details Manage linked devices Invite friends + Conversations archive Import / export @@ -1048,6 +1057,7 @@ Delete selected Select all + Archive selected Search @@ -1055,6 +1065,7 @@ Contact Photo Image Error alert + Archived New conversation @@ -1151,6 +1162,7 @@ Transport icon + diff --git a/res/values/themes.xml b/res/values/themes.xml index c8b7f4f380..4e746c6e04 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -95,7 +95,7 @@ @color/white @drawable/list_selected_holo_light @drawable/conversation_list_item_unread_background - @drawable/conversation_list_item_background + @drawable/conversation_list_item_read_background #66333333 #FF333333 #FF444444 @@ -205,12 +205,13 @@ @style/ThemeOverlay.AppCompat.Dark @color/text_color_dark_theme @color/text_color_secondary_dark_theme + @color/textsecure_primary_dark @color/signal_primary_dark @color/signal_primary_dark @color/black @drawable/list_selected_holo_dark @drawable/conversation_list_item_unread_background_dark - @drawable/conversation_list_item_background + @drawable/conversation_list_item_read_background_dark #66dddddd #ffdddddd #ffdddddd diff --git a/src/org/thoughtcrime/securesms/BindableConversationListItem.java b/src/org/thoughtcrime/securesms/BindableConversationListItem.java new file mode 100644 index 0000000000..1da1ac2864 --- /dev/null +++ b/src/org/thoughtcrime/securesms/BindableConversationListItem.java @@ -0,0 +1,15 @@ +package org.thoughtcrime.securesms; + +import android.support.annotation.NonNull; + +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.database.model.ThreadRecord; + +import java.util.Locale; +import java.util.Set; + +public interface BindableConversationListItem extends Unbindable { + + public void bind(@NonNull MasterSecret masterSecret, @NonNull ThreadRecord thread, + @NonNull Locale locale, @NonNull Set selectedThreads, boolean batchMode); +} diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index d07eace41c..12bb463ad9 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -392,7 +392,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity switch (item.getItemId()) { case R.id.menu_call_secure: case R.id.menu_call_insecure: handleDial(getRecipients().getPrimaryRecipient()); return true; - case R.id.menu_delete_thread: handleDeleteThread(); return true; case R.id.menu_add_attachment: handleAddAttachment(); return true; case R.id.menu_view_media: handleViewMedia(); return true; case R.id.menu_add_to_contacts: handleAddToContacts(); return true; @@ -650,28 +649,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity new GroupMembersDialog(this, getRecipients()).display(); } - private void handleDeleteThread() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.ConversationActivity_delete_thread_question); - builder.setIconAttribute(R.attr.dialog_alert_icon); - builder.setCancelable(true); - builder.setMessage(R.string.ConversationActivity_this_will_permanently_delete_all_messages_in_this_conversation); - builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (threadId > 0) { - DatabaseFactory.getThreadDatabase(ConversationActivity.this).deleteConversation(threadId); - } - composeText.getText().clear(); - threadId = -1; - finish(); - } - }); - - builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); - } - private void handleAddToContacts() { final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); intent.putExtra(ContactsContract.Intents.Insert.PHONE, recipients.getPrimaryRecipient().getNumber()); @@ -1089,9 +1066,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity draftDatabase.insertDrafts(new MasterCipher(thisMasterSecret), threadId, drafts); threadDatabase.updateSnippet(threadId, drafts.getSnippet(ConversationActivity.this), drafts.getUriSnippet(ConversationActivity.this), - System.currentTimeMillis(), Types.BASE_DRAFT_TYPE); + System.currentTimeMillis(), Types.BASE_DRAFT_TYPE, true); } else if (threadId > 0) { - threadDatabase.update(threadId); + threadDatabase.update(threadId, false); } return threadId; diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java index 56fcadc332..a2297fc270 100644 --- a/src/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationFragment.java @@ -57,7 +57,7 @@ import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.sms.MessageSender; -import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask; +import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.thoughtcrime.securesms.util.SaveAttachmentTask; import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment; import org.thoughtcrime.securesms.util.ViewUtil; diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java index 0428eb4ff1..cff90af531 100644 --- a/src/org/thoughtcrime/securesms/ConversationListActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java @@ -161,15 +161,6 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit @Override public void onCreateConversation(long threadId, Recipients recipients, int distributionType) { - createConversation(threadId, recipients, distributionType); - } - - private void createGroup() { - Intent intent = new Intent(this, GroupCreateActivity.class); - startActivity(intent); - } - - private void createConversation(long threadId, Recipients recipients, int distributionType) { Intent intent = new Intent(this, ConversationActivity.class); intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, recipients.getIds()); intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId); @@ -179,6 +170,17 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out); } + @Override + public void onSwitchToArchive() { + Intent intent = new Intent(this, ConversationListArchiveActivity.class); + startActivity(intent); + } + + private void createGroup() { + Intent intent = new Intent(this, GroupCreateActivity.class); + startActivity(intent); + } + private void handleDisplaySettings() { Intent preferencesIntent = new Intent(this, ApplicationPreferencesActivity.class); startActivity(preferencesIntent); diff --git a/src/org/thoughtcrime/securesms/ConversationListAdapter.java b/src/org/thoughtcrime/securesms/ConversationListAdapter.java index 3325b699db..e29737ce82 100644 --- a/src/org/thoughtcrime/securesms/ConversationListAdapter.java +++ b/src/org/thoughtcrime/securesms/ConversationListAdapter.java @@ -49,6 +49,9 @@ import java.util.Set; */ public class ConversationListAdapter extends CursorRecyclerViewAdapter { + private static final int MESSAGE_TYPE_SWITCH_ARCHIVE = 1; + private static final int MESSAGE_TYPE_THREAD = 2; + private final ThreadDatabase threadDatabase; private final MasterSecret masterSecret; private final MasterCipher masterCipher; @@ -61,37 +64,25 @@ public class ConversationListAdapter extends CursorRecyclerViewAdapter ViewHolder(final @NonNull V itemView) { super(itemView); - itemView.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - if (clickListener != null) clickListener.onItemClick(itemView); - } - }); - itemView.setOnLongClickListener(new OnLongClickListener() { - @Override - public boolean onLongClick(View view) { - if (clickListener != null) clickListener.onItemLongClick(itemView); - return true; - } - }); } - public ConversationListItem getItem() { - return (ConversationListItem)itemView; + public BindableConversationListItem getItem() { + return (BindableConversationListItem)itemView; } } @Override public long getItemId(@NonNull Cursor cursor) { - ThreadRecord record = getThreadRecord(cursor); - StringBuilder builder = new StringBuilder(""+record.getThreadId()); + ThreadRecord record = getThreadRecord(cursor); + StringBuilder builder = new StringBuilder("" + record.getThreadId()); + for (long recipientId : record.getRecipients().getIds()) { builder.append("::").append(recipientId); } + return Conversions.byteArrayToLong(digest.digest(builder.toString().getBytes())); } @@ -116,10 +107,51 @@ public class ConversationListAdapter extends CursorRecyclerViewAdapter, ActionMode.Callback, ItemClickListener { + + public static final String ARCHIVE = "archive"; + private MasterSecret masterSecret; private ActionMode actionMode; private RecyclerView list; @@ -76,27 +91,39 @@ public class ConversationListFragment extends Fragment private FloatingActionButton fab; private Locale locale; private String queryFilter = ""; + private boolean archive; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); masterSecret = getArguments().getParcelable("master_secret"); locale = (Locale) getArguments().getSerializable(PassphraseRequiredActionBarActivity.LOCALE_EXTRA); + archive = getArguments().getBoolean(ARCHIVE, false); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { final View view = inflater.inflate(R.layout.conversation_list_fragment, container, false); - reminderView = (ReminderView) view.findViewById(R.id.reminder); - list = (RecyclerView) view.findViewById(R.id.list); - fab = (FloatingActionButton) view.findViewById(R.id.fab); + + reminderView = ViewUtil.findById(view, R.id.reminder); + list = ViewUtil.findById(view, R.id.list); + fab = ViewUtil.findById(view, R.id.fab); + + if (archive) fab.setVisibility(View.GONE); + else fab.setVisibility(View.VISIBLE); + reminderView.setOnDismissListener(new OnDismissListener() { - @Override public void onDismiss() { + @Override + public void onDismiss() { updateReminders(); } }); + list.setHasFixedSize(true); list.setLayoutManager(new LinearLayoutManager(getActivity())); + + new ItemTouchHelper(new ArchiveListenerCallback()).attachToRecyclerView(list); + return view; } @@ -170,6 +197,49 @@ public class ConversationListFragment extends Fragment getLoaderManager().restartLoader(0, null, this); } + private void handleArchiveAllSelected() { + final Set selectedConversations = new HashSet<>(getListAdapter().getBatchSelections()); + final boolean archive = this.archive; + + String snackBarTitle; + + if (archive) snackBarTitle = getString(R.string.ConversationListFragment_moved_conversations_to_inbox); + else snackBarTitle = getString(R.string.ConversationListFragment_archived_conversations); + + new SnackbarAsyncTask(getView(), snackBarTitle, + getString(R.string.ConversationListFragment_undo), + getResources().getColor(R.color.amber_500), + Snackbar.LENGTH_LONG, true) + { + + @Override + protected void onPostExecute(Void result) { + super.onPostExecute(result); + + if (actionMode != null) { + actionMode.finish(); + actionMode = null; + } + } + + @Override + protected void executeAction(@Nullable Void parameter) { + for (long threadId : selectedConversations) { + if (!archive) DatabaseFactory.getThreadDatabase(getActivity()).archiveConversation(threadId); + else DatabaseFactory.getThreadDatabase(getActivity()).unarchiveConversation(threadId); + } + } + + @Override + protected void reverseAction(@Nullable Void parameter) { + for (long threadId : selectedConversations) { + if (!archive) DatabaseFactory.getThreadDatabase(getActivity()).unarchiveConversation(threadId); + else DatabaseFactory.getThreadDatabase(getActivity()).archiveConversation(threadId); + } + } + }.execute(); + } + private void handleDeleteAllSelected() { int conversationsCount = getListAdapter().getBatchSelections().size(); AlertDialog.Builder alert = new AlertDialog.Builder(getActivity()); @@ -225,7 +295,7 @@ public class ConversationListFragment extends Fragment private void handleSelectAllThreads() { getListAdapter().selectAllThreads(); actionMode.setSubtitle(getString(R.string.conversation_fragment_cab__batch_selection_amount, - getListAdapter().getBatchSelections().size())); + getListAdapter().getBatchSelections().size())); } private void handleCreateConversation(long threadId, Recipients recipients, int distributionType) { @@ -234,7 +304,7 @@ public class ConversationListFragment extends Fragment @Override public Loader onCreateLoader(int arg0, Bundle arg1) { - return new ConversationListLoader(getActivity(), queryFilter); + return new ConversationListLoader(getActivity(), queryFilter, archive); } @Override @@ -276,21 +346,30 @@ public class ConversationListFragment extends Fragment getListAdapter().notifyDataSetChanged(); } + @Override + public void onSwitchToArchive() { + ((ConversationSelectedListener)getActivity()).onSwitchToArchive(); + } + public interface ConversationSelectedListener { void onCreateConversation(long threadId, Recipients recipients, int distributionType); + void onSwitchToArchive(); } @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { MenuInflater inflater = getActivity().getMenuInflater(); + + if (archive) inflater.inflate(R.menu.conversation_list_batch_unarchive, menu); + else inflater.inflate(R.menu.conversation_list_batch_archive, menu); + inflater.inflate(R.menu.conversation_list_batch, menu); mode.setTitle(R.string.conversation_fragment_cab__batch_selection_mode); mode.setSubtitle(getString(R.string.conversation_fragment_cab__batch_selection_amount, 1)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - getActivity().getWindow() - .setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar)); + getActivity().getWindow().setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar)); } return true; @@ -304,8 +383,9 @@ public class ConversationListFragment extends Fragment @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { switch (item.getItemId()) { - case R.id.menu_select_all: handleSelectAllThreads(); return true; - case R.id.menu_delete_selected: handleDeleteAllSelected(); return true; + case R.id.menu_select_all: handleSelectAllThreads(); return true; + case R.id.menu_delete_selected: handleDeleteAllSelected(); return true; + case R.id.menu_archive_selected: handleArchiveAllSelected(); return true; } return false; @@ -316,8 +396,7 @@ public class ConversationListFragment extends Fragment getListAdapter().initializeBatchMode(false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - TypedArray color = getActivity().getTheme() - .obtainStyledAttributes(new int[] { android.R.attr.statusBarColor }); + TypedArray color = getActivity().getTheme().obtainStyledAttributes(new int[] {android.R.attr.statusBarColor}); getActivity().getWindow().setStatusBarColor(color.getColor(0, Color.BLACK)); color.recycle(); } @@ -325,6 +404,114 @@ public class ConversationListFragment extends Fragment actionMode = null; } + private class ArchiveListenerCallback extends ItemTouchHelper.SimpleCallback { + + public ArchiveListenerCallback() { + super(0, ItemTouchHelper.RIGHT); + } + + @Override + public boolean onMove(RecyclerView recyclerView, + RecyclerView.ViewHolder viewHolder, + RecyclerView.ViewHolder target) + { + return false; + } + + @Override + public int getSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { + if (viewHolder.itemView instanceof ConversationListItemAction) { + return 0; + } + + if (actionMode != null) { + return 0; + } + + return super.getSwipeDirs(recyclerView, viewHolder); + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { + final long threadId = ((ConversationListItem)viewHolder.itemView).getThreadId(); + + if (archive) { + new SnackbarAsyncTask(getView(), + getString(R.string.ConversationListFragment_moved_conversation_to_inbox), + getString(R.string.ConversationListFragment_undo), + getResources().getColor(R.color.amber_500), + Snackbar.LENGTH_SHORT, false) + { + @Override + protected void executeAction(@Nullable Long parameter) { + DatabaseFactory.getThreadDatabase(getActivity()).unarchiveConversation(threadId); + } + + @Override + protected void reverseAction(@Nullable Long parameter) { + DatabaseFactory.getThreadDatabase(getActivity()).archiveConversation(threadId); + } + }.execute(threadId); + } else { + new SnackbarAsyncTask(getView(), + getString(R.string.ConversationListFragment_archived_conversation), + getString(R.string.ConversationListFragment_undo), + getResources().getColor(R.color.amber_500), + Snackbar.LENGTH_SHORT, false) + { + @Override + protected void executeAction(@Nullable Long parameter) { + DatabaseFactory.getThreadDatabase(getActivity()).archiveConversation(threadId); + } + + @Override + protected void reverseAction(@Nullable Long parameter) { + DatabaseFactory.getThreadDatabase(getActivity()).unarchiveConversation(threadId); + } + }.execute(threadId); + } + } + + @Override + public void onChildDraw(Canvas c, RecyclerView recyclerView, + RecyclerView.ViewHolder viewHolder, + float dX, float dY, int actionState, + boolean isCurrentlyActive) + { + + if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { + View itemView = viewHolder.itemView; + Paint p = new Paint(); + + if (dX > 0) { + Bitmap icon; + + if (archive) icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_unarchive_white_36dp); + else icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_archive_white_36dp); + + p.setColor(getResources().getColor(R.color.green_500)); + + c.drawRect((float) itemView.getLeft(), (float) itemView.getTop(), dX, + (float) itemView.getBottom(), p); + + c.drawBitmap(icon, + (float) itemView.getLeft() + getResources().getDimension(R.dimen.conversation_list_fragment_archive_padding), + (float) itemView.getTop() + ((float) itemView.getBottom() - (float) itemView.getTop() - icon.getHeight())/2, + p); + } + + if (Build.VERSION.SDK_INT >= 11) { + float alpha = 1.0f - Math.abs(dX) / (float) viewHolder.itemView.getWidth(); + viewHolder.itemView.setAlpha(alpha); + viewHolder.itemView.setTranslationX(dX); + } + + } else { + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); + } + } + } + } diff --git a/src/org/thoughtcrime/securesms/ConversationListItem.java b/src/org/thoughtcrime/securesms/ConversationListItem.java index 0f51dce854..366b09158c 100644 --- a/src/org/thoughtcrime/securesms/ConversationListItem.java +++ b/src/org/thoughtcrime/securesms/ConversationListItem.java @@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.ResUtil; +import org.thoughtcrime.securesms.util.ViewUtil; import java.util.Locale; import java.util.Set; @@ -53,7 +54,8 @@ import static org.thoughtcrime.securesms.util.SpanUtil.color; */ public class ConversationListItem extends RelativeLayout - implements Recipients.RecipientsModifiedListener, Unbindable + implements Recipients.RecipientsModifiedListener, + BindableConversationListItem, Unbindable { private final static String TAG = ConversationListItem.class.getSimpleName(); @@ -66,6 +68,7 @@ public class ConversationListItem extends RelativeLayout private TextView subjectView; private FromTextView fromView; private TextView dateView; + private TextView archivedView; private boolean read; private AvatarImageView contactPhotoImage; private ThumbnailView thumbnailView; @@ -94,11 +97,12 @@ public class ConversationListItem extends RelativeLayout this.dateView = (TextView) findViewById(R.id.date); this.contactPhotoImage = (AvatarImageView) findViewById(R.id.contact_photo_image); this.thumbnailView = (ThumbnailView) findViewById(R.id.thumbnail); + this.archivedView = ViewUtil.findById(this, R.id.archived); thumbnailView.setClickable(false); } - public void set(@NonNull MasterSecret masterSecret, @NonNull ThreadRecord thread, - @NonNull Locale locale, @NonNull Set selectedThreads, boolean batchMode) + public void bind(@NonNull MasterSecret masterSecret, @NonNull ThreadRecord thread, + @NonNull Locale locale, @NonNull Set selectedThreads, boolean batchMode) { this.selectedThreads = selectedThreads; this.recipients = thread.getRecipients(); @@ -118,6 +122,12 @@ public class ConversationListItem extends RelativeLayout dateView.setTypeface(read ? LIGHT_TYPEFACE : BOLD_TYPEFACE); } + if (thread.isArchived()) { + this.archivedView.setVisibility(View.VISIBLE); + } else { + this.archivedView.setVisibility(View.GONE); + } + setThumbnailSnippet(masterSecret, thread); setBatchState(batchMode); setBackground(thread); @@ -158,7 +168,7 @@ public class ConversationListItem extends RelativeLayout this.thumbnailView.setVisibility(View.GONE); LayoutParams subjectParams = (RelativeLayout.LayoutParams)this.subjectView.getLayoutParams(); - subjectParams.addRule(RelativeLayout.LEFT_OF, 0); + subjectParams.addRule(RelativeLayout.LEFT_OF, R.id.archived); this.subjectView.setLayoutParams(subjectParams); } } @@ -187,4 +197,5 @@ public class ConversationListItem extends RelativeLayout } }); } + } diff --git a/src/org/thoughtcrime/securesms/ConversationListItemAction.java b/src/org/thoughtcrime/securesms/ConversationListItemAction.java new file mode 100644 index 0000000000..6b49f7360e --- /dev/null +++ b/src/org/thoughtcrime/securesms/ConversationListItemAction.java @@ -0,0 +1,50 @@ +package org.thoughtcrime.securesms; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.database.model.ThreadRecord; +import org.thoughtcrime.securesms.util.ViewUtil; + +import java.util.Locale; +import java.util.Set; + +public class ConversationListItemAction extends LinearLayout implements BindableConversationListItem { + + private TextView description; + + public ConversationListItemAction(Context context) { + super(context); + } + + public ConversationListItemAction(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public ConversationListItemAction(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void onFinishInflate() { + super.onFinishInflate(); + this.description = ViewUtil.findById(this, R.id.description); + } + + @Override + public void bind(@NonNull MasterSecret masterSecret, @NonNull ThreadRecord thread, @NonNull Locale locale, @NonNull Set selectedThreads, boolean batchMode) { + this.description.setText(getContext().getString(R.string.ConversationListItemAction_archived_conversations_d, thread.getCount())); + } + + @Override + public void unbind() { + + } +} diff --git a/src/org/thoughtcrime/securesms/DeviceActivity.java b/src/org/thoughtcrime/securesms/DeviceActivity.java index d92e6a7d5e..0e2c8cf686 100644 --- a/src/org/thoughtcrime/securesms/DeviceActivity.java +++ b/src/org/thoughtcrime/securesms/DeviceActivity.java @@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.push.TextSecureCommunicationFactory; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicTheme; -import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask; +import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libaxolotl.IdentityKeyPair; diff --git a/src/org/thoughtcrime/securesms/DeviceListFragment.java b/src/org/thoughtcrime/securesms/DeviceListFragment.java index 23d8b19eff..762742066d 100644 --- a/src/org/thoughtcrime/securesms/DeviceListFragment.java +++ b/src/org/thoughtcrime/securesms/DeviceListFragment.java @@ -22,7 +22,7 @@ import com.melnykov.fab.FloatingActionButton; import org.thoughtcrime.securesms.database.loaders.DeviceListLoader; import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask; +import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.ViewUtil; import org.whispersystems.textsecure.api.TextSecureAccountManager; diff --git a/src/org/thoughtcrime/securesms/DeviceProvisioningActivity.java b/src/org/thoughtcrime/securesms/DeviceProvisioningActivity.java index 167a8ca212..0cf49f65f0 100644 --- a/src/org/thoughtcrime/securesms/DeviceProvisioningActivity.java +++ b/src/org/thoughtcrime/securesms/DeviceProvisioningActivity.java @@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.push.TextSecureCommunicationFactory; import org.thoughtcrime.securesms.util.Base64; -import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask; +import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libaxolotl.IdentityKeyPair; import org.whispersystems.libaxolotl.InvalidKeyException; diff --git a/src/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/org/thoughtcrime/securesms/GroupCreateActivity.java index c189ce46b8..511283aab6 100644 --- a/src/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/src/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -65,7 +65,7 @@ import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.GroupUtil; -import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask; +import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter; import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter.OnRecipientDeletedListener; import org.thoughtcrime.securesms.util.TextSecurePreferences; diff --git a/src/org/thoughtcrime/securesms/InviteActivity.java b/src/org/thoughtcrime/securesms/InviteActivity.java index 0b83cbe1a3..bf3b79754a 100644 --- a/src/org/thoughtcrime/securesms/InviteActivity.java +++ b/src/org/thoughtcrime/securesms/InviteActivity.java @@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.OutgoingTextMessage; -import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask; +import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener; diff --git a/src/org/thoughtcrime/securesms/ShareFragment.java b/src/org/thoughtcrime/securesms/ShareFragment.java index 0577077dfa..9a0a7b1472 100644 --- a/src/org/thoughtcrime/securesms/ShareFragment.java +++ b/src/org/thoughtcrime/securesms/ShareFragment.java @@ -90,7 +90,7 @@ public class ShareFragment extends ListFragment implements LoaderManager.LoaderC @Override public Loader onCreateLoader(int arg0, Bundle arg1) { - return new ConversationListLoader(getActivity(), null); + return new ConversationListLoader(getActivity(), null, false); } @Override diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index 18cbd066cb..4d16f89676 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -68,7 +68,8 @@ public class DatabaseFactory { private static final int INTRODUCED_DB_OPTIMIZATIONS_VERSION = 21; private static final int INTRODUCED_INVITE_REMINDERS_VERSION = 22; private static final int INTRODUCED_CONVERSATION_LIST_THUMBNAILS_VERSION = 23; - private static final int DATABASE_VERSION = 23; + private static final int INTRODUCED_ARCHIVE_VERSION = 24; + private static final int DATABASE_VERSION = 24; private static final String DATABASE_NAME = "messages.db"; private static final Object lock = new Object(); @@ -778,6 +779,11 @@ public class DatabaseFactory { db.execSQL("ALTER TABLE thread ADD COLUMN snippet_uri TEXT DEFAULT NULL"); } + if (oldVersion < INTRODUCED_ARCHIVE_VERSION) { + db.execSQL("ALTER TABLE thread ADD COLUMN archived INTEGER DEFAULT 0"); + db.execSQL("CREATE INDEX IF NOT EXISTS archived_index ON thread (archived)"); + } + db.setTransactionSuccessful(); db.endTransaction(); } diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java index 037052accb..a942341746 100644 --- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -449,7 +449,7 @@ public class MmsDatabase extends MessagingDatabase { long threadId = getThreadIdForMessage(messageId); - DatabaseFactory.getThreadDatabase(context).update(threadId); + DatabaseFactory.getThreadDatabase(context).update(threadId, true); notifyConversationListeners(threadId); notifyConversationListListeners(); @@ -604,7 +604,7 @@ public class MmsDatabase extends MessagingDatabase { contentValues); DatabaseFactory.getThreadDatabase(context).setUnread(threadId); - DatabaseFactory.getThreadDatabase(context).update(threadId); + DatabaseFactory.getThreadDatabase(context).update(threadId, true); notifyConversationListeners(threadId); jobManager.add(new TrimThreadJob(context, threadId)); @@ -692,7 +692,7 @@ public class MmsDatabase extends MessagingDatabase { public void markIncomingNotificationReceived(long threadId) { notifyConversationListeners(threadId); - DatabaseFactory.getThreadDatabase(context).update(threadId); + DatabaseFactory.getThreadDatabase(context).update(threadId, true); if (org.thoughtcrime.securesms.util.Util.isDefaultSmsProvider(context)) { DatabaseFactory.getThreadDatabase(context).setUnread(threadId); @@ -808,7 +808,7 @@ public class MmsDatabase extends MessagingDatabase { db.endTransaction(); notifyConversationListeners(contentValues.getAsLong(THREAD_ID)); - DatabaseFactory.getThreadDatabase(context).update(contentValues.getAsLong(THREAD_ID)); + DatabaseFactory.getThreadDatabase(context).update(contentValues.getAsLong(THREAD_ID), true); } } @@ -821,7 +821,7 @@ public class MmsDatabase extends MessagingDatabase { SQLiteDatabase database = databaseHelper.getWritableDatabase(); database.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""}); - boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId); + boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false); notifyConversationListeners(threadId); return threadDeleted; } diff --git a/src/org/thoughtcrime/securesms/database/PlaintextBackupImporter.java b/src/org/thoughtcrime/securesms/database/PlaintextBackupImporter.java index ac56bc95d0..c1cf58c30d 100644 --- a/src/org/thoughtcrime/securesms/database/PlaintextBackupImporter.java +++ b/src/org/thoughtcrime/securesms/database/PlaintextBackupImporter.java @@ -83,7 +83,7 @@ public class PlaintextBackupImporter { } for (long threadId : modifiedThreads) { - threads.update(threadId); + threads.update(threadId, true); } Log.w("PlaintextBackupImporter", "Exited loop"); diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java index 0f17ebab66..365c471fae 100644 --- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -118,7 +118,7 @@ public class SmsDatabase extends MessagingDatabase { long threadId = getThreadIdForMessage(id); - DatabaseFactory.getThreadDatabase(context).update(threadId); + DatabaseFactory.getThreadDatabase(context).update(threadId, false); notifyConversationListeners(threadId); notifyConversationListListeners(); } @@ -310,7 +310,7 @@ public class SmsDatabase extends MessagingDatabase { long threadId = getThreadIdForMessage(messageId); - DatabaseFactory.getThreadDatabase(context).update(threadId); + DatabaseFactory.getThreadDatabase(context).update(threadId, true); notifyConversationListeners(threadId); notifyConversationListListeners(); @@ -335,7 +335,7 @@ public class SmsDatabase extends MessagingDatabase { SQLiteDatabase db = databaseHelper.getWritableDatabase(); long newMessageId = db.insert(TABLE_NAME, null, contentValues); - DatabaseFactory.getThreadDatabase(context).update(record.getThreadId()); + DatabaseFactory.getThreadDatabase(context).update(record.getThreadId(), true); notifyConversationListeners(record.getThreadId()); jobManager.add(new TrimThreadJob(context, record.getThreadId())); @@ -372,7 +372,7 @@ public class SmsDatabase extends MessagingDatabase { SQLiteDatabase db = databaseHelper.getWritableDatabase(); long messageId = db.insert(TABLE_NAME, null, values); - DatabaseFactory.getThreadDatabase(context).update(threadId); + DatabaseFactory.getThreadDatabase(context).update(threadId, true); notifyConversationListeners(threadId); jobManager.add(new TrimThreadJob(context, threadId)); @@ -450,7 +450,7 @@ public class SmsDatabase extends MessagingDatabase { DatabaseFactory.getThreadDatabase(context).setUnread(threadId); } - DatabaseFactory.getThreadDatabase(context).update(threadId); + DatabaseFactory.getThreadDatabase(context).update(threadId, true); notifyConversationListeners(threadId); jobManager.add(new TrimThreadJob(context, threadId)); @@ -481,7 +481,7 @@ public class SmsDatabase extends MessagingDatabase { SQLiteDatabase db = databaseHelper.getWritableDatabase(); long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues); - DatabaseFactory.getThreadDatabase(context).update(threadId); + DatabaseFactory.getThreadDatabase(context).update(threadId, true); notifyConversationListeners(threadId); jobManager.add(new TrimThreadJob(context, threadId)); @@ -526,7 +526,7 @@ public class SmsDatabase extends MessagingDatabase { SQLiteDatabase db = databaseHelper.getWritableDatabase(); long threadId = getThreadIdForMessage(messageId); db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""}); - boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId); + boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false); notifyConversationListeners(threadId); return threadDeleted; } diff --git a/src/org/thoughtcrime/securesms/database/SmsMigrator.java b/src/org/thoughtcrime/securesms/database/SmsMigrator.java index 499219b9bf..f78318b7b0 100644 --- a/src/org/thoughtcrime/securesms/database/SmsMigrator.java +++ b/src/org/thoughtcrime/securesms/database/SmsMigrator.java @@ -197,7 +197,7 @@ public class SmsMigrator { } ourSmsDatabase.endTransaction(transaction); - DatabaseFactory.getThreadDatabase(context).update(ourThreadId); + DatabaseFactory.getThreadDatabase(context).update(ourThreadId, true); DatabaseFactory.getThreadDatabase(context).notifyConversationListeners(ourThreadId); } finally { diff --git a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java index c16ab17b01..b468159661 100644 --- a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -59,19 +59,21 @@ public class ThreadDatabase extends Database { public static final String SNIPPET = "snippet"; private static final String SNIPPET_CHARSET = "snippet_cs"; public static final String READ = "read"; - private static final String TYPE = "type"; + public static final String TYPE = "type"; private static final String ERROR = "error"; public static final String SNIPPET_TYPE = "snippet_type"; - private static final String SNIPPET_URI = "snippet_uri"; + public static final String SNIPPET_URI = "snippet_uri"; + public static final String ARCHIVED = "archived"; public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + DATE + " INTEGER DEFAULT 0, " + MESSAGE_COUNT + " INTEGER DEFAULT 0, " + RECIPIENT_IDS + " TEXT, " + SNIPPET + " TEXT, " + SNIPPET_CHARSET + " INTEGER DEFAULT 0, " + READ + " INTEGER DEFAULT 1, " + TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " + - SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL);"; + SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL, " + ARCHIVED + " INTEGER DEFAULT 0);"; public static final String[] CREATE_INDEXS = { "CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + RECIPIENT_IDS + ");", + "CREATE INDEX IF NOT EXISTS archived_index ON " + TABLE_NAME + " (" + ARCHIVED + ");", }; public ThreadDatabase(Context context, SQLiteOpenHelper databaseHelper) { @@ -124,27 +126,36 @@ public class ThreadDatabase extends Database { return db.insert(TABLE_NAME, null, contentValues); } - private void updateThread(long threadId, long count, String body, @Nullable Uri attachment, long date, long type) + private void updateThread(long threadId, long count, String body, @Nullable Uri attachment, long date, long type, boolean unarchive) { - ContentValues contentValues = new ContentValues(4); + ContentValues contentValues = new ContentValues(5); contentValues.put(DATE, date - date % 1000); contentValues.put(MESSAGE_COUNT, count); contentValues.put(SNIPPET, body); contentValues.put(SNIPPET_URI, attachment == null ? null : attachment.toString()); contentValues.put(SNIPPET_TYPE, type); + if (unarchive) { + contentValues.put(ARCHIVED, 0); + } + SQLiteDatabase db = databaseHelper.getWritableDatabase(); db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId + ""}); notifyConversationListListeners(); } - public void updateSnippet(long threadId, String snippet, @Nullable Uri attachment, long date, long type) { + public void updateSnippet(long threadId, String snippet, @Nullable Uri attachment, long date, long type, boolean unarchive) { ContentValues contentValues = new ContentValues(3); contentValues.put(DATE, date - date % 1000); contentValues.put(SNIPPET, snippet); contentValues.put(SNIPPET_TYPE, type); contentValues.put(SNIPPET_URI, attachment == null ? null : attachment.toString()); + + if (unarchive) { + contentValues.put(ARCHIVED, 0); + } + SQLiteDatabase db = databaseHelper.getWritableDatabase(); db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId + ""}); notifyConversationListListeners(); @@ -217,7 +228,7 @@ public class ThreadDatabase extends Database { DatabaseFactory.getSmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, lastTweetDate); DatabaseFactory.getMmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, lastTweetDate); - update(threadId); + update(threadId, false); notifyConversationListeners(threadId); } } finally { @@ -302,12 +313,60 @@ public class ThreadDatabase extends Database { } public Cursor getConversationList() { - SQLiteDatabase db = databaseHelper.getReadableDatabase(); - Cursor cursor = db.query(TABLE_NAME, null, null, null, null, null, DATE + " DESC"); + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + Cursor cursor = db.query(TABLE_NAME, null, ARCHIVED + " = ?", new String[] {"0"}, null, null, DATE + " DESC"); + setNotifyConverationListListeners(cursor); + return cursor; } + public Cursor getArchivedConversationList() { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + Cursor cursor = db.query(TABLE_NAME, null, ARCHIVED + " = ?", new String[] {"1"}, null, null, DATE + " DESC"); + + setNotifyConverationListListeners(cursor); + + return cursor; + } + + public int getArchivedConversationListCount() { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + Cursor cursor = null; + + try { + cursor = db.query(TABLE_NAME, new String[] {"COUNT(*)"}, ARCHIVED + " = ?", + new String[] {"1"}, null, null, null); + + if (cursor != null && cursor.moveToFirst()) { + return cursor.getInt(0); + } + + } finally { + if (cursor != null) cursor.close(); + } + + return 0; + } + + public void archiveConversation(long threadId) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + ContentValues contentValues = new ContentValues(1); + contentValues.put(ARCHIVED, 1); + + db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""}); + notifyConversationListListeners(); + } + + public void unarchiveConversation(long threadId) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + ContentValues contentValues = new ContentValues(1); + contentValues.put(ARCHIVED, 0); + + db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""}); + notifyConversationListListeners(); + } + public void deleteConversation(long threadId) { DatabaseFactory.getSmsDatabase(context).deleteThread(threadId); DatabaseFactory.getMmsDatabase(context).deleteThread(threadId); @@ -317,7 +376,6 @@ public class ThreadDatabase extends Database { notifyConversationListListeners(); } - public void deleteConversations(Set selectedConversations) { DatabaseFactory.getSmsDatabase(context).deleteThreads(selectedConversations); DatabaseFactory.getMmsDatabase(context).deleteThreads(selectedConversations); @@ -399,7 +457,7 @@ public class ThreadDatabase extends Database { return null; } - public boolean update(long threadId) { + public boolean update(long threadId, boolean unarchive) { MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context); long count = mmsSmsDatabase.getConversationCount(threadId); @@ -421,7 +479,10 @@ public class ThreadDatabase extends Database { if (record.isPush()) timestamp = record.getDateSent(); else timestamp = record.getDateReceived(); - updateThread(threadId, count, record.getBody().getBody(), getAttachmentUriFor(record), timestamp, record.getType()); + updateThread(threadId, count, record.getBody().getBody(), + getAttachmentUriFor(record), timestamp, + record.getType(), unarchive); + notifyConversationListListeners(); return false; } else { @@ -456,6 +517,7 @@ public class ThreadDatabase extends Database { public static final int DEFAULT = 2; public static final int BROADCAST = 1; public static final int CONVERSATION = 2; + public static final int ARCHIVE = 3; } public class Reader { @@ -486,10 +548,11 @@ public class ThreadDatabase extends Database { long read = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.READ)); long type = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_TYPE)); int distributionType = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.TYPE)); + boolean archived = cursor.getInt(cursor.getColumnIndex(ThreadDatabase.ARCHIVED)) != 0; Uri snippetUri = getSnippetUri(cursor); return new ThreadRecord(context, body, snippetUri, recipients, date, count, - read == 1, threadId, type, distributionType); + read == 1, threadId, type, distributionType, archived); } private DisplayRecord.Body getPlaintextBody(Cursor cursor) { diff --git a/src/org/thoughtcrime/securesms/database/loaders/ConversationListLoader.java b/src/org/thoughtcrime/securesms/database/loaders/ConversationListLoader.java index d4a34755b2..f0749c40f8 100644 --- a/src/org/thoughtcrime/securesms/database/loaders/ConversationListLoader.java +++ b/src/org/thoughtcrime/securesms/database/loaders/ConversationListLoader.java @@ -2,30 +2,64 @@ package org.thoughtcrime.securesms.database.loaders; import android.content.Context; import android.database.Cursor; +import android.database.MatrixCursor; +import android.database.MergeCursor; import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.util.AbstractCursorLoader; +import java.util.LinkedList; import java.util.List; public class ConversationListLoader extends AbstractCursorLoader { private final String filter; + private final boolean archived; - public ConversationListLoader(Context context, String filter) { + public ConversationListLoader(Context context, String filter, boolean archived) { super(context); - this.filter = filter; + this.filter = filter; + this.archived = archived; } @Override public Cursor getCursor() { - if (filter != null && filter.trim().length() != 0) { - List numbers = ContactAccessor.getInstance().getNumbersForThreadSearchFilter(context, filter); + if (filter != null && filter.trim().length() != 0) return getFilteredConversationList(filter); + else if (!archived) return getUnarchivedConversationList(); + else return getArchivedConversationList(); + } - return DatabaseFactory.getThreadDatabase(context).getFilteredConversationList(numbers); - } else { - return DatabaseFactory.getThreadDatabase(context).getConversationList(); + private Cursor getUnarchivedConversationList() { + List cursorList = new LinkedList<>(); + cursorList.add(DatabaseFactory.getThreadDatabase(context).getConversationList()); + + int archivedCount = DatabaseFactory.getThreadDatabase(context) + .getArchivedConversationListCount(); + + if (archivedCount > 0) { + MatrixCursor switchToArchiveCursor = new MatrixCursor(new String[] { + ThreadDatabase.ID, ThreadDatabase.DATE, ThreadDatabase.MESSAGE_COUNT, + ThreadDatabase.RECIPIENT_IDS, ThreadDatabase.SNIPPET, ThreadDatabase.READ, + ThreadDatabase.TYPE, ThreadDatabase.SNIPPET_TYPE, ThreadDatabase.SNIPPET_URI, + ThreadDatabase.ARCHIVED}, 1); + + switchToArchiveCursor.addRow(new Object[] {-1L, System.currentTimeMillis(), archivedCount, + "-1", null, 1, ThreadDatabase.DistributionTypes.ARCHIVE, 0, null, 0}); + + cursorList.add(switchToArchiveCursor); } + + return new MergeCursor(cursorList.toArray(new Cursor[0])); + } + + private Cursor getArchivedConversationList() { + return DatabaseFactory.getThreadDatabase(context).getArchivedConversationList(); + } + + private Cursor getFilteredConversationList(String filter) { + List numbers = ContactAccessor.getInstance().getNumbersForThreadSearchFilter(context, filter); + return DatabaseFactory.getThreadDatabase(context).getFilteredConversationList(numbers); } } diff --git a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java index 539167646b..471d26da6b 100644 --- a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -44,10 +44,11 @@ public class ThreadRecord extends DisplayRecord { private final long count; private final boolean read; private final int distributionType; + private final boolean archived; public ThreadRecord(@NonNull Context context, @NonNull Body body, @Nullable Uri snippetUri, @NonNull Recipients recipients, long date, long count, boolean read, - long threadId, long snippetType, int distributionType) + long threadId, long snippetType, int distributionType, boolean archived) { super(context, body, recipients, date, date, threadId, snippetType); this.context = context.getApplicationContext(); @@ -55,6 +56,7 @@ public class ThreadRecord extends DisplayRecord { this.count = count; this.read = read; this.distributionType = distributionType; + this.archived = archived; } public @Nullable Uri getSnippetUri() { @@ -124,6 +126,10 @@ public class ThreadRecord extends DisplayRecord { return getDateReceived(); } + public boolean isArchived() { + return archived; + } + public int getDistributionType() { return distributionType; } diff --git a/src/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java index 6e874bf8f3..87be7a2a27 100644 --- a/src/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java @@ -31,7 +31,7 @@ import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.contacts.ContactIdentityManager; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.push.TextSecureCommunicationFactory; -import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask; +import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libaxolotl.util.guava.Optional; import org.whispersystems.textsecure.api.TextSecureAccountManager; diff --git a/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java index 1216b4ff1e..6072414b1f 100644 --- a/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java +++ b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java @@ -13,6 +13,7 @@ import android.widget.Toast; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.mms.PartAuthority; +import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import java.io.File; import java.io.FileOutputStream; diff --git a/src/org/thoughtcrime/securesms/util/ProgressDialogAsyncTask.java b/src/org/thoughtcrime/securesms/util/task/ProgressDialogAsyncTask.java similarity index 96% rename from src/org/thoughtcrime/securesms/util/ProgressDialogAsyncTask.java rename to src/org/thoughtcrime/securesms/util/task/ProgressDialogAsyncTask.java index 6ea6d02f1a..e862d5d4f5 100644 --- a/src/org/thoughtcrime/securesms/util/ProgressDialogAsyncTask.java +++ b/src/org/thoughtcrime/securesms/util/task/ProgressDialogAsyncTask.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.util; +package org.thoughtcrime.securesms.util.task; import android.app.ProgressDialog; import android.content.Context; @@ -7,6 +7,7 @@ import android.os.AsyncTask; import java.lang.ref.WeakReference; public abstract class ProgressDialogAsyncTask extends AsyncTask { + private final WeakReference contextReference; private ProgressDialog progress; private final String title; diff --git a/src/org/thoughtcrime/securesms/util/task/SnackbarAsyncTask.java b/src/org/thoughtcrime/securesms/util/task/SnackbarAsyncTask.java new file mode 100644 index 0000000000..db2fd0fc19 --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/task/SnackbarAsyncTask.java @@ -0,0 +1,94 @@ +package org.thoughtcrime.securesms.util.task; + +import android.app.ProgressDialog; +import android.os.AsyncTask; +import android.support.annotation.Nullable; +import android.support.design.widget.Snackbar; +import android.view.View; + +public abstract class SnackbarAsyncTask + extends AsyncTask + implements View.OnClickListener +{ + + private final View view; + private final String snackbarText; + private final String snackbarActionText; + private final int snackbarActionColor; + private final int snackbarDuration; + private final boolean showProgress; + + private @Nullable Params reversibleParameter; + private @Nullable ProgressDialog progressDialog; + + public SnackbarAsyncTask(View view, + String snackbarText, + String snackbarActionText, + int snackbarActionColor, + int snackbarDuration, + boolean showProgress) + { + this.view = view; + this.snackbarText = snackbarText; + this.snackbarActionText = snackbarActionText; + this.snackbarActionColor = snackbarActionColor; + this.snackbarDuration = snackbarDuration; + this.showProgress = showProgress; + } + + @Override + protected void onPreExecute() { + if (this.showProgress) this.progressDialog = ProgressDialog.show(view.getContext(), "", "", true); + else this.progressDialog = null; + } + + @SafeVarargs + @Override + protected final Void doInBackground(Params... params) { + this.reversibleParameter = params != null && params.length > 0 ?params[0] : null; + executeAction(reversibleParameter); + return null; + } + + @Override + protected void onPostExecute(Void result) { + if (this.showProgress && this.progressDialog != null) { + this.progressDialog.dismiss(); + this.progressDialog = null; + } + + Snackbar.make(view, snackbarText, snackbarDuration) + .setAction(snackbarActionText, this) + .setActionTextColor(snackbarActionColor) + .show(); + } + + @Override + public void onClick(View v) { + new AsyncTask() { + @Override + protected void onPreExecute() { + if (showProgress) progressDialog = ProgressDialog.show(view.getContext(), "", "", true); + else progressDialog = null; + } + + @Override + protected Void doInBackground(Void... params) { + reverseAction(reversibleParameter); + return null; + } + + @Override + protected void onPostExecute(Void result) { + if (showProgress && progressDialog != null) { + progressDialog.dismiss(); + progressDialog = null; + } + } + }.execute(); + } + + protected abstract void executeAction(@Nullable Params parameter); + protected abstract void reverseAction(@Nullable Params parameter); + +}