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.
This commit is contained in:
Greyson Parrelli 2018-03-13 18:24:42 -07:00 committed by Moxie Marlinspike
parent a8cf5b8efa
commit 0c768a24e4
7 changed files with 161 additions and 27 deletions

View File

@ -13,4 +13,8 @@
android:title="@string/media_preview__all_media_title"
android:icon="@drawable/ic_photo_library_white_24dp"
app:showAsAction="ifRoom"/>
<item android:id="@+id/delete"
android:title="@string/delete"
android:icon="@drawable/ic_delete_white_24dp"
app:showAsAction="ifRoom"/>
</menu>

View File

@ -664,6 +664,8 @@
<string name="MediaPreviewActivity_draft">Draft</string>
<string name="MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied">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\".</string>
<string name="MediaPreviewActivity_unable_to_write_to_external_storage_without_permission">Unable to save to external storage without permissions</string>
<string name="MediaPreviewActivity_media_delete_confirmation_title">Delete message?</string>
<string name="MediaPreviewActivity_media_delete_confirmation_message">This will permanently delete this message.</string>
<!-- MessageNotifier -->

View File

@ -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<Void, Void, Void>() {
@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);
}
}

View File

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

View File

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

View File

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

View File

@ -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<BucketedThreadMed
@SuppressWarnings("unused")
private static final String TAG = BucketedThreadMediaLoader.class.getSimpleName();
private final Address address;
private final Address address;
private final ContentObserver observer;
public BucketedThreadMediaLoader(@NonNull Context context, @NonNull Address address) {
super(context);
this.address = address;
this.address = address;
this.observer = new ForceLoadContentObserver();
onContentChanged();
}
@ -51,11 +55,17 @@ public class BucketedThreadMediaLoader extends AsyncTaskLoader<BucketedThreadMed
cancelLoad();
}
@Override
protected void onAbandon() {
DatabaseFactory.getMediaDatabase(getContext()).unsubscribeToMediaChanges(observer);
}
@Override
public BucketedThreadMedia loadInBackground() {
BucketedThreadMedia result = new BucketedThreadMedia(getContext());
long threadId = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(Recipient.from(getContext(), address, true));
DatabaseFactory.getMediaDatabase(getContext()).subscribeToMediaChanges(observer);
try (Cursor cursor = DatabaseFactory.getMediaDatabase(getContext()).getGalleryMediaForThread(threadId)) {
while (cursor != null && cursor.moveToNext()) {
result.add(MediaDatabase.MediaRecord.from(getContext(), cursor));