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();
+ }
+}
+