in-app image media preview

// FREEBIE
This commit is contained in:
Jake McGinty 2014-08-12 12:11:23 -07:00
parent 503d1ef452
commit 53da1f849a
11 changed files with 498 additions and 206 deletions

View File

@ -192,6 +192,12 @@
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".MediaPreviewActivity"
android:label="@string/AndroidManifest__media_preview"
android:theme="@style/TextSecure.DarkTheme"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DummyActivity"
android:theme="@android:style/Theme.NoDisplay"
android:enabled="true"

View File

@ -36,6 +36,7 @@ dependencies {
compile 'com.astuetz:pagerslidingtabstrip:1.0.1'
compile 'org.w3c:smil:1.0.0'
compile 'org.apache.httpcomponents:httpclient-android:4.3.5'
compile 'com.github.chrisbanes.photoview:library:1.2.3'
androidTestCompile 'com.squareup:fest-android:1.0.8'
androidTestCompile 'com.google.dexmaker:dexmaker:1.1'
@ -59,6 +60,7 @@ dependencyVerification {
'com.madgag:sc-light-jdk15on:931f39d351429fb96c2f749e7ecb1a256a8ebbf5edca7995c9cc085b94d1841d',
'com.googlecode.libphonenumber:libphonenumber:eba17eae81dd622ea89a00a3a8c025b2f25d342e0d9644c5b62e16f15687c3ab',
'org.whispersystems:gson:08f4f7498455d1539c9233e5aac18e9b1805815ef29221572996508eb512fe51',
'com.github.chrisbanes.photoview:library:8b5344e206f125e7ba9d684008f36c4992d03853c57e5814125f88496126e3cc'
]
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/media_preview_activity__image_content_description"
android:visibility="gone"/>
</RelativeLayout>

View File

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

View File

@ -102,13 +102,13 @@
<string name="ConversationFragment_sender_s_transport_s_sent_s_received_s">Sender: %1$s\nTransport: %2$s\nSent: %3$s\nReceived: %4$s</string>
<string name="ConversationFragment_confirm_message_delete">Confirm message delete</string>
<string name="ConversationFragment_are_you_sure_you_want_to_permanently_delete_this_message">Are you sure that you want to permanently delete this message?</string>
<string name="ConversationFragment_save_to_sd_card">Save to SD card?</string>
<string name="ConversationFragment_this_media_has_been_stored_in_an_encrypted_database_warning">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?</string>
<string name="ConversationFragment_error_while_saving_attachment_to_sd_card">Error while saving attachment to SD card!</string>
<string name="ConversationFragment_save_to_sd_card">Save to storage?</string>
<string name="ConversationFragment_this_media_has_been_stored_in_an_encrypted_database_warning">Saving this media to storage will allow any other apps on your phone to access it.\n\nContinue?</string>
<string name="ConversationFragment_error_while_saving_attachment_to_sd_card">Error while saving attachment to storage!</string>
<string name="ConversationFragment_success_exclamation">Success!</string>
<string name="ConversationFragment_unable_to_write_to_sd_card_exclamation">Unable to write to SD card!</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 SD card...</string>
<string name="ConversationFragment_saving_attachment_to_sd_card">Saving attachment to storage...</string>
<!-- ConversationListAdapter -->
<string name="ConversationListAdapter_key_exchange_message">Key exchange message...</string>
@ -674,6 +674,7 @@
<string name="AndroidManifest__manage_identity_keys">Manage identity keys</string>
<string name="AndroidManifest__complete_key_exchange">Complete key exchange</string>
<string name="AndroidManifest__log_submit">Submit debug logs</string>
<string name="AndroidManifest__media_preview">Media Preview</string>
<!-- arrays.xml -->
<string name="arrays__import_export">Import / export</string>
@ -871,6 +872,15 @@
<string name="reminder_header_sms_import_text">TextSecure can copy your phone\'s SMS messages into its encrypted database.</string>
<string name="reminder_header_push_title">Enable TextSecure messages?</string>
<string name="reminder_header_push_text">Instant delivery, stronger privacy, and no SMS fees.</string>
<!-- MediaPreviewActivity -->
<string name="MediaPreviewActivity_you">You</string>
<!-- media_preview -->
<string name="media_preview__save_title">Save</string>
<!-- media_preview_activity -->
<string name="media_preview_activity__image_content_description">Image Preview</string>
<!-- EOF -->
</resources>

View File

@ -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<Cursor>
@ -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<MediaMmsMessageRecord, Void, Integer> {
private static final int SUCCESS = 0;
private static final int FAILURE = 1;
private static final int WRITE_ACCESS_FAILURE = 2;
private final WeakReference<Context> contextReference;
private ProgressDialog progressDialog;
public SaveAttachmentTask(Context context) {
this.contextReference = new WeakReference<Context>(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<Slide> 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;
}
}
}

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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/");
}
}

View File

@ -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> slideDeck;
private final ListenableFutureTask<SlideDeck> 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<SlideDeck> getSlideDeck() {
return slideDeck;
public ListenableFutureTask<SlideDeck> 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<Slide> slides = deck.getSlides();
for (Slide slide : slides) {
if (slide.hasImage() || slide.hasVideo() || slide.hasAudio()) {
return slide;
}
}
return null;
}
public int getPartCount() {
return partCount;
}

View File

@ -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<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
private final Context context;
private ProgressDialog progress;
private final String title;
private final String message;
private final WeakReference<Context> 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>(context);
this.title = title;
this.message = message;
}
public ProgressDialogAsyncTask(Context context, int title, int message) {
@ -23,7 +25,8 @@ public abstract class ProgressDialogAsyncTask<Params, Progress, Result> 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

View File

@ -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<SaveAttachmentTask.Attachment, Void, Integer> {
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<Context> contextReference;
private final WeakReference<MasterSecret> 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>(context);
this.masterSecretReference = new WeakReference<MasterSecret>(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();
}
}