Allow saving all attachments of a thread

Closes #3975
This commit is contained in:
Andreas Fehn 2015-08-22 13:03:07 +02:00 committed by Moxie Marlinspike
parent 170a4291de
commit 238471b847
10 changed files with 136 additions and 25 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/save"
android:title="@string/media_overview__save_all"
android:icon="@drawable/ic_save_all_white_24dp"
app:showAsAction="ifRoom"/>
</menu>

View File

@ -161,12 +161,25 @@
<item quantity="other">This will permanently delete all %1$d selected messages.</item>
</plurals>
<string name="ConversationFragment_save_to_sd_card">Save to storage?</string>
<string name="ConversationFragment_saving_this_media_to_storage_warning">Saving this media to storage will allow any other apps on your device to access it.\n\nContinue?</string>
<string name="ConversationFragment_error_while_saving_attachment_to_sd_card">Error while saving attachment to storage!</string>
<plurals name="ConversationFragment_saving_n_media_to_storage_warning">
<item quantity="one">Saving this media to storage will allow any other apps on your device to access it.\n\nContinue?</item>
<item quantity="other">Saving all %1$d media to storage will allow any other apps on your device to access them.\n\nContinue?</item>
</plurals>
<plurals name="ConversationFragment_error_while_saving_attachments_to_sd_card">
<item quantity="one">Error while saving attachment to storage!</item>
<item quantity="other">Error while saving attachments to storage!</item>
</plurals>
<string name="ConversationFragment_success_exclamation">Success!</string>
<string name="ConversationFragment_unable_to_write_to_sd_card_exclamation">Unable to write to storage!</string>
<string name="ConversationFragment_saving_attachment">Saving attachment</string>
<string name="ConversationFragment_saving_attachment_to_sd_card">Saving attachment to storage...</string>
<plurals name="ConversationFragment_saving_n_attachments">
<item quantity="one">Saving attachment</item>
<item quantity="other">Saving %1$d attachments</item>
</plurals>
<plurals name="ConversationFragment_saving_n_attachments_to_sd_card">
<item quantity="one">Saving attachment to storage...</item>
<item quantity="other">Saving %1$d attachments to storage...</item>
</plurals>
<string name="ConversationFragment_collecting_attahments">Collecting attachments...</string>
<string name="ConversationFragment_pending">Pending...</string>
<string name="ConversationFragment_push">Data (Signal)</string>
<string name="ConversationFragment_mms">MMS</string>
@ -1165,6 +1178,9 @@
<!-- media_preview -->
<string name="media_preview__save_title">Save</string>
<!-- media_overview -->
<string name="media_overview__save_all">Save all</string>
<!-- media_preview_activity -->
<string name="media_preview_activity__image_content_description">Image preview</string>

View File

@ -348,7 +348,9 @@ public class ConversationFragment extends Fragment
}
Log.w(TAG, "No slide with attachable media found, failing nicely.");
Toast.makeText(getActivity(), R.string.ConversationFragment_error_while_saving_attachment_to_sd_card, Toast.LENGTH_LONG).show();
Toast.makeText(getActivity(),
getResources().getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1),
Toast.LENGTH_LONG).show();
}
});
}

View File

@ -18,6 +18,7 @@ package org.thoughtcrime.securesms;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.database.Cursor;
import android.os.Build.VERSION;
@ -29,6 +30,8 @@ import android.support.v4.content.Loader;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
@ -37,11 +40,17 @@ import android.widget.TextView;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ImageDatabase.ImageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipient.RecipientModifiedListener;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.util.AbstractCursorLoader;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import java.util.ArrayList;
import java.util.List;
/**
* Activity for displaying media attachments in-app
@ -137,12 +146,62 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity i
}
}
private void saveToDisk() {
final Context c = this;
SaveAttachmentTask.showWarningDialog(this, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
new ProgressDialogAsyncTask<Void, Void, List<SaveAttachmentTask.Attachment>>(c,
R.string.ConversationFragment_collecting_attahments,
R.string.please_wait) {
@Override
protected List<SaveAttachmentTask.Attachment> doInBackground(Void... params) {
Cursor cursor = DatabaseFactory.getImageDatabase(c).getImagesForThread(threadId);
List<SaveAttachmentTask.Attachment> attachments = new ArrayList<>(cursor.getCount());
while (cursor != null && cursor.moveToNext()) {
ImageRecord record = ImageRecord.from(cursor);
attachments.add(new SaveAttachmentTask.Attachment(record.getAttachment().getDataUri(),
record.getContentType(),
record.getDate()));
}
return attachments;
}
@Override
protected void onPostExecute(List<SaveAttachmentTask.Attachment> attachments) {
super.onPostExecute(attachments);
SaveAttachmentTask saveTask = new SaveAttachmentTask(c, masterSecret, attachments.size());
saveTask.execute(attachments.toArray(new SaveAttachmentTask.Attachment[attachments.size()]));
}
}.execute();
}
}, gridView.getAdapter().getItemCount());
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
menu.clear();
if (gridView.getAdapter() != null && gridView.getAdapter().getItemCount() > 0) {
MenuInflater inflater = this.getMenuInflater();
inflater.inflate(R.menu.media_overview, menu);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home: finish(); return true;
case R.id.save: saveToDisk(); return true;
case android.R.id.home: finish(); return true;
}
return false;
@ -158,6 +217,7 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity i
Log.w(TAG, "onLoadFinished()");
gridView.setAdapter(new ImageMediaAdapter(this, masterSecret, cursor));
noImages.setVisibility(gridView.getAdapter().getItemCount() > 0 ? View.GONE : View.VISIBLE);
invalidateOptionsMenu();
}
@Override

View File

@ -33,18 +33,26 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
private final WeakReference<Context> contextReference;
private final WeakReference<MasterSecret> masterSecretReference;
private final int attachmentCount;
public SaveAttachmentTask(Context context, MasterSecret masterSecret) {
super(context, R.string.ConversationFragment_saving_attachment, R.string.ConversationFragment_saving_attachment_to_sd_card);
this(context, masterSecret, 1);
}
public SaveAttachmentTask(Context context, MasterSecret masterSecret, int count) {
super(context,
context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_attachments, count, count),
context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_attachments_to_sd_card, count, count));
this.contextReference = new WeakReference<>(context);
this.masterSecretReference = new WeakReference<>(masterSecret);
this.attachmentCount = count;
}
@Override
protected Integer doInBackground(SaveAttachmentTask.Attachment... attachments) {
if (attachments == null || attachments.length != 1 || attachments[0] == null) {
throw new AssertionError("must pass in exactly one attachment");
if (attachments == null || attachments.length == 0) {
throw new AssertionError("must pass in at least one attachment");
}
Attachment attachment = attachments[0];
try {
Context context = contextReference.get();
@ -58,20 +66,12 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
return FAILURE;
}
String contentType = MediaUtil.getCorrectedMimeType(attachment.contentType);
File mediaFile = constructOutputFile(contentType, attachment.date);
InputStream inputStream = PartAuthority.getAttachmentStream(context, masterSecret, attachment.uri);
if (inputStream == null) {
return FAILURE;
for (Attachment attachment : attachments) {
if (attachment != null && !saveAttachment(context, masterSecret, attachment)) {
return FAILURE;
}
}
OutputStream outputStream = new FileOutputStream(mediaFile);
Util.copy(inputStream, outputStream);
MediaScannerConnection.scanFile(context, new String[]{mediaFile.getAbsolutePath()},
new String[]{contentType}, null);
return SUCCESS;
} catch (IOException ioe) {
Log.w(TAG, ioe);
@ -79,6 +79,24 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
}
}
private boolean saveAttachment(Context context, MasterSecret masterSecret, Attachment attachment) throws IOException {
String contentType = MediaUtil.getCorrectedMimeType(attachment.contentType);
File mediaFile = constructOutputFile(contentType, attachment.date);
InputStream inputStream = PartAuthority.getAttachmentStream(context, masterSecret, attachment.uri);
if (inputStream == null) {
return false;
}
OutputStream outputStream = new FileOutputStream(mediaFile);
Util.copy(inputStream, outputStream);
MediaScannerConnection.scanFile(context, new String[]{mediaFile.getAbsolutePath()},
new String[]{contentType}, null);
return true;
}
@Override
protected void onPostExecute(Integer result) {
super.onPostExecute(result);
@ -87,8 +105,10 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
switch (result) {
case FAILURE:
Toast.makeText(context, R.string.ConversationFragment_error_while_saving_attachment_to_sd_card,
Toast.LENGTH_LONG).show();
Toast.makeText(context,
context.getResources().getQuantityText(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card,
attachmentCount),
Toast.LENGTH_LONG).show();
break;
case SUCCESS:
Toast.makeText(context, R.string.ConversationFragment_success_exclamation,
@ -149,11 +169,16 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
}
public static void showWarningDialog(Context context, OnClickListener onAcceptListener) {
showWarningDialog(context, onAcceptListener, 1);
}
public static void showWarningDialog(Context context, OnClickListener onAcceptListener, int count) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.ConversationFragment_save_to_sd_card);
builder.setIconAttribute(R.attr.dialog_alert_icon);
builder.setCancelable(true);
builder.setMessage(R.string.ConversationFragment_saving_this_media_to_storage_warning);
builder.setMessage(context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_media_to_storage_warning,
count, count));
builder.setPositiveButton(R.string.yes, onAcceptListener);
builder.setNegativeButton(R.string.no, null);
builder.show();