Move media attachment long-click event to context menu.

Long-click on a media attachment will now bring up the normal
context menu for a ConversationItem long-click, but with the
addition of a "save attachment" option.

This allows users to long-click on messages with media in them
and still see the other contextual menu options.

// FREEBIE
This commit is contained in:
Moxie Marlinspike
2014-06-11 18:03:01 -07:00
parent 68747142d6
commit c719a48a2c
9 changed files with 228 additions and 146 deletions

View File

@@ -2,41 +2,61 @@ 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.net.Uri;
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;
import android.text.ClipboardManager;
import android.util.Log;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.support.v4.widget.CursorAdapter;
import android.webkit.MimeTypeMap;
import android.widget.ListView;
import android.widget.Toast;
import com.actionbarsherlock.app.SherlockListFragment;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.loaders.ConversationLoader;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
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.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>
{
private static final String TAG = ConversationFragment.class.getSimpleName();
private ConversationFragmentListener listener;
@@ -66,10 +86,23 @@ public class ConversationFragment extends SherlockListFragment
inflater.inflate(R.menu.conversation_context, menu);
MessageRecord messageRecord = getMessageRecord();
if (messageRecord.isFailed()) {
MenuItem resend = menu.findItem(R.id.menu_context_resend);
resend.setVisible(true);
}
if (messageRecord.isMms() && !messageRecord.isMmsNotification()) {
try {
if (((MediaMmsMessageRecord)messageRecord).getSlideDeck().get().containsMediaSlide()) {
inflater.inflate(R.menu.conversation_context_image, menu);
}
} catch (InterruptedException ie) {
Log.w(TAG, ie);
} catch (ExecutionException ee) {
Log.w(TAG, ee);
}
}
}
@Override
@@ -81,6 +114,7 @@ public class ConversationFragment extends SherlockListFragment
case R.id.menu_context_details: handleDisplayDetails(messageRecord); return true;
case R.id.menu_context_forward: handleForwardMessage(messageRecord); return true;
case R.id.menu_context_resend: handleResendMessage(messageRecord); return true;
case R.id.menu_context_save_attachment:handleSaveAttachment(messageRecord); return true;
}
return false;
@@ -196,11 +230,26 @@ 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() {
public void onClick(DialogInterface dialog, int which) {
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity());
saveTask.execute((MediaMmsMessageRecord) message);
}
});
builder.setNegativeButton(R.string.no, null);
builder.show();
}
private void initializeResources() {
String recipientIds = this.getActivity().getIntent().getStringExtra("recipients");
this.masterSecret = (MasterSecret)this.getActivity().getIntent()
.getParcelableExtra("master_secret");
this.masterSecret = this.getActivity().getIntent().getParcelableExtra("master_secret");
this.recipients = RecipientFactory.getRecipientsForIds(getActivity(), recipientIds, true);
this.threadId = this.getActivity().getIntent().getLongExtra("thread_id", -1);
}
@@ -244,4 +293,130 @@ public class ConversationFragment extends SherlockListFragment
public void setComposeText(String text);
}
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);
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) 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());
if (extension == null)
extension = "attach";
return File.createTempFile("textsecure", "." + extension, outputDirectory);
}
}
}

View File

@@ -17,17 +17,14 @@
package org.thoughtcrime.securesms;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.provider.Contacts.Intents;
@@ -35,12 +32,10 @@ import android.provider.ContactsContract.QuickContact;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.webkit.MimeTypeMap;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
@@ -53,18 +48,12 @@ import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.Emoji;
import org.thoughtcrime.securesms.util.Dialogs;
import org.thoughtcrime.securesms.util.Emoji;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.util.FutureTaskListener;
import org.whispersystems.textsecure.util.ListenableFutureTask;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* A view that displays an individual conversation item within a conversation
* thread. Used by ComposeMessageActivity's ListActivity via a ConversationAdapter.
@@ -182,9 +171,9 @@ public class ConversationItem extends LinearLayout {
setEvents(messageRecord);
setMinimumWidth();
if (messageRecord instanceof NotificationMmsMessageRecord) {
if (messageRecord.isMmsNotification()) {
setNotificationMmsAttributes((NotificationMmsMessageRecord)messageRecord);
} else if (messageRecord instanceof MediaMmsMessageRecord) {
} else if (messageRecord.isMms()) {
setMediaMmsAttributes((MediaMmsMessageRecord)messageRecord);
}
}
@@ -365,9 +354,13 @@ public class ConversationItem extends LinearLayout {
for (Slide slide : result.getSlides()) {
if (slide.hasImage()) {
slide.setThumbnailOn(mmsThumbnail);
// mmsThumbnail.setImageBitmap(slide.getThumbnail());
mmsThumbnail.setOnClickListener(new ThumbnailClickListener(slide));
mmsThumbnail.setOnLongClickListener(new ThumbnailSaveListener(slide));
mmsThumbnail.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return false;
}
});
mmsThumbnail.setVisibility(View.VISIBLE);
return;
}
@@ -439,127 +432,6 @@ public class ConversationItem extends LinearLayout {
context.startActivity(intent);
}
private class ThumbnailSaveListener extends Handler implements View.OnLongClickListener, Runnable, MediaScannerConnection.MediaScannerConnectionClient {
private static final int SUCCESS = 0;
private static final int FAILURE = 1;
private static final int WRITE_ACCESS_FAILURE = 2;
private final Slide slide;
private ProgressDialog progressDialog;
private MediaScannerConnection mediaScannerConnection;
private File mediaFile;
public ThumbnailSaveListener(Slide slide) {
this.slide = slide;
}
public void run() {
if (!Environment.getExternalStorageDirectory().canWrite()) {
this.obtainMessage(WRITE_ACCESS_FAILURE).sendToTarget();
return;
}
try {
mediaFile = constructOutputFile();
InputStream inputStream = slide.getPartDataInputStream();
OutputStream outputStream = new FileOutputStream(mediaFile);
byte[] buffer = new byte[4096];
int read;
while ((read = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, read);
}
outputStream.close();
inputStream.close();
mediaScannerConnection = new MediaScannerConnection(context, this);
mediaScannerConnection.connect();
} catch (IOException ioe) {
Log.w(TAG, ioe);
this.obtainMessage(FAILURE).sendToTarget();
}
}
private File constructOutputFile() 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());
if (extension == null)
extension = "attach";
return File.createTempFile("textsecure", "." + extension, outputDirectory);
}
private void saveToSdCard() {
progressDialog = new ProgressDialog(context);
progressDialog.setTitle(context.getString(R.string.ConversationItem_saving_attachment));
progressDialog.setMessage(context.getString(R.string.ConversationItem_saving_attachment_to_sd_card));
progressDialog.setCancelable(false);
progressDialog.setIndeterminate(true);
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progressDialog.show();
new Thread(this).start();
}
public boolean onLongClick(View v) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.ConversationItem_save_to_sd_card);
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_warning);
builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
saveToSdCard();
}
});
builder.setNegativeButton(R.string.no, null);
builder.show();
return true;
}
@Override
public void handleMessage(Message message) {
switch (message.what) {
case FAILURE:
Toast.makeText(context, R.string.ConversationItem_error_while_saving_attachment_to_sd_card,
Toast.LENGTH_LONG).show();
break;
case SUCCESS:
Toast.makeText(context, R.string.ConversationItem_success_exclamation,
Toast.LENGTH_LONG).show();
break;
case WRITE_ACCESS_FAILURE:
Toast.makeText(context, R.string.ConversationItem_unable_to_write_to_sd_card_exclamation,
Toast.LENGTH_LONG).show();
break;
}
progressDialog.dismiss();
}
public void onMediaScannerConnected() {
mediaScannerConnection.scanFile(mediaFile.getAbsolutePath(), slide.getContentType());
}
public void onScanCompleted(String path, Uri uri) {
mediaScannerConnection.disconnect();
this.obtainMessage(SUCCESS).sendToTarget();
}
}
private class ThumbnailClickListener implements View.OnClickListener {
private final Slide slide;

View File

@@ -68,6 +68,11 @@ public class MediaMmsMessageRecord extends MessageRecord {
return true;
}
@Override
public boolean isMmsNotification() {
return false;
}
@Override
public SpannableString getDisplayBody() {
if (MmsDatabase.Types.isDecryptInProgressType(type)) {

View File

@@ -62,6 +62,7 @@ public abstract class MessageRecord extends DisplayRecord {
}
public abstract boolean isMms();
public abstract boolean isMmsNotification();
public boolean isFailed() {
return

View File

@@ -101,6 +101,11 @@ public class NotificationMmsMessageRecord extends MessageRecord {
return true;
}
@Override
public boolean isMmsNotification() {
return true;
}
@Override
public SpannableString getDisplayBody() {
return emphasisAdded(context.getString(R.string.NotificationMmsMessageRecord_multimedia_message));

View File

@@ -98,6 +98,11 @@ public class SmsMessageRecord extends MessageRecord {
return false;
}
@Override
public boolean isMmsNotification() {
return false;
}
private static int getGenericDeliveryStatus(int status) {
if (status == SmsDatabase.Status.STATUS_NONE) {
return MessageRecord.DELIVERY_STATUS_NONE;

View File

@@ -74,5 +74,15 @@ public class SlideDeck {
public List<Slide> getSlides() {
return slides;
}
public boolean containsMediaSlide() {
for (Slide slide : slides) {
if (slide.hasImage() || slide.hasVideo() || slide.hasAudio()) {
return true;
}
}
return false;
}
}