diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 1110d5888..fb12c1ace 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -192,6 +192,12 @@ android:windowSoftInputMode="stateHidden" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> + + + + + + + + diff --git a/res/menu/media_preview.xml b/res/menu/media_preview.xml new file mode 100644 index 000000000..8b6dcb739 --- /dev/null +++ b/res/menu/media_preview.xml @@ -0,0 +1,7 @@ + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 1eb89a5b6..209e4e1a8 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -102,13 +102,13 @@ Sender: %1$s\nTransport: %2$s\nSent: %3$s\nReceived: %4$s Confirm message delete Are you sure that you want to permanently delete this message? - Save to SD card? - This media has been stored in an encrypted database. The version you save to the SD card will no longer be encrypted. Would you like to continue? - Error while saving attachment to SD card! + Save to storage? + Saving this media to storage will allow any other apps on your phone to access it.\n\nContinue? + Error while saving attachment to storage! Success! - Unable to write to SD card! + Unable to write to storage! Saving attachment - Saving attachment to SD card... + Saving attachment to storage... Key exchange message... @@ -674,6 +674,7 @@ Manage identity keys Complete key exchange Submit debug logs + Media Preview Import / export @@ -871,6 +872,15 @@ TextSecure can copy your phone\'s SMS messages into its encrypted database. Enable TextSecure messages? Instant delivery, stronger privacy, and no SMS fees. + + + You + + + Save + + + Image Preview diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java index 2e4eedeb6..38c7e41d0 100644 --- a/src/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationFragment.java @@ -2,15 +2,11 @@ package org.thoughtcrime.securesms; import android.app.Activity; import android.app.AlertDialog; -import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; -import android.media.MediaScannerConnection; -import android.os.AsyncTask; import android.os.Bundle; -import android.os.Environment; import android.os.Handler; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; @@ -20,7 +16,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.support.v4.widget.CursorAdapter; -import android.webkit.MimeTypeMap; import android.widget.AdapterView; import android.widget.ListView; import android.widget.Toast; @@ -41,19 +36,12 @@ import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.util.Dialogs; import org.thoughtcrime.securesms.util.DirectoryHelper; +import org.thoughtcrime.securesms.util.SaveAttachmentTask; +import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment; import org.whispersystems.textsecure.crypto.MasterSecret; -import org.whispersystems.textsecure.util.Util; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.ref.WeakReference; import java.sql.Date; import java.text.SimpleDateFormat; -import java.util.List; -import java.util.concurrent.ExecutionException; public class ConversationFragment extends SherlockListFragment implements LoaderManager.LoaderCallbacks @@ -140,17 +128,7 @@ public class ConversationFragment extends SherlockListFragment else resend.setVisible(false); if (messageRecord.isMms() && !messageRecord.isMmsNotification()) { - try { - if (((MediaMmsMessageRecord)messageRecord).getSlideDeck().get().containsMediaSlide()) { - saveAttachment.setVisible(true); - } else { - saveAttachment.setVisible(false); - } - } catch (InterruptedException ie) { - Log.w(TAG, ie); - } catch (ExecutionException ee) { - Log.w(TAG, ee); - } + saveAttachment.setVisible(((MediaMmsMessageRecord)messageRecord).containsMediaSlide()); } else { saveAttachment.setVisible(false); } @@ -260,20 +238,20 @@ public class ConversationFragment extends SherlockListFragment MessageSender.resend(activity, messageId, message.isMms()); } - private void handleSaveAttachment(final MessageRecord message) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(R.string.ConversationFragment_save_to_sd_card); - builder.setIcon(Dialogs.resolveIcon(getActivity(), R.attr.dialog_alert_icon)); - builder.setCancelable(true); - builder.setMessage(R.string.ConversationFragment_this_media_has_been_stored_in_an_encrypted_database_warning); - builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { + private void handleSaveAttachment(final MediaMmsMessageRecord message) { + SaveAttachmentTask.showWarningDialog(getActivity(), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { - SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity()); - saveTask.execute((MediaMmsMessageRecord) message); + + final Slide slide = message.getMediaSlideSync(); + if (slide == null) { + 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(); + return; + } + SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity(), masterSecret); + saveTask.execute(new Attachment(slide.getUri(), slide.getContentType(), message.getDateReceived())); } }); - builder.setNegativeButton(R.string.no, null); - builder.show(); } @Override @@ -358,7 +336,7 @@ public class ConversationFragment extends SherlockListFragment actionMode.finish(); return true; case R.id.menu_context_save_attachment: - handleSaveAttachment(messageRecord); + handleSaveAttachment((MediaMmsMessageRecord)messageRecord); actionMode.finish(); return true; } @@ -366,139 +344,4 @@ public class ConversationFragment extends SherlockListFragment return false; } }; - - private class SaveAttachmentTask extends AsyncTask { - - private static final int SUCCESS = 0; - private static final int FAILURE = 1; - private static final int WRITE_ACCESS_FAILURE = 2; - - private final WeakReference contextReference; - private ProgressDialog progressDialog; - - public SaveAttachmentTask(Context context) { - this.contextReference = new WeakReference(context); - } - - @Override - protected void onPreExecute() { - Context context = contextReference.get(); - - if (context != null) { - progressDialog = ProgressDialog.show(context, - context.getString(R.string.ConversationFragment_saving_attachment), - context.getString(R.string.ConversationFragment_saving_attachment_to_sd_card), - true, false); - } - } - - @Override - protected Integer doInBackground(MediaMmsMessageRecord... messageRecord) { - try { - Context context = contextReference.get(); - - if (!Environment.getExternalStorageDirectory().canWrite()) { - return WRITE_ACCESS_FAILURE; - } - - if (context == null) { - return FAILURE; - } - - Slide slide = getAttachment(messageRecord[0]); - - if (slide == null) { - return FAILURE; - } - - File mediaFile = constructOutputFile(slide, messageRecord[0].getDateReceived()); - InputStream inputStream = slide.getPartDataInputStream(); - OutputStream outputStream = new FileOutputStream(mediaFile); - - Util.copy(inputStream, outputStream); - - MediaScannerConnection.scanFile(context, new String[] {mediaFile.getAbsolutePath()}, - new String[] {slide.getContentType()}, null); - - return SUCCESS; - } catch (IOException ioe) { - Log.w(TAG, ioe); - return FAILURE; - } catch (InterruptedException e) { - throw new AssertionError(e); - } catch (ExecutionException e) { - Log.w(TAG, e); - return FAILURE; - } - } - - @Override - protected void onPostExecute(Integer result) { - Context context = contextReference.get(); - if (context == null) return; - - switch (result) { - case FAILURE: - Toast.makeText(context, R.string.ConversationFragment_error_while_saving_attachment_to_sd_card, - Toast.LENGTH_LONG).show(); - break; - case SUCCESS: - Toast.makeText(context, R.string.ConversationFragment_success_exclamation, - Toast.LENGTH_LONG).show(); - break; - case WRITE_ACCESS_FAILURE: - Toast.makeText(context, R.string.ConversationFragment_unable_to_write_to_sd_card_exclamation, - Toast.LENGTH_LONG).show(); - break; - } - - if (progressDialog != null) - progressDialog.dismiss(); - } - - private Slide getAttachment(MediaMmsMessageRecord record) - throws ExecutionException, InterruptedException - { - List slides = record.getSlideDeck().get().getSlides(); - - for (Slide slide : slides) { - if (slide.hasImage() || slide.hasVideo() || slide.hasAudio()) { - return slide; - } - } - - return null; - } - - private File constructOutputFile(Slide slide, long timestamp) throws IOException { - File sdCard = Environment.getExternalStorageDirectory(); - File outputDirectory; - - if (slide.hasVideo()) { - outputDirectory = new File(sdCard.getAbsoluteFile() + File.separator + "Movies"); - } else if (slide.hasAudio()) { - outputDirectory = new File(sdCard.getAbsolutePath() + File.separator + "Music"); - } else { - outputDirectory = new File(sdCard.getAbsolutePath() + File.separator + "Pictures"); - } - - outputDirectory.mkdirs(); - - MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); - String extension = mimeTypeMap.getExtensionFromMimeType(slide.getContentType()); - SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd-HHmmss"); - String base = "textsecure-" + dateFormatter.format(timestamp); - - if (extension == null) - extension = "attach"; - - int i = 0; - File file = new File(outputDirectory, base + "." + extension); - while (file.exists()) { - file = new File(outputDirectory, base + "-" + (++i) + "." + extension); - } - - return file; - } - } } diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java index ca19a8523..6d7c51623 100644 --- a/src/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/ConversationItem.java @@ -25,7 +25,6 @@ import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.net.Uri; -import android.os.Build; import android.os.Handler; import android.os.Message; import android.provider.Contacts.Intents; @@ -345,7 +344,7 @@ public class ConversationItem extends LinearLayout { mmsContainer.setVisibility(View.GONE); } - slideDeck = messageRecord.getSlideDeck(); + slideDeck = messageRecord.getSlideDeckFuture(); slideDeck.setListener(new FutureTaskListener() { @Override public void onSuccess(final SlideDeck result) { @@ -457,18 +456,28 @@ public class ConversationItem extends LinearLayout { } public void onClick(View v) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.ConversationItem_view_secure_media_question); - builder.setIcon(Dialogs.resolveIcon(context, R.attr.dialog_alert_icon)); - builder.setCancelable(true); - builder.setMessage(R.string.ConversationItem_this_media_has_been_stored_in_an_encrypted_database_external_viewer_warning); - builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - fireIntent(); - } - }); - builder.setNegativeButton(R.string.no, null); - builder.show(); + if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType())) { + Intent intent = new Intent(context, MediaPreviewActivity.class); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setDataAndType(slide.getUri(), slide.getContentType()); + intent.putExtra(MediaPreviewActivity.MASTER_SECRET_EXTRA, masterSecret); + if (!messageRecord.isOutgoing()) intent.putExtra(MediaPreviewActivity.RECIPIENT_EXTRA, messageRecord.getIndividualRecipient()); + intent.putExtra(MediaPreviewActivity.DATE_EXTRA, messageRecord.getDateReceived()); + context.startActivity(intent); + } else { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.ConversationItem_view_secure_media_question); + builder.setIcon(Dialogs.resolveIcon(context, R.attr.dialog_alert_icon)); + builder.setCancelable(true); + builder.setMessage(R.string.ConversationItem_this_media_has_been_stored_in_an_encrypted_database_external_viewer_warning); + builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + fireIntent(); + } + }); + builder.setNegativeButton(R.string.no, null); + builder.show(); + } } } diff --git a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java new file mode 100644 index 000000000..c67d86c9a --- /dev/null +++ b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -0,0 +1,199 @@ +/** + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.thoughtcrime.securesms; + +import android.annotation.TargetApi; +import android.content.ContentUris; +import android.content.DialogInterface; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.WindowManager; +import android.widget.ImageView; +import android.widget.Toast; + +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuInflater; +import com.actionbarsherlock.view.MenuItem; + +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.providers.PartProvider; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.DateUtils; +import org.thoughtcrime.securesms.util.DynamicLanguage; +import org.thoughtcrime.securesms.util.DynamicTheme; +import org.thoughtcrime.securesms.util.SaveAttachmentTask; +import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment; +import org.whispersystems.textsecure.crypto.MasterSecret; + +import java.io.IOException; +import java.io.InputStream; + +import uk.co.senab.photoview.PhotoViewAttacher; + +/** + * Activity for displaying media attachments in-app + */ +public class MediaPreviewActivity extends PassphraseRequiredSherlockActivity { + private final static String TAG = MediaPreviewActivity.class.getSimpleName(); + + public final static String MASTER_SECRET_EXTRA = "master_secret"; + public final static String RECIPIENT_EXTRA = "recipient"; + public final static String DATE_EXTRA = "date"; + + private final DynamicTheme dynamicTheme = new DynamicTheme(); + private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); + + private MasterSecret masterSecret; + + private ImageView image; + private PhotoViewAttacher imageAttacher; + private Uri mediaUri; + private String mediaType; + private Recipient recipient; + private long date; + + @Override + protected void onCreate(Bundle bundle) { + setFullscreenIfPossible(); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + dynamicTheme.onCreate(this); + dynamicLanguage.onCreate(this); + super.onCreate(bundle); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + setContentView(R.layout.media_preview_activity); + initializeResources(); + } + + @TargetApi(VERSION_CODES.HONEYCOMB) + private void setFullscreenIfPossible() { + if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { + getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN); + } + } + + @Override + public void onResume() { + super.onResume(); + dynamicTheme.onResume(this); + dynamicLanguage.onResume(this); + + masterSecret = getIntent().getParcelableExtra(MASTER_SECRET_EXTRA); + + mediaUri = getIntent().getData(); + mediaType = getIntent().getType(); + recipient = getIntent().getParcelableExtra(RECIPIENT_EXTRA); + date = getIntent().getLongExtra(DATE_EXTRA, -1); + + final CharSequence relativeTimeSpan; + if (date > 0) { + relativeTimeSpan = DateUtils.getRelativeTimeSpanString(date, + System.currentTimeMillis(), + DateUtils.MINUTE_IN_MILLIS); + } else { + relativeTimeSpan = null; + } + getSupportActionBar().setTitle(recipient == null ? getString(R.string.MediaPreviewActivity_you) : recipient.getName()); + getSupportActionBar().setSubtitle(relativeTimeSpan); + + if (!isContentTypeSupported(mediaType)) { + Log.w(TAG, "Unsupported media type sent to MediaPreviewActivity, finishing."); + Toast.makeText(getApplicationContext(), "Unsupported media type", Toast.LENGTH_LONG).show(); + finish(); + } + + try { + Log.w(TAG, "Loading Part URI: " + mediaUri); + + final InputStream is = getInputStream(mediaUri, masterSecret); + + if (mediaType != null && mediaType.startsWith("image/")) { + displayImage(is); + } + } catch (IOException ioe) { + Log.w(TAG, ioe); + Toast.makeText(getApplicationContext(), "Could not read the media", Toast.LENGTH_LONG).show(); + finish(); + } + } + + private InputStream getInputStream(Uri uri, MasterSecret masterSecret) throws IOException { + if (PartProvider.isAuthority(uri)) { + return DatabaseFactory.getEncryptingPartDatabase(this, masterSecret).getPartStream(ContentUris.parseId(uri)); + } else { + throw new AssertionError("Given a URI that is not handled by our app."); + } + } + + @Override + public void onPause() { + super.onPause(); + } + + private void initializeResources() { + image = (ImageView) findViewById(R.id.image); + imageAttacher = new PhotoViewAttacher(image); + } + + private void displayImage(final InputStream is) { + image.setImageBitmap(BitmapFactory.decodeStream(is)); + image.setVisibility(View.VISIBLE); + imageAttacher.update(); + } + + private void saveToDisk() { + SaveAttachmentTask.showWarningDialog(this, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this, masterSecret); + saveTask.execute(new Attachment(mediaUri, mediaType, date)); + } + }); + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + + menu.clear(); + MenuInflater inflater = this.getSupportMenuInflater(); + inflater.inflate(R.menu.media_preview, menu); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + super.onOptionsItemSelected(item); + + switch (item.getItemId()) { + case R.id.save: saveToDisk(); return true; + case android.R.id.home: finish(); return true; + } + + return false; + } + + public static boolean isContentTypeSupported(final String contentType) { + return contentType != null && contentType.startsWith("image/"); + } +} diff --git a/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java index 1aff24577..594014217 100644 --- a/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java @@ -18,15 +18,19 @@ package org.thoughtcrime.securesms.database.model; import android.content.Context; import android.text.SpannableString; +import android.util.Log; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.MmsDatabase; -import org.thoughtcrime.securesms.database.SmsDatabase; +import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipients; import org.whispersystems.textsecure.util.ListenableFutureTask; +import java.util.List; +import java.util.concurrent.ExecutionException; + /** * Represents the message record model for MMS messages that contain * media (ie: they've been downloaded). @@ -36,10 +40,11 @@ import org.whispersystems.textsecure.util.ListenableFutureTask; */ public class MediaMmsMessageRecord extends MessageRecord { + private final static String TAG = MediaMmsMessageRecord.class.getSimpleName(); private final Context context; private final int partCount; - private final ListenableFutureTask slideDeck; + private final ListenableFutureTask slideDeckFutureTask; public MediaMmsMessageRecord(Context context, long id, Recipients recipients, Recipient individualRecipient, int recipientDeviceId, @@ -51,15 +56,48 @@ public class MediaMmsMessageRecord extends MessageRecord { super(context, id, body, recipients, individualRecipient, recipientDeviceId, dateSent, dateReceived, threadId, deliveredCount, DELIVERY_STATUS_NONE, mailbox); - this.context = context.getApplicationContext(); - this.partCount = partCount; - this.slideDeck = slideDeck; + this.context = context.getApplicationContext(); + this.partCount = partCount; + this.slideDeckFutureTask = slideDeck; } - public ListenableFutureTask getSlideDeck() { - return slideDeck; + public ListenableFutureTask getSlideDeckFuture() { + return slideDeckFutureTask; } + private SlideDeck getSlideDeckSync() { + try { + return slideDeckFutureTask.get(); + } catch (InterruptedException e) { + Log.w(TAG, e); + return null; + } catch (ExecutionException e) { + Log.w(TAG, e); + return null; + } + } + + public boolean containsMediaSlide() { + SlideDeck deck = getSlideDeckSync(); + return deck != null && deck.containsMediaSlide(); + } + + public Slide getMediaSlideSync() { + SlideDeck deck = getSlideDeckSync(); + if (deck == null) { + return null; + } + List slides = deck.getSlides(); + + for (Slide slide : slides) { + if (slide.hasImage() || slide.hasVideo() || slide.hasAudio()) { + return slide; + } + } + return null; + } + + public int getPartCount() { return partCount; } diff --git a/src/org/thoughtcrime/securesms/util/ProgressDialogAsyncTask.java b/src/org/thoughtcrime/securesms/util/ProgressDialogAsyncTask.java index 215b829d1..fc24120f8 100644 --- a/src/org/thoughtcrime/securesms/util/ProgressDialogAsyncTask.java +++ b/src/org/thoughtcrime/securesms/util/ProgressDialogAsyncTask.java @@ -4,17 +4,19 @@ import android.app.ProgressDialog; import android.content.Context; import android.os.AsyncTask; +import java.lang.ref.WeakReference; + public abstract class ProgressDialogAsyncTask extends AsyncTask { - private final Context context; - private ProgressDialog progress; - private final String title; - private final String message; + private final WeakReference contextReference; + private ProgressDialog progress; + private final String title; + private final String message; public ProgressDialogAsyncTask(Context context, String title, String message) { super(); - this.context = context; - this.title = title; - this.message = message; + this.contextReference = new WeakReference(context); + this.title = title; + this.message = message; } public ProgressDialogAsyncTask(Context context, int title, int message) { @@ -23,7 +25,8 @@ public abstract class ProgressDialogAsyncTask extends @Override protected void onPreExecute() { - progress = ProgressDialog.show(context, title, message, true); + final Context context = contextReference.get(); + if (context != null) progress = ProgressDialog.show(context, title, message, true); } @Override diff --git a/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java new file mode 100644 index 000000000..89621f954 --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java @@ -0,0 +1,162 @@ +package org.thoughtcrime.securesms.util; + +import android.app.AlertDialog; +import android.content.ContentUris; +import android.content.Context; +import android.content.DialogInterface.OnClickListener; +import android.media.MediaScannerConnection; +import android.net.Uri; +import android.os.Environment; +import android.util.Log; +import android.webkit.MimeTypeMap; +import android.widget.Toast; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.providers.PartProvider; +import org.whispersystems.textsecure.crypto.MasterSecret; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.ref.WeakReference; +import java.text.SimpleDateFormat; + +public class SaveAttachmentTask extends ProgressDialogAsyncTask { + private static final String TAG = SaveAttachmentTask.class.getSimpleName(); + + private static final int SUCCESS = 0; + private static final int FAILURE = 1; + private static final int WRITE_ACCESS_FAILURE = 2; + + private final WeakReference contextReference; + private final WeakReference masterSecretReference; + + public SaveAttachmentTask(Context context, MasterSecret masterSecret) { + super(context, R.string.ConversationFragment_saving_attachment, R.string.ConversationFragment_saving_attachment_to_sd_card); + this.contextReference = new WeakReference(context); + this.masterSecretReference = new WeakReference(masterSecret); + } + + @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"); + } + Attachment attachment = attachments[0]; + + try { + Context context = contextReference.get(); + MasterSecret masterSecret = masterSecretReference.get(); + + if (!Environment.getExternalStorageDirectory().canWrite()) { + return WRITE_ACCESS_FAILURE; + } + + if (context == null) { + return FAILURE; + } + + File mediaFile = constructOutputFile(attachment.contentType, attachment.date); + InputStream inputStream = DatabaseFactory.getEncryptingPartDatabase(context, masterSecret).getPartStream(ContentUris.parseId(attachment.uri)); + OutputStream outputStream = new FileOutputStream(mediaFile); + + org.whispersystems.textsecure.util.Util.copy(inputStream, outputStream); + + MediaScannerConnection.scanFile(context, new String[]{mediaFile.getAbsolutePath()}, + new String[]{attachment.contentType}, null); + + return SUCCESS; + } catch (IOException ioe) { + Log.w(TAG, ioe); + return FAILURE; + } + } + + @Override + protected void onPostExecute(Integer result) { + super.onPostExecute(result); + Context context = contextReference.get(); + if (context == null) return; + + switch (result) { + case FAILURE: + Toast.makeText(context, R.string.ConversationFragment_error_while_saving_attachment_to_sd_card, + Toast.LENGTH_LONG).show(); + break; + case SUCCESS: + Toast.makeText(context, R.string.ConversationFragment_success_exclamation, + Toast.LENGTH_LONG).show(); + break; + case WRITE_ACCESS_FAILURE: + Toast.makeText(context, R.string.ConversationFragment_unable_to_write_to_sd_card_exclamation, + Toast.LENGTH_LONG).show(); + break; + } + } + + private File constructOutputFile(String contentType, long timestamp) throws IOException { + File sdCard = Environment.getExternalStorageDirectory(); + File outputDirectory; + + if (contentType.startsWith("video/")) { + outputDirectory = new File(sdCard.getAbsoluteFile() + File.separator + Environment.DIRECTORY_MOVIES); + } else if (contentType.startsWith("audio/")) { + outputDirectory = new File(sdCard.getAbsolutePath() + File.separator + Environment.DIRECTORY_MUSIC); + } else if (contentType.startsWith("image/")) { + outputDirectory = new File(sdCard.getAbsolutePath() + File.separator + Environment.DIRECTORY_PICTURES); + } else { + outputDirectory = new File(sdCard.getAbsolutePath() + File.separator + Environment.DIRECTORY_DOWNLOADS); + } + + if (!outputDirectory.mkdirs()) Log.w(TAG, "mkdirs() returned false, attempting to continue"); + + MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); + String extension = mimeTypeMap.getExtensionFromMimeType(contentType); + SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd-HHmmss"); + String base = "textsecure-" + dateFormatter.format(timestamp); + + if (extension == null) + extension = "attach"; + + int i = 0; + File file = new File(outputDirectory, base + "." + extension); + while (file.exists()) { + file = new File(outputDirectory, base + "-" + (++i) + "." + extension); + } + + return file; + } + + public static class Attachment { + public Uri uri; + public String contentType; + public long date; + + public Attachment(Uri uri, String contentType, long date) { + if (uri == null || contentType == null || date < 0) { + throw new AssertionError("uri, content type, and date must all be specified"); + } + if (!PartProvider.isAuthority(uri)) { + throw new AssertionError("attachment must be a TextSecure attachment"); + } + this.uri = uri; + this.contentType = contentType; + this.date = date; + } + } + + public static void showWarningDialog(Context context, OnClickListener onAcceptListener) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.ConversationFragment_save_to_sd_card); + builder.setIcon(Dialogs.resolveIcon(context, R.attr.dialog_alert_icon)); + builder.setCancelable(true); + builder.setMessage(R.string.ConversationFragment_this_media_has_been_stored_in_an_encrypted_database_warning); + builder.setPositiveButton(R.string.yes, onAcceptListener); + builder.setNegativeButton(R.string.no, null); + builder.show(); + } +} +