From 0c768a24e40b7cbc4c7d6c1e7cd34fcab08d5410 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 13 Mar 2018 18:24:42 -0700 Subject: [PATCH] Allow deletion of media through preview. When viewing a media in the media preview, you can delete it by pressing a delete button on the action bar. It will then ask you to confirm your choice. If you confirm, it will delete the attachment from the database and from disk. If it was the only attachment for that message, the message itself will also be deleted. --- res/menu/media_preview.xml | 4 + res/values/strings.xml | 2 + .../securesms/MediaPreviewActivity.java | 93 ++++++++++++++++--- .../database/AttachmentDatabase.java | 50 ++++++++-- .../securesms/database/Database.java | 13 +++ .../securesms/database/MediaDatabase.java | 12 ++- .../loaders/BucketedThreadMediaLoader.java | 14 ++- 7 files changed, 161 insertions(+), 27 deletions(-) diff --git a/res/menu/media_preview.xml b/res/menu/media_preview.xml index 9e2d676827..0494165551 100644 --- a/res/menu/media_preview.xml +++ b/res/menu/media_preview.xml @@ -13,4 +13,8 @@ android:title="@string/media_preview__all_media_title" android:icon="@drawable/ic_photo_library_white_24dp" app:showAsAction="ifRoom"/> + diff --git a/res/values/strings.xml b/res/values/strings.xml index 0ab7aaf660..eb80d38108 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -664,6 +664,8 @@ Draft Signal needs the Storage permission in order to save to external storage, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Storage\". Unable to save to external storage without permissions + Delete message? + This will permanently delete this message. diff --git a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java index 0ba9349136..976c3580ad 100644 --- a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -20,6 +20,7 @@ import android.Manifest; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; import android.net.Uri; @@ -34,6 +35,7 @@ import android.support.v4.content.Loader; import android.support.v4.util.Pair; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; +import android.support.v7.app.AlertDialog; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -46,9 +48,13 @@ import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.Toast; +import org.thoughtcrime.securesms.attachments.AttachmentId; +import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.components.MediaView; import org.thoughtcrime.securesms.components.viewpager.ExtendedOnPageChangedListener; import org.thoughtcrime.securesms.database.Address; +import org.thoughtcrime.securesms.database.AttachmentDatabase; +import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord; import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader; import org.thoughtcrime.securesms.mms.GlideApp; @@ -251,6 +257,48 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im } } + @SuppressLint("StaticFieldLeak") + private void deleteMedia() { + MediaItem mediaItem = getCurrentMediaItem(); + if (mediaItem == null || mediaItem.attachment == null) { + return; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setIconAttribute(R.attr.dialog_alert_icon); + builder.setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title); + builder.setMessage(R.string.MediaPreviewActivity_media_delete_confirmation_message); + builder.setCancelable(true); + + builder.setPositiveButton(R.string.delete, (dialogInterface, which) -> { + new AsyncTask() { + @Override + protected Void doInBackground(Void... voids) { + 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); + } + return null; + } + }.execute(); + + finish(); + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + } + @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); @@ -258,7 +306,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im menu.clear(); MenuInflater inflater = this.getMenuInflater(); inflater.inflate(R.menu.media_preview, menu); - if (conversationRecipient == null) menu.findItem(R.id.media_preview__overview).setVisible(false); + + if (!isMediaInDb()) { + menu.findItem(R.id.media_preview__overview).setVisible(false); + menu.findItem(R.id.delete).setVisible(false); + } return true; } @@ -271,12 +323,17 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im case R.id.media_preview__overview: showOverview(); return true; case R.id.media_preview__forward: forward(); return true; case R.id.save: saveToDisk(); return true; + case R.id.delete: deleteMedia(); return true; case android.R.id.home: finish(); return true; } return false; } + private boolean isMediaInDb() { + return conversationRecipient != null; + } + private @Nullable MediaItem getCurrentMediaItem() { MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter(); @@ -402,7 +459,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im @Override public MediaItem getMediaItemFor(int position) { - return new MediaItem(null, uri, mediaType, -1, true); + return new MediaItem(null, null, uri, mediaType, -1, true); } @Override @@ -495,6 +552,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im if (mediaRecord.getAttachment().getDataUri() == null) throw new AssertionError(); return new MediaItem(address != null ? Recipient.from(context, address,true) : null, + mediaRecord.getAttachment(), mediaRecord.getAttachment().getDataUri(), mediaRecord.getContentType(), mediaRecord.getDate(), @@ -514,18 +572,26 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im } private static class MediaItem { - private final @Nullable Recipient recipient; - private final @NonNull Uri uri; - private final @NonNull String type; - private final long date; - private final boolean outgoing; + private final @Nullable Recipient recipient; + private final @Nullable DatabaseAttachment attachment; + private final @NonNull Uri uri; + private final @NonNull String type; + private final long date; + private final boolean outgoing; - private MediaItem(@Nullable Recipient recipient, @NonNull Uri uri, @NonNull String type, long date, boolean outgoing) { - this.recipient = recipient; - this.uri = uri; - this.type = type; - this.date = date; - this.outgoing = outgoing; + private MediaItem(@Nullable Recipient recipient, + @Nullable DatabaseAttachment attachment, + @NonNull Uri uri, + @NonNull String type, + long date, + boolean outgoing) + { + this.recipient = recipient; + this.attachment = attachment; + this.uri = uri; + this.type = type; + this.date = date; + this.outgoing = outgoing; } } @@ -533,5 +599,4 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im MediaItem getMediaItemFor(int position); void pause(int position); } - } diff --git a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java index f029babe1e..db796a7f4a 100644 --- a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -234,16 +234,7 @@ public class AttachmentDatabase extends Database { new String[] {mmsId+""}, null, null, null); while (cursor != null && cursor.moveToNext()) { - String data = cursor.getString(0); - String thumbnail = cursor.getString(1); - - if (!TextUtils.isEmpty(data)) { - new File(data).delete(); - } - - if (!TextUtils.isEmpty(thumbnail)) { - new File(thumbnail).delete(); - } + deleteAttachmentOnDisk(cursor.getString(0), cursor.getString(1)); } } finally { if (cursor != null) @@ -251,8 +242,34 @@ public class AttachmentDatabase extends Database { } database.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {mmsId + ""}); + notifyAttachmentListeners(); } + public void deleteAttachment(@NonNull AttachmentId id) { + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + + try (Cursor cursor = database.query(TABLE_NAME, + new String[]{DATA, THUMBNAIL}, + PART_ID_WHERE, + id.toStrings(), + null, + null, + null)) + { + if (cursor == null || !cursor.moveToNext()) { + Log.w(TAG, "Tried to delete an attachment, but it didn't exist."); + return; + } + String data = cursor.getString(0); + String thumbnail = cursor.getString(1); + + database.delete(TABLE_NAME, PART_ID_WHERE, id.toStrings()); + deleteAttachmentOnDisk(data, thumbnail); + notifyAttachmentListeners(); + } + } + + @SuppressWarnings("ResultOfMethodCallIgnored") void deleteAllAttachments() { SQLiteDatabase database = databaseHelper.getWritableDatabase(); @@ -264,6 +281,19 @@ public class AttachmentDatabase extends Database { for (File attachment : attachments) { attachment.delete(); } + + notifyAttachmentListeners(); + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private void deleteAttachmentOnDisk(@Nullable String data, @Nullable String thumbnail) { + if (!TextUtils.isEmpty(data)) { + new File(data).delete(); + } + + if (!TextUtils.isEmpty(thumbnail)) { + new File(thumbnail).delete(); + } } public void insertAttachmentsForPlaceholder(long mmsId, @NonNull AttachmentId attachmentId, @NonNull InputStream inputStream) diff --git a/src/org/thoughtcrime/securesms/database/Database.java b/src/org/thoughtcrime/securesms/database/Database.java index 41cc673267..793fd09e90 100644 --- a/src/org/thoughtcrime/securesms/database/Database.java +++ b/src/org/thoughtcrime/securesms/database/Database.java @@ -17,8 +17,10 @@ package org.thoughtcrime.securesms.database; import android.content.Context; +import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; +import android.support.annotation.NonNull; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; @@ -29,6 +31,7 @@ public abstract class Database { protected static final String ID_WHERE = "_id = ?"; private static final String CONVERSATION_URI = "content://textsecure/thread/"; private static final String CONVERSATION_LIST_URI = "content://textsecure/conversation-list"; + private static final String ATTACHMENT_URI = "content://textsecure/attachment/"; protected SQLCipherOpenHelper databaseHelper; protected final Context context; @@ -59,6 +62,16 @@ public abstract class Database { cursor.setNotificationUri(context.getContentResolver(), Uri.parse(CONVERSATION_LIST_URI)); } + protected void registerAttachmentListeners(@NonNull ContentObserver observer) { + context.getContentResolver().registerContentObserver(Uri.parse(ATTACHMENT_URI), + true, + observer); + } + + protected void notifyAttachmentListeners() { + context.getContentResolver().notifyChange(Uri.parse(ATTACHMENT_URI), null); + } + public void reset(SQLCipherOpenHelper databaseHelper) { this.databaseHelper = databaseHelper; } diff --git a/src/org/thoughtcrime/securesms/database/MediaDatabase.java b/src/org/thoughtcrime/securesms/database/MediaDatabase.java index df30bf517e..e10ce75cee 100644 --- a/src/org/thoughtcrime/securesms/database/MediaDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MediaDatabase.java @@ -1,6 +1,8 @@ package org.thoughtcrime.securesms.database; import android.content.Context; +import android.database.ContentObservable; +import android.database.ContentObserver; import android.database.Cursor; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -55,6 +57,14 @@ public class MediaDatabase extends Database { return cursor; } + public void subscribeToMediaChanges(@NonNull ContentObserver observer) { + registerAttachmentListeners(observer); + } + + public void unsubscribeToMediaChanges(@NonNull ContentObserver observer) { + context.getContentResolver().unregisterContentObserver(observer); + } + public Cursor getDocumentMediaForThread(long threadId) { SQLiteDatabase database = databaseHelper.getReadableDatabase(); Cursor cursor = database.rawQuery(DOCUMENT_MEDIA_QUERY, new String[]{threadId+""}); @@ -98,7 +108,7 @@ public class MediaDatabase extends Database { return new MediaRecord(attachment, address, date, outgoing); } - public Attachment getAttachment() { + public DatabaseAttachment getAttachment() { return attachment; } diff --git a/src/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java b/src/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java index 8962b89ada..b6b7ed1be3 100644 --- a/src/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java +++ b/src/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database.loaders; import android.content.Context; +import android.database.ContentObserver; import android.database.Cursor; import android.support.annotation.NonNull; import android.support.v4.content.AsyncTaskLoader; @@ -10,6 +11,7 @@ import com.annimon.stream.Stream; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.Address; +import org.thoughtcrime.securesms.database.Database; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MediaDatabase; import org.thoughtcrime.securesms.recipients.Recipient; @@ -30,11 +32,13 @@ public class BucketedThreadMediaLoader extends AsyncTaskLoader