unjankify incoming media sharing

Closes #4374
Fixes #3989
// FREEBIE
This commit is contained in:
Jake McGinty 2015-10-15 14:40:45 -07:00 committed by Moxie Marlinspike
parent 60ab71099f
commit 59f2446a2b
7 changed files with 216 additions and 109 deletions

View file

@ -1,9 +1,20 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer_layout" xmlns:wheel="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<FrameLayout android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<com.pnikosis.materialishprogress.ProgressWheel android:id="@+id/progress_wheel"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_gravity="center"
wheel:matProg_progressIndeterminate="true" />
</FrameLayout> </FrameLayout>

View file

@ -16,7 +16,6 @@
*/ */
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.annotation.TargetApi;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
@ -98,7 +97,7 @@ import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.providers.CaptureProvider; import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
@ -201,7 +200,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private DynamicTheme dynamicTheme = new DynamicTheme(); private DynamicTheme dynamicTheme = new DynamicTheme();
private DynamicLanguage dynamicLanguage = new DynamicLanguage(); private DynamicLanguage dynamicLanguage = new DynamicLanguage();
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override @Override
protected void onPreCreate() { protected void onPreCreate() {
dynamicTheme.onCreate(this); dynamicTheme.onCreate(this);
@ -311,13 +309,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
switch (reqCode) { switch (reqCode) {
case PICK_IMAGE: case PICK_IMAGE:
boolean isGif = MediaUtil.isGif(MediaUtil.getMimeType(this, data.getData())); boolean isGif = MediaUtil.isGif(MediaUtil.getMimeType(this, data.getData()));
setMedia(data.getData(), isGif ? MediaType.GIF : MediaType.IMAGE, false); setMedia(data.getData(), isGif ? MediaType.GIF : MediaType.IMAGE);
break; break;
case PICK_VIDEO: case PICK_VIDEO:
setMedia(data.getData(), MediaType.VIDEO, false); setMedia(data.getData(), MediaType.VIDEO);
break; break;
case PICK_AUDIO: case PICK_AUDIO:
setMedia(data.getData(), MediaType.AUDIO, false); setMedia(data.getData(), MediaType.AUDIO);
break; break;
case PICK_CONTACT_INFO: case PICK_CONTACT_INFO:
addAttachmentContactInfo(data.getData()); addAttachmentContactInfo(data.getData());
@ -331,7 +329,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
break; break;
case TAKE_PHOTO: case TAKE_PHOTO:
if (attachmentManager.getCaptureUri() != null) { if (attachmentManager.getCaptureUri() != null) {
setMedia(attachmentManager.getCaptureUri(), MediaType.IMAGE, true); setMedia(attachmentManager.getCaptureUri(), MediaType.IMAGE);
} }
break; break;
} }
@ -717,9 +715,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (draftText != null) composeText.setText(draftText); if (draftText != null) composeText.setText(draftText);
if (draftImage != null) setMedia(draftImage, MediaType.IMAGE, false); if (draftImage != null) setMedia(draftImage, MediaType.IMAGE);
else if (draftAudio != null) setMedia(draftAudio, MediaType.AUDIO, false); else if (draftAudio != null) setMedia(draftAudio, MediaType.AUDIO);
else if (draftVideo != null) setMedia(draftVideo, MediaType.VIDEO, false); else if (draftVideo != null) setMedia(draftVideo, MediaType.VIDEO);
if (draftText == null && draftImage == null && draftAudio == null && draftVideo == null) { if (draftText == null && draftImage == null && draftAudio == null && draftVideo == null) {
initializeDraftFromDatabase(); initializeDraftFromDatabase();
@ -753,11 +751,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (draft.getType().equals(Draft.TEXT)) { if (draft.getType().equals(Draft.TEXT)) {
composeText.setText(draft.getValue()); composeText.setText(draft.getValue());
} else if (draft.getType().equals(Draft.IMAGE)) { } else if (draft.getType().equals(Draft.IMAGE)) {
setMedia(Uri.parse(draft.getValue()), MediaType.IMAGE, false); setMedia(Uri.parse(draft.getValue()), MediaType.IMAGE);
} else if (draft.getType().equals(Draft.AUDIO)) { } else if (draft.getType().equals(Draft.AUDIO)) {
setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO, false); setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO);
} else if (draft.getType().equals(Draft.VIDEO)) { } else if (draft.getType().equals(Draft.VIDEO)) {
setMedia(Uri.parse(draft.getValue()), MediaType.VIDEO, false); setMedia(Uri.parse(draft.getValue()), MediaType.VIDEO);
} }
} }
@ -1012,8 +1010,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
} }
private void setMedia(Uri uri, MediaType mediaType, boolean isCapture) { private void setMedia(Uri uri, MediaType mediaType) {
attachmentManager.setMedia(masterSecret, uri, mediaType, getCurrentMediaConstraints(), isCapture); attachmentManager.setMedia(masterSecret, uri, mediaType, getCurrentMediaConstraints());
} }
private void addAttachmentContactInfo(Uri contactUri) { private void addAttachmentContactInfo(Uri contactUri) {
@ -1053,7 +1051,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
drafts.add(new Draft(Draft.TEXT, composeText.getText().toString())); drafts.add(new Draft(Draft.TEXT, composeText.getText().toString()));
} }
for (Slide slide : attachmentManager.getSlideDeck().getSlides()) { for (Slide slide : attachmentManager.buildSlideDeck().getSlides()) {
if (slide.hasAudio()) drafts.add(new Draft(Draft.AUDIO, slide.getUri().toString())); if (slide.hasAudio()) drafts.add(new Draft(Draft.AUDIO, slide.getUri().toString()));
else if (slide.hasVideo()) drafts.add(new Draft(Draft.VIDEO, slide.getUri().toString())); else if (slide.hasVideo()) drafts.add(new Draft(Draft.VIDEO, slide.getUri().toString()));
else if (slide.hasImage()) drafts.add(new Draft(Draft.IMAGE, slide.getUri().toString())); else if (slide.hasImage()) drafts.add(new Draft(Draft.IMAGE, slide.getUri().toString()));
@ -1263,7 +1261,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
{ {
final Context context = getApplicationContext(); final Context context = getApplicationContext();
OutgoingMediaMessage outgoingMessage = new OutgoingMediaMessage(recipients, OutgoingMediaMessage outgoingMessage = new OutgoingMediaMessage(recipients,
attachmentManager.getSlideDeck(), attachmentManager.buildSlideDeck(),
getMessage(), getMessage(),
System.currentTimeMillis(), System.currentTimeMillis(),
distributionType); distributionType);
@ -1336,7 +1334,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override @Override
public void onImageCapture(@NonNull final byte[] imageBytes) { public void onImageCapture(@NonNull final byte[] imageBytes) {
setMedia(CaptureProvider.getInstance(this).create(masterSecret, recipients, imageBytes), MediaType.IMAGE, true); setMedia(PersistentBlobProvider.getInstance(this).create(masterSecret, recipients, imageBytes), MediaType.IMAGE);
quickAttachmentDrawer.hide(false); quickAttachmentDrawer.hide(false);
} }

View file

@ -17,21 +17,29 @@
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.net.URLDecoder; import java.io.IOException;
import java.io.InputStream;
import ws.com.google.android.mms.ContentType; import ws.com.google.android.mms.ContentType;
@ -43,9 +51,17 @@ import ws.com.google.android.mms.ContentType;
public class ShareActivity extends PassphraseRequiredActionBarActivity public class ShareActivity extends PassphraseRequiredActionBarActivity
implements ShareFragment.ConversationSelectedListener implements ShareFragment.ConversationSelectedListener
{ {
private static final String TAG = ShareActivity.class.getSimpleName();
private final DynamicTheme dynamicTheme = new DynamicTheme (); private final DynamicTheme dynamicTheme = new DynamicTheme ();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
private MasterSecret masterSecret;
private ViewGroup fragmentContainer;
private View progressWheel;
private Uri resolvedExtra;
private boolean isPassingAlongMedia;
@Override @Override
protected void onPreCreate() { protected void onPreCreate() {
dynamicTheme.onCreate(this); dynamicTheme.onCreate(this);
@ -54,14 +70,21 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
@Override @Override
protected void onCreate(Bundle icicle, @NonNull MasterSecret masterSecret) { protected void onCreate(Bundle icicle, @NonNull MasterSecret masterSecret) {
this.masterSecret = masterSecret;
setContentView(R.layout.share_activity); setContentView(R.layout.share_activity);
fragmentContainer = ViewUtil.findById(this, R.id.drawer_layout);
progressWheel = ViewUtil.findById(this, R.id.progress_wheel);
initFragment(R.id.drawer_layout, new ShareFragment(), masterSecret); initFragment(R.id.drawer_layout, new ShareFragment(), masterSecret);
initializeMedia();
} }
@Override @Override
protected void onNewIntent(Intent intent) { protected void onNewIntent(Intent intent) {
super.onNewIntent(intent); super.onNewIntent(intent);
setIntent(intent); setIntent(intent);
initializeMedia();
} }
@Override @Override
@ -75,7 +98,46 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
@Override @Override
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
if (!isFinishing()) finish(); if (!isPassingAlongMedia && resolvedExtra != null) {
PersistentBlobProvider.getInstance(this).delete(resolvedExtra);
}
if (!isFinishing()) {
finish();
}
}
private void initializeMedia() {
final Context context = this;
isPassingAlongMedia = false;
fragmentContainer.setVisibility(View.GONE);
progressWheel.setVisibility(View.VISIBLE);
new AsyncTask<Uri, Void, Uri>() {
@Override
protected Uri doInBackground(Uri... uris) {
try {
if (uris.length != 1 || uris[0] == null) {
return null;
}
InputStream input = context.getContentResolver().openInputStream(uris[0]);
if (input == null) {
return null;
}
return PersistentBlobProvider.getInstance(context).create(masterSecret, input);
} catch (IOException ioe) {
Log.w(TAG, ioe);
return null;
}
}
@Override
protected void onPostExecute(Uri uri) {
resolvedExtra = uri;
ViewUtil.fadeIn(fragmentContainer, 300);
ViewUtil.fadeOut(progressWheel, 300);
}
}.execute(getIntent().<Uri>getParcelableExtra(Intent.EXTRA_STREAM));
} }
@Override @Override
@ -100,6 +162,7 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
private void handleNewConversation() { private void handleNewConversation() {
Intent intent = getBaseShareIntent(NewConversationActivity.class); Intent intent = getBaseShareIntent(NewConversationActivity.class);
isPassingAlongMedia = true;
startActivity(intent); startActivity(intent);
} }
@ -114,38 +177,24 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId); intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType); intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
isPassingAlongMedia = true;
startActivity(intent); startActivity(intent);
} }
private Uri getStreamExtra() { private Intent getBaseShareIntent(final @NonNull Class<?> target) {
Uri streamUri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
if (streamUri == null) {
return null;
}
if (streamUri.getAuthority().equals("com.google.android.apps.photos.contentprovider") &&
streamUri.toString().endsWith("/ACTUAL"))
{
String[] parts = streamUri.toString().split("/");
if (parts.length > 3) {
return Uri.parse(URLDecoder.decode(parts[parts.length - 2]));
}
}
return streamUri;
}
private Intent getBaseShareIntent(final Class<?> target) {
final Intent intent = new Intent(this, target); final Intent intent = new Intent(this, target);
final String textExtra = getIntent().getStringExtra(Intent.EXTRA_TEXT); final String textExtra = getIntent().getStringExtra(Intent.EXTRA_TEXT);
final Uri streamExtra = getStreamExtra(); final Uri streamExtra = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
final String type = streamExtra != null ? getMimeType(streamExtra) : getIntent().getType(); final String type = streamExtra != null ? getMimeType(streamExtra) : getIntent().getType();
if (resolvedExtra != null) {
if (ContentType.isImageType(type)) { if (ContentType.isImageType(type)) {
intent.putExtra(ConversationActivity.DRAFT_IMAGE_EXTRA, streamExtra); intent.putExtra(ConversationActivity.DRAFT_IMAGE_EXTRA, resolvedExtra);
} else if (ContentType.isAudioType(type)) { } else if (ContentType.isAudioType(type)) {
intent.putExtra(ConversationActivity.DRAFT_AUDIO_EXTRA, streamExtra); intent.putExtra(ConversationActivity.DRAFT_AUDIO_EXTRA, resolvedExtra);
} else if (ContentType.isVideoType(type)) { } else if (ContentType.isVideoType(type)) {
intent.putExtra(ConversationActivity.DRAFT_VIDEO_EXTRA, streamExtra); intent.putExtra(ConversationActivity.DRAFT_VIDEO_EXTRA, resolvedExtra);
}
} }
intent.putExtra(ConversationActivity.DRAFT_TEXT_EXTRA, textExtra); intent.putExtra(ConversationActivity.DRAFT_TEXT_EXTRA, textExtra);

View file

@ -38,9 +38,11 @@ import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.RemovableMediaView; import org.thoughtcrime.securesms.components.RemovableMediaView;
import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.providers.CaptureProvider; import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.libaxolotl.util.guava.Optional;
import java.io.IOException; import java.io.IOException;
@ -53,18 +55,17 @@ public class AttachmentManager {
private final @NonNull RemovableMediaView removableMediaView; private final @NonNull RemovableMediaView removableMediaView;
private final @NonNull ThumbnailView thumbnail; private final @NonNull ThumbnailView thumbnail;
private final @NonNull AudioView audioView; private final @NonNull AudioView audioView;
private final @NonNull SlideDeck slideDeck;
private final @NonNull AttachmentListener attachmentListener; private final @NonNull AttachmentListener attachmentListener;
private Uri captureUri; private @NonNull Optional<Slide> slide = Optional.absent();
private @Nullable Uri captureUri;
public AttachmentManager(@NonNull Activity view, @NonNull AttachmentListener listener) { public AttachmentManager(@NonNull Activity activity, @NonNull AttachmentListener listener) {
this.attachmentView = view.findViewById(R.id.attachment_editor); this.attachmentView = ViewUtil.findById(activity, R.id.attachment_editor);
this.thumbnail = (ThumbnailView) view.findViewById(R.id.attachment_thumbnail); this.thumbnail = ViewUtil.findById(activity, R.id.attachment_thumbnail);
this.audioView = (AudioView) view.findViewById(R.id.attachment_audio); this.audioView = ViewUtil.findById(activity, R.id.attachment_audio);
this.removableMediaView = (RemovableMediaView) view.findViewById(R.id.removable_media_view); this.removableMediaView = ViewUtil.findById(activity, R.id.removable_media_view);
this.slideDeck = new SlideDeck(); this.context = activity;
this.context = view;
this.attachmentListener = listener; this.attachmentListener = listener;
removableMediaView.setRemoveClickListener(new RemoveButtonListener()); removableMediaView.setRemoveClickListener(new RemoveButtonListener());
@ -76,11 +77,13 @@ public class AttachmentManager {
animation.setAnimationListener(new Animation.AnimationListener() { animation.setAnimationListener(new Animation.AnimationListener() {
@Override @Override
public void onAnimationStart(Animation animation) {} public void onAnimationStart(Animation animation) {}
@Override @Override
public void onAnimationRepeat(Animation animation) {} public void onAnimationRepeat(Animation animation) {}
@Override @Override
public void onAnimationEnd(Animation animation) { public void onAnimationEnd(Animation animation) {
slideDeck.clear(); slide = Optional.absent();
thumbnail.clear(); thumbnail.clear();
attachmentView.setVisibility(View.GONE); attachmentView.setVisibility(View.GONE);
attachmentListener.onAttachmentChanged(); attachmentListener.onAttachmentChanged();
@ -92,26 +95,39 @@ public class AttachmentManager {
} }
public void cleanup() { public void cleanup() {
if (captureUri != null) CaptureProvider.getInstance(context).delete(captureUri); cleanup(captureUri);
cleanup(getSlideUri());
captureUri = null; captureUri = null;
slide = Optional.absent();
}
private void cleanup(final @Nullable Uri uri) {
if (uri != null && PersistentBlobProvider.isAuthority(context, uri)) {
Log.w(TAG, "cleaning up " + uri);
PersistentBlobProvider.getInstance(context).delete(uri);
}
}
private void setSlide(@NonNull Slide slide) {
if (getSlideUri() != null) cleanup(getSlideUri());
if (captureUri != null && slide.getUri() != captureUri) cleanup(captureUri);
this.captureUri = null;
this.slide = Optional.of(slide);
} }
public void setMedia(@NonNull final MasterSecret masterSecret, public void setMedia(@NonNull final MasterSecret masterSecret,
@NonNull final Uri uri, @NonNull final Uri uri,
@NonNull final MediaType mediaType, @NonNull final MediaType mediaType,
@NonNull final MediaConstraints constraints, @NonNull final MediaConstraints constraints)
final boolean isCapture)
{ {
new AsyncTask<Void, Void, Slide>() { new AsyncTask<Void, Void, Slide>() {
@Override @Override
protected void onPreExecute() { protected void onPreExecute() {
slideDeck.clear();
thumbnail.clear(); thumbnail.clear();
thumbnail.showProgressSpinner(); thumbnail.showProgressSpinner();
attachmentView.setVisibility(View.VISIBLE); attachmentView.setVisibility(View.VISIBLE);
if (isCapture) captureUri = uri;
if (!uri.equals(captureUri)) cleanup();
} }
@Override @Override
@ -141,7 +157,7 @@ public class AttachmentManager {
R.string.ConversationActivity_attachment_exceeds_size_limits, R.string.ConversationActivity_attachment_exceeds_size_limits,
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} else { } else {
slideDeck.addSlide(slide); setSlide(slide);
attachmentView.setVisibility(View.VISIBLE); attachmentView.setVisibility(View.VISIBLE);
if (slide.hasAudio()) { if (slide.hasAudio()) {
@ -162,9 +178,10 @@ public class AttachmentManager {
return attachmentView.getVisibility() == View.VISIBLE; return attachmentView.getVisibility() == View.VISIBLE;
} }
public @NonNull SlideDeck buildSlideDeck() {
public @NonNull SlideDeck getSlideDeck() { SlideDeck deck = new SlideDeck();
return slideDeck; if (slide.isPresent()) deck.addSlide(slide.get());
return deck;
} }
public static void selectVideo(Activity activity, int requestCode) { public static void selectVideo(Activity activity, int requestCode) {
@ -184,7 +201,11 @@ public class AttachmentManager {
activity.startActivityForResult(intent, requestCode); activity.startActivityForResult(intent, requestCode);
} }
public Uri getCaptureUri() { private @Nullable Uri getSlideUri() {
return slide.isPresent() ? slide.get().getUri() : null;
}
public @Nullable Uri getCaptureUri() {
return captureUri; return captureUri;
} }
@ -192,7 +213,10 @@ public class AttachmentManager {
try { try {
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) { if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
captureUri = CaptureProvider.getInstance(context).createForExternal(recipients); if (captureUri == null) {
captureUri = PersistentBlobProvider.getInstance(context).createForExternal(recipients);
}
Log.w(TAG, "captureUri path is " + captureUri.getPath());
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, captureUri); captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, captureUri);
activity.startActivityForResult(captureIntent, requestCode); activity.startActivityForResult(captureIntent, requestCode);
} }
@ -237,8 +261,8 @@ public class AttachmentManager {
private class RemoveButtonListener implements View.OnClickListener { private class RemoveButtonListener implements View.OnClickListener {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
clear();
cleanup(); cleanup();
clear();
} }
} }

View file

@ -9,7 +9,7 @@ import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.providers.CaptureProvider; import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.providers.PartProvider; import org.thoughtcrime.securesms.providers.PartProvider;
import org.thoughtcrime.securesms.providers.SingleUseBlobProvider; import org.thoughtcrime.securesms.providers.SingleUseBlobProvider;
@ -25,7 +25,7 @@ public class PartAuthority {
private static final int PART_ROW = 1; private static final int PART_ROW = 1;
private static final int THUMB_ROW = 2; private static final int THUMB_ROW = 2;
private static final int CAPTURE_ROW = 3; private static final int PERSISTENT_ROW = 3;
private static final int SINGLE_USE_ROW = 4; private static final int SINGLE_USE_ROW = 4;
private static final UriMatcher uriMatcher; private static final UriMatcher uriMatcher;
@ -34,7 +34,7 @@ public class PartAuthority {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI("org.thoughtcrime.securesms", "part/*/#", PART_ROW); uriMatcher.addURI("org.thoughtcrime.securesms", "part/*/#", PART_ROW);
uriMatcher.addURI("org.thoughtcrime.securesms", "thumb/*/#", THUMB_ROW); uriMatcher.addURI("org.thoughtcrime.securesms", "thumb/*/#", THUMB_ROW);
uriMatcher.addURI(CaptureProvider.AUTHORITY, CaptureProvider.EXPECTED_PATH, CAPTURE_ROW); uriMatcher.addURI(PersistentBlobProvider.AUTHORITY, PersistentBlobProvider.EXPECTED_PATH, PERSISTENT_ROW);
uriMatcher.addURI(SingleUseBlobProvider.AUTHORITY, SingleUseBlobProvider.PATH, SINGLE_USE_ROW); uriMatcher.addURI(SingleUseBlobProvider.AUTHORITY, SingleUseBlobProvider.PATH, SINGLE_USE_ROW);
} }
@ -50,8 +50,8 @@ public class PartAuthority {
case THUMB_ROW: case THUMB_ROW:
partUri = new PartUriParser(uri); partUri = new PartUriParser(uri);
return DatabaseFactory.getAttachmentDatabase(context).getThumbnailStream(masterSecret, partUri.getPartId()); return DatabaseFactory.getAttachmentDatabase(context).getThumbnailStream(masterSecret, partUri.getPartId());
case CAPTURE_ROW: case PERSISTENT_ROW:
return CaptureProvider.getInstance(context).getStream(masterSecret, ContentUris.parseId(uri)); return PersistentBlobProvider.getInstance(context).getStream(masterSecret, ContentUris.parseId(uri));
case SINGLE_USE_ROW: case SINGLE_USE_ROW:
return SingleUseBlobProvider.getInstance().getStream(ContentUris.parseId(uri)); return SingleUseBlobProvider.getInstance().getStream(ContentUris.parseId(uri));
default: default:

View file

@ -6,7 +6,6 @@ import android.content.UriMatcher;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.util.SparseArrayCompat;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
@ -21,25 +20,27 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class CaptureProvider { public class PersistentBlobProvider {
private static final String TAG = CaptureProvider.class.getSimpleName(); private static final String TAG = PersistentBlobProvider.class.getSimpleName();
private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture"; private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture";
public static final Uri CONTENT_URI = Uri.parse(URI_STRING); public static final Uri CONTENT_URI = Uri.parse(URI_STRING);
public static final String AUTHORITY = "org.thoughtcrime.securesms"; public static final String AUTHORITY = "org.thoughtcrime.securesms";
public static final String EXPECTED_PATH = "capture/*/#"; public static final String EXPECTED_PATH = "capture/*/#";
private static final int MATCH = 1; private static final int MATCH = 1;
public static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH) {{ private static final UriMatcher MATCHER = new UriMatcher(UriMatcher.NO_MATCH) {{
addURI(AUTHORITY, EXPECTED_PATH, MATCH); addURI(AUTHORITY, EXPECTED_PATH, MATCH);
}}; }};
private static volatile CaptureProvider instance; private static volatile PersistentBlobProvider instance;
public static CaptureProvider getInstance(Context context) { public static PersistentBlobProvider getInstance(Context context) {
if (instance == null) { if (instance == null) {
synchronized (CaptureProvider.class) { synchronized (PersistentBlobProvider.class) {
if (instance == null) { if (instance == null) {
instance = new CaptureProvider(context); instance = new PersistentBlobProvider(context);
} }
} }
} }
@ -47,9 +48,9 @@ public class CaptureProvider {
} }
private final Context context; private final Context context;
private final SparseArrayCompat<byte[]> cache = new SparseArrayCompat<>(); private final Map<Long, byte[]> cache = new HashMap<>();
private CaptureProvider(Context context) { private PersistentBlobProvider(Context context) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
} }
@ -57,19 +58,31 @@ public class CaptureProvider {
@NonNull Recipients recipients, @NonNull Recipients recipients,
@NonNull byte[] imageBytes) @NonNull byte[] imageBytes)
{ {
final int id = generateId(recipients); final long id = generateId(recipients);
cache.put(id, imageBytes); cache.put(id, imageBytes);
persistToDisk(masterSecret, id, imageBytes); return create(masterSecret, new ByteArrayInputStream(imageBytes), id);
}
public Uri create(@NonNull MasterSecret masterSecret,
@NonNull InputStream input)
{
return create(masterSecret, input, System.currentTimeMillis());
}
private Uri create(MasterSecret masterSecret, InputStream input, long id) {
persistToDisk(masterSecret, id, input);
final Uri uniqueUri = Uri.withAppendedPath(CONTENT_URI, String.valueOf(System.currentTimeMillis())); final Uri uniqueUri = Uri.withAppendedPath(CONTENT_URI, String.valueOf(System.currentTimeMillis()));
return ContentUris.withAppendedId(uniqueUri, id); return ContentUris.withAppendedId(uniqueUri, id);
} }
private void persistToDisk(final MasterSecret masterSecret, final int id, final byte[] imageBytes) { private void persistToDisk(final MasterSecret masterSecret, final long id,
final InputStream input)
{
new AsyncTask<Void, Void, Void>() { new AsyncTask<Void, Void, Void>() {
@Override protected Void doInBackground(Void... params) { @Override protected Void doInBackground(Void... params) {
try { try {
final OutputStream output = new EncryptingPartOutputStream(getFile(id), masterSecret); final OutputStream output = new EncryptingPartOutputStream(getFile(id), masterSecret);
Util.copy(new ByteArrayInputStream(imageBytes), output); Util.copy(input, output);
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
} }
@ -83,23 +96,21 @@ public class CaptureProvider {
} }
public Uri createForExternal(@NonNull Recipients recipients) throws IOException { public Uri createForExternal(@NonNull Recipients recipients) throws IOException {
final File externalDir = context.getExternalFilesDir(null); return Uri.fromFile(new File(getExternalDir(context), String.valueOf(generateId(recipients)) + ".jpg"))
if (externalDir == null) throw new IOException("no external files directory");
return Uri.fromFile(new File(externalDir, String.valueOf(generateId(recipients)) + ".jpg"))
.buildUpon() .buildUpon()
.appendQueryParameter("unique", String.valueOf(System.currentTimeMillis())) .appendQueryParameter("unique", String.valueOf(System.currentTimeMillis()))
.build(); .build();
} }
public boolean delete(@NonNull Uri uri) { public boolean delete(@NonNull Uri uri) {
switch (uriMatcher.match(uri)) { switch (MATCHER.match(uri)) {
case MATCH: return getFile(ContentUris.parseId(uri)).delete(); case MATCH: return getFile(ContentUris.parseId(uri)).delete();
default: return new File(uri.getPath()).delete(); default: return new File(uri.getPath()).delete();
} }
} }
public @NonNull InputStream getStream(MasterSecret masterSecret, long id) throws IOException { public @NonNull InputStream getStream(MasterSecret masterSecret, long id) throws IOException {
final byte[] cached = cache.get((int)id); final byte[] cached = cache.get(id);
return cached != null ? new ByteArrayInputStream(cached) return cached != null ? new ByteArrayInputStream(cached)
: new DecryptingPartInputStream(getFile(id), masterSecret); : new DecryptingPartInputStream(getFile(id), masterSecret);
} }
@ -111,4 +122,18 @@ public class CaptureProvider {
private File getFile(long id) { private File getFile(long id) {
return new File(context.getDir("captures", Context.MODE_PRIVATE), id + ".jpg"); return new File(context.getDir("captures", Context.MODE_PRIVATE), id + ".jpg");
} }
private static @NonNull File getExternalDir(Context context) throws IOException {
final File externalDir = context.getExternalFilesDir(null);
if (externalDir == null) throw new IOException("no external files directory");
return externalDir;
}
public static boolean isAuthority(@NonNull Context context, @NonNull Uri uri) {
try {
return MATCHER.match(uri) == MATCH || uri.getPath().startsWith(getExternalDir(context).getAbsolutePath());
} catch (IOException ioe) {
return false;
}
}
} }

View file

@ -27,7 +27,7 @@ import java.util.Map;
public class SingleUseBlobProvider { public class SingleUseBlobProvider {
private static final String TAG = CaptureProvider.class.getSimpleName(); private static final String TAG = SingleUseBlobProvider.class.getSimpleName();
public static final String AUTHORITY = "org.thoughtcrime.securesms"; public static final String AUTHORITY = "org.thoughtcrime.securesms";
public static final String PATH = "memory/*/#"; public static final String PATH = "memory/*/#";