diff --git a/res/layout/media_overview_gallery_item.xml b/res/layout/media_overview_gallery_item.xml
index 5c649bdcbb..44112d8af9 100644
--- a/res/layout/media_overview_gallery_item.xml
+++ b/res/layout/media_overview_gallery_item.xml
@@ -11,4 +11,18 @@
android:layout_height="match_parent"
android:contentDescription="@string/media_preview_activity__media_content_description" />
+
+
+
+
+
diff --git a/res/menu/media_overview_context.xml b/res/menu/media_overview_context.xml
new file mode 100644
index 0000000000..4c3c26f781
--- /dev/null
+++ b/res/menu/media_overview_context.xml
@@ -0,0 +1,7 @@
+
+
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 55b19f38f2..f506215986 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -40,4 +40,6 @@
#8cf437
#00FFFFFF
+
+ #88000000
diff --git a/res/values/strings.xml b/res/values/strings.xml
index eb80d38108..65803f27a2 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -401,6 +401,16 @@
Media
+
+ - Delete selected message?
+ - Delete selected messages?
+
+
+ - This will permanently delete the selected message.
+ - This will permanently delete all %1$d selected messages.
+
+ Deleting
+ Deleting messages...
Documents
diff --git a/src/org/thoughtcrime/securesms/MediaGalleryAdapter.java b/src/org/thoughtcrime/securesms/MediaGalleryAdapter.java
index 6942b1c069..526531e0de 100644
--- a/src/org/thoughtcrime/securesms/MediaGalleryAdapter.java
+++ b/src/org/thoughtcrime/securesms/MediaGalleryAdapter.java
@@ -17,7 +17,6 @@
package org.thoughtcrime.securesms;
import android.content.Context;
-import android.content.Intent;
import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
@@ -27,14 +26,16 @@ import android.widget.TextView;
import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter;
import org.thoughtcrime.securesms.components.ThumbnailView;
-import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.util.MediaUtil;
+import java.util.Collection;
+import java.util.HashSet;
import java.util.Locale;
+import java.util.Set;
class MediaGalleryAdapter extends StickyHeaderGridAdapter {
@@ -44,16 +45,19 @@ class MediaGalleryAdapter extends StickyHeaderGridAdapter {
private final Context context;
private final GlideRequests glideRequests;
private final Locale locale;
- private final Address address;
+ private final ItemClickListener itemClickListener;
+ private final Set selected;
private BucketedThreadMedia media;
private static class ViewHolder extends StickyHeaderGridAdapter.ItemViewHolder {
ThumbnailView imageView;
+ View selectedIndicator;
ViewHolder(View v) {
super(v);
- imageView = v.findViewById(R.id.image);
+ imageView = v.findViewById(R.id.image);
+ selectedIndicator = v.findViewById(R.id.selected_indicator);
}
}
@@ -66,14 +70,18 @@ class MediaGalleryAdapter extends StickyHeaderGridAdapter {
}
}
- MediaGalleryAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
- BucketedThreadMedia media, Locale locale, Address address)
+ MediaGalleryAdapter(@NonNull Context context,
+ @NonNull GlideRequests glideRequests,
+ BucketedThreadMedia media,
+ Locale locale,
+ ItemClickListener clickListener)
{
- this.context = context;
- this.glideRequests = glideRequests;
- this.locale = locale;
- this.media = media;
- this.address = address;
+ this.context = context;
+ this.glideRequests = glideRequests;
+ this.locale = locale;
+ this.media = media;
+ this.itemClickListener = clickListener;
+ this.selected = new HashSet<>();
}
public void setMedia(BucketedThreadMedia media) {
@@ -97,16 +105,22 @@ class MediaGalleryAdapter extends StickyHeaderGridAdapter {
@Override
public void onBindItemViewHolder(ItemViewHolder viewHolder, int section, int offset) {
- MediaRecord mediaRecord = media.get(section, offset);
- ThumbnailView thumbnailView = ((ViewHolder)viewHolder).imageView;
-
- Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment());
+ MediaRecord mediaRecord = media.get(section, offset);
+ ThumbnailView thumbnailView = ((ViewHolder)viewHolder).imageView;
+ View selectedIndicator = ((ViewHolder)viewHolder).selectedIndicator;
+ Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment());
if (slide != null) {
thumbnailView.setImageResource(glideRequests, slide, false, false);
}
- thumbnailView.setOnClickListener(new OnMediaClickListener(mediaRecord));
+ thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
+ thumbnailView.setOnLongClickListener(view -> {
+ itemClickListener.onMediaLongClicked(mediaRecord);
+ return true;
+ });
+
+ selectedIndicator.setVisibility(selected.contains(mediaRecord) ? View.VISIBLE : View.GONE);
}
@Override
@@ -119,32 +133,29 @@ class MediaGalleryAdapter extends StickyHeaderGridAdapter {
return media.getSectionItemCount(section);
}
- private class OnMediaClickListener implements View.OnClickListener {
-
- private final MediaRecord mediaRecord;
-
- private OnMediaClickListener(MediaRecord mediaRecord) {
- this.mediaRecord = mediaRecord;
- }
-
- @Override
- public void onClick(View v) {
- if (mediaRecord.getAttachment().getDataUri() != null) {
- Intent intent = new Intent(context, MediaPreviewActivity.class);
- intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate());
- intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.getAttachment().getSize());
- intent.putExtra(MediaPreviewActivity.ADDRESS_EXTRA, address);
- intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, mediaRecord.isOutgoing());
- intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, true);
-
- if (mediaRecord.getAddress() != null) {
- intent.putExtra(MediaPreviewActivity.ADDRESS_EXTRA, mediaRecord.getAddress());
- }
-
- intent.setDataAndType(mediaRecord.getAttachment().getDataUri(), mediaRecord.getContentType());
- context.startActivity(intent);
- }
+ public void toggleSelection(@NonNull MediaRecord mediaRecord) {
+ if (!selected.remove(mediaRecord)) {
+ selected.add(mediaRecord);
}
+ notifyDataSetChanged();
}
+ public int getSelectedMediaCount() {
+ return selected.size();
+ }
+
+ @NonNull
+ public Collection getSelectedMedia() {
+ return new HashSet<>(selected);
+ }
+
+ public void clearSelection() {
+ selected.clear();
+ notifyDataSetChanged();
+ }
+
+ interface ItemClickListener {
+ void onMediaClicked(@NonNull MediaRecord mediaRecord);
+ void onMediaLongClicked(MediaRecord mediaRecord);
+ }
}
diff --git a/src/org/thoughtcrime/securesms/MediaOverviewActivity.java b/src/org/thoughtcrime/securesms/MediaOverviewActivity.java
index 8905c8d2eb..fe8270a195 100644
--- a/src/org/thoughtcrime/securesms/MediaOverviewActivity.java
+++ b/src/org/thoughtcrime/securesms/MediaOverviewActivity.java
@@ -16,8 +16,13 @@
*/
package org.thoughtcrime.securesms;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.Intent;
import android.content.res.Configuration;
+import android.content.res.Resources;
import android.database.Cursor;
+import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.TabLayout;
@@ -27,31 +32,40 @@ import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.view.ViewPager;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.view.ActionMode;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.LayoutInflater;
+import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
+import android.view.Window;
import android.widget.TextView;
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
+import org.thoughtcrime.securesms.database.MediaDatabase;
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader;
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.ViewUtil;
+import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
+import java.util.Collection;
import java.util.Locale;
/**
@@ -186,9 +200,14 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity
}
}
- public static class MediaOverviewGalleryFragment extends MediaOverviewFragment {
+ public static class MediaOverviewGalleryFragment
+ extends MediaOverviewFragment
+ implements MediaGalleryAdapter.ItemClickListener
+ {
private StickyHeaderGridLayoutManager gridManager;
+ private ActionMode actionMode;
+ private ActionModeCallback actionModeCallback = new ActionModeCallback();
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@@ -198,7 +217,11 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity
this.noMedia = ViewUtil.findById(view, R.id.no_images);
this.gridManager = new StickyHeaderGridLayoutManager(getResources().getInteger(R.integer.media_overview_cols));
- this.recyclerView.setAdapter(new MediaGalleryAdapter(getContext(), GlideApp.with(this), new BucketedThreadMedia(getContext()), locale, recipient.getAddress()));
+ this.recyclerView.setAdapter(new MediaGalleryAdapter(getContext(),
+ GlideApp.with(this),
+ new BucketedThreadMedia(getContext()),
+ locale,
+ this));
this.recyclerView.setLayoutManager(gridManager);
this.recyclerView.setHasFixedSize(true);
@@ -232,6 +255,150 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity
public void onLoaderReset(Loader cursorLoader) {
((MediaGalleryAdapter) recyclerView.getAdapter()).setMedia(new BucketedThreadMedia(getContext()));
}
+
+ @Override
+ public void onMediaClicked(@NonNull MediaDatabase.MediaRecord mediaRecord) {
+ if (actionMode != null) {
+ handleMediaMultiSelectClick(mediaRecord);
+ } else {
+ handleMediaPreviewClick(mediaRecord);
+ }
+ }
+
+ private void handleMediaMultiSelectClick(@NonNull MediaDatabase.MediaRecord mediaRecord) {
+ MediaGalleryAdapter adapter = getListAdapter();
+
+ adapter.toggleSelection(mediaRecord);
+ if (adapter.getSelectedMediaCount() == 0) {
+ actionMode.finish();
+ actionMode = null;
+ } else {
+ actionMode.setTitle(String.valueOf(adapter.getSelectedMediaCount()));
+ }
+ }
+
+ private void handleMediaPreviewClick(@NonNull MediaDatabase.MediaRecord mediaRecord) {
+ if (mediaRecord.getAttachment().getDataUri() == null) {
+ return;
+ }
+
+ Context context = getContext();
+ if (context == null) {
+ return;
+ }
+
+ Intent intent = new Intent(context, MediaPreviewActivity.class);
+ intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate());
+ intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.getAttachment().getSize());
+ intent.putExtra(MediaPreviewActivity.ADDRESS_EXTRA, recipient.getAddress());
+ intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, mediaRecord.isOutgoing());
+ intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, true);
+
+ if (mediaRecord.getAddress() != null) {
+ intent.putExtra(MediaPreviewActivity.ADDRESS_EXTRA, mediaRecord.getAddress());
+ }
+
+ intent.setDataAndType(mediaRecord.getAttachment().getDataUri(), mediaRecord.getContentType());
+ context.startActivity(intent);
+ }
+
+ @Override
+ public void onMediaLongClicked(MediaDatabase.MediaRecord mediaRecord) {
+ if (actionMode == null) {
+ ((MediaGalleryAdapter) recyclerView.getAdapter()).toggleSelection(mediaRecord);
+ recyclerView.getAdapter().notifyDataSetChanged();
+
+ actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(actionModeCallback);
+ }
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ private void handleDeleteMedia(@NonNull Collection mediaRecords) {
+ int recordCount = mediaRecords.size();
+ Resources res = getContext().getResources();
+ String confirmTitle = res.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_title,
+ recordCount,
+ recordCount);
+ String confirmMessage = res.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_message,
+ recordCount,
+ recordCount);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+ builder.setIconAttribute(R.attr.dialog_alert_icon);
+ builder.setTitle(confirmTitle);
+ builder.setMessage(confirmMessage);
+ builder.setCancelable(true);
+
+ builder.setPositiveButton(R.string.delete, (dialogInterface, i) -> {
+ new ProgressDialogAsyncTask(getContext(),
+ R.string.MediaOverviewActivity_Media_delete_progress_title,
+ R.string.MediaOverviewActivity_Media_delete_progress_message)
+ {
+ @Override
+ protected Void doInBackground(MediaDatabase.MediaRecord... records) {
+ if (records == null || records.length == 0) {
+ return null;
+ }
+
+ for (MediaDatabase.MediaRecord record : records) {
+ AttachmentUtil.deleteAttachment(getContext(), record.getAttachment());
+ }
+ return null;
+ }
+
+ }.execute(mediaRecords.toArray(new MediaDatabase.MediaRecord[mediaRecords.size()]));
+ });
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.show();
+ }
+
+ private MediaGalleryAdapter getListAdapter() {
+ return (MediaGalleryAdapter) recyclerView.getAdapter();
+ }
+
+ private class ActionModeCallback implements ActionMode.Callback {
+
+ private int originalStatusBarColor;
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ mode.getMenuInflater().inflate(R.menu.media_overview_context, menu);
+ mode.setTitle("1");
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ Window window = getActivity().getWindow();
+ originalStatusBarColor = window.getStatusBarColor();
+ window.setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) {
+ switch (menuItem.getItemId()) {
+ case R.id.delete:
+ handleDeleteMedia(getListAdapter().getSelectedMedia());
+ mode.finish();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ actionMode = null;
+ getListAdapter().clearSelection();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ getActivity().getWindow().setStatusBarColor(originalStatusBarColor);
+ }
+ }
+ }
}
public static class MediaOverviewDocumentsFragment extends MediaOverviewFragment {
diff --git a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
index 976c3580ad..2290fc054e 100644
--- a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
+++ b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
@@ -62,6 +62,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
+import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
@@ -277,18 +278,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
if (mediaItem.attachment == null) {
return null;
}
- Context context = MediaPreviewActivity.this.getApplicationContext();
- AttachmentId attachmentId = mediaItem.attachment.getAttachmentId();
- long mmsId = mediaItem.attachment.getMmsId();
- int attachmentCount = DatabaseFactory.getAttachmentDatabase(context)
- .getAttachmentsForMessage(mmsId)
- .size();
-
- if (attachmentCount <= 1) {
- DatabaseFactory.getMmsDatabase(context).delete(mmsId);
- } else {
- DatabaseFactory.getAttachmentDatabase(context).deleteAttachment(attachmentId);
- }
+ AttachmentUtil.deleteAttachment(MediaPreviewActivity.this.getApplicationContext(),
+ mediaItem.attachment);
return null;
}
}.execute();
diff --git a/src/org/thoughtcrime/securesms/util/AttachmentUtil.java b/src/org/thoughtcrime/securesms/util/AttachmentUtil.java
index 2851670cc1..4975594913 100644
--- a/src/org/thoughtcrime/securesms/util/AttachmentUtil.java
+++ b/src/org/thoughtcrime/securesms/util/AttachmentUtil.java
@@ -6,10 +6,14 @@ import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.Log;
import org.thoughtcrime.securesms.attachments.Attachment;
+import org.thoughtcrime.securesms.attachments.AttachmentId;
+import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
import java.util.Collections;
import java.util.Set;
@@ -36,6 +40,27 @@ public class AttachmentUtil {
}
}
+ /**
+ * Deletes the specified attachment. If its the only attachment for its linked message, the entire
+ * message is deleted.
+ */
+ @WorkerThread
+ public static void deleteAttachment(@NonNull Context context,
+ @NonNull DatabaseAttachment attachment)
+ {
+ AttachmentId attachmentId = attachment.getAttachmentId();
+ long mmsId = attachment.getMmsId();
+ int attachmentCount = DatabaseFactory.getAttachmentDatabase(context)
+ .getAttachmentsForMessage(mmsId)
+ .size();
+
+ if (attachmentCount <= 1) {
+ DatabaseFactory.getMmsDatabase(context).delete(mmsId);
+ } else {
+ DatabaseFactory.getAttachmentDatabase(context).deleteAttachment(attachmentId);
+ }
+ }
+
private static boolean isNonDocumentType(String contentType) {
return
MediaUtil.isImageType(contentType) ||