diff --git a/res/layout/share_activity.xml b/res/layout/share_activity.xml index 366cf777e1..bd30ed1746 100644 --- a/res/layout/share_activity.xml +++ b/res/layout/share_activity.xml @@ -1,9 +1,20 @@ + xmlns:wheel="http://schemas.android.com/apk/res-auto" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + + + diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index e15bddab92..12dacfe0c9 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -16,7 +16,6 @@ */ package org.thoughtcrime.securesms; -import android.annotation.TargetApi; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; 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.Slide; 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.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; @@ -201,7 +200,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity private DynamicTheme dynamicTheme = new DynamicTheme(); private DynamicLanguage dynamicLanguage = new DynamicLanguage(); - @TargetApi(Build.VERSION_CODES.KITKAT) @Override protected void onPreCreate() { dynamicTheme.onCreate(this); @@ -311,13 +309,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity switch (reqCode) { case PICK_IMAGE: 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; case PICK_VIDEO: - setMedia(data.getData(), MediaType.VIDEO, false); + setMedia(data.getData(), MediaType.VIDEO); break; case PICK_AUDIO: - setMedia(data.getData(), MediaType.AUDIO, false); + setMedia(data.getData(), MediaType.AUDIO); break; case PICK_CONTACT_INFO: addAttachmentContactInfo(data.getData()); @@ -331,7 +329,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity break; case TAKE_PHOTO: if (attachmentManager.getCaptureUri() != null) { - setMedia(attachmentManager.getCaptureUri(), MediaType.IMAGE, true); + setMedia(attachmentManager.getCaptureUri(), MediaType.IMAGE); } break; } @@ -717,9 +715,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity if (draftText != null) composeText.setText(draftText); - if (draftImage != null) setMedia(draftImage, MediaType.IMAGE, false); - else if (draftAudio != null) setMedia(draftAudio, MediaType.AUDIO, false); - else if (draftVideo != null) setMedia(draftVideo, MediaType.VIDEO, false); + if (draftImage != null) setMedia(draftImage, MediaType.IMAGE); + else if (draftAudio != null) setMedia(draftAudio, MediaType.AUDIO); + else if (draftVideo != null) setMedia(draftVideo, MediaType.VIDEO); if (draftText == null && draftImage == null && draftAudio == null && draftVideo == null) { initializeDraftFromDatabase(); @@ -753,11 +751,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity if (draft.getType().equals(Draft.TEXT)) { composeText.setText(draft.getValue()); } 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)) { - setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO, false); + setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO); } 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) { - attachmentManager.setMedia(masterSecret, uri, mediaType, getCurrentMediaConstraints(), isCapture); + private void setMedia(Uri uri, MediaType mediaType) { + attachmentManager.setMedia(masterSecret, uri, mediaType, getCurrentMediaConstraints()); } private void addAttachmentContactInfo(Uri contactUri) { @@ -1053,7 +1051,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity 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())); 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())); @@ -1263,7 +1261,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity { final Context context = getApplicationContext(); OutgoingMediaMessage outgoingMessage = new OutgoingMediaMessage(recipients, - attachmentManager.getSlideDeck(), + attachmentManager.buildSlideDeck(), getMessage(), System.currentTimeMillis(), distributionType); @@ -1336,7 +1334,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity @Override 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); } diff --git a/src/org/thoughtcrime/securesms/ShareActivity.java b/src/org/thoughtcrime/securesms/ShareActivity.java index bb69698f11..1f26924cb0 100644 --- a/src/org/thoughtcrime/securesms/ShareActivity.java +++ b/src/org/thoughtcrime/securesms/ShareActivity.java @@ -17,21 +17,29 @@ package org.thoughtcrime.securesms; +import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.NonNull; +import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; import android.webkit.MimeTypeMap; import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.providers.PersistentBlobProvider; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.util.DynamicLanguage; 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; @@ -43,9 +51,17 @@ import ws.com.google.android.mms.ContentType; public class ShareActivity extends PassphraseRequiredActionBarActivity implements ShareFragment.ConversationSelectedListener { + private static final String TAG = ShareActivity.class.getSimpleName(); + private final DynamicTheme dynamicTheme = new DynamicTheme (); private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); + private MasterSecret masterSecret; + private ViewGroup fragmentContainer; + private View progressWheel; + private Uri resolvedExtra; + private boolean isPassingAlongMedia; + @Override protected void onPreCreate() { dynamicTheme.onCreate(this); @@ -54,14 +70,21 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity @Override protected void onCreate(Bundle icicle, @NonNull MasterSecret masterSecret) { + this.masterSecret = masterSecret; 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); + initializeMedia(); } @Override protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - setIntent(intent); + super.onNewIntent(intent); + setIntent(intent); + initializeMedia(); } @Override @@ -75,7 +98,46 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity @Override public void 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() { + @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().getParcelableExtra(Intent.EXTRA_STREAM)); } @Override @@ -100,6 +162,7 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity private void handleNewConversation() { Intent intent = getBaseShareIntent(NewConversationActivity.class); + isPassingAlongMedia = true; startActivity(intent); } @@ -114,38 +177,24 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId); intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType); + isPassingAlongMedia = true; startActivity(intent); } - private Uri getStreamExtra() { - 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) { + private Intent getBaseShareIntent(final @NonNull Class target) { final Intent intent = new Intent(this, target); 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(); - if (ContentType.isImageType(type)) { - intent.putExtra(ConversationActivity.DRAFT_IMAGE_EXTRA, streamExtra); - } else if (ContentType.isAudioType(type)) { - intent.putExtra(ConversationActivity.DRAFT_AUDIO_EXTRA, streamExtra); - } else if (ContentType.isVideoType(type)) { - intent.putExtra(ConversationActivity.DRAFT_VIDEO_EXTRA, streamExtra); + if (resolvedExtra != null) { + if (ContentType.isImageType(type)) { + intent.putExtra(ConversationActivity.DRAFT_IMAGE_EXTRA, resolvedExtra); + } else if (ContentType.isAudioType(type)) { + intent.putExtra(ConversationActivity.DRAFT_AUDIO_EXTRA, resolvedExtra); + } else if (ContentType.isVideoType(type)) { + intent.putExtra(ConversationActivity.DRAFT_VIDEO_EXTRA, resolvedExtra); + } } intent.putExtra(ConversationActivity.DRAFT_TEXT_EXTRA, textExtra); diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java index b8b103d597..cdc8a2a7d2 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -38,9 +38,11 @@ import org.thoughtcrime.securesms.components.AudioView; import org.thoughtcrime.securesms.components.RemovableMediaView; import org.thoughtcrime.securesms.components.ThumbnailView; 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.util.MediaUtil; +import org.thoughtcrime.securesms.util.ViewUtil; +import org.whispersystems.libaxolotl.util.guava.Optional; import java.io.IOException; @@ -53,18 +55,17 @@ public class AttachmentManager { private final @NonNull RemovableMediaView removableMediaView; private final @NonNull ThumbnailView thumbnail; private final @NonNull AudioView audioView; - private final @NonNull SlideDeck slideDeck; private final @NonNull AttachmentListener attachmentListener; - private Uri captureUri; + private @NonNull Optional slide = Optional.absent(); + private @Nullable Uri captureUri; - public AttachmentManager(@NonNull Activity view, @NonNull AttachmentListener listener) { - this.attachmentView = view.findViewById(R.id.attachment_editor); - this.thumbnail = (ThumbnailView) view.findViewById(R.id.attachment_thumbnail); - this.audioView = (AudioView) view.findViewById(R.id.attachment_audio); - this.removableMediaView = (RemovableMediaView) view.findViewById(R.id.removable_media_view); - this.slideDeck = new SlideDeck(); - this.context = view; + public AttachmentManager(@NonNull Activity activity, @NonNull AttachmentListener listener) { + this.attachmentView = ViewUtil.findById(activity, R.id.attachment_editor); + this.thumbnail = ViewUtil.findById(activity, R.id.attachment_thumbnail); + this.audioView = ViewUtil.findById(activity, R.id.attachment_audio); + this.removableMediaView = ViewUtil.findById(activity, R.id.removable_media_view); + this.context = activity; this.attachmentListener = listener; removableMediaView.setRemoveClickListener(new RemoveButtonListener()); @@ -76,11 +77,13 @@ public class AttachmentManager { animation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) {} + @Override public void onAnimationRepeat(Animation animation) {} + @Override public void onAnimationEnd(Animation animation) { - slideDeck.clear(); + slide = Optional.absent(); thumbnail.clear(); attachmentView.setVisibility(View.GONE); attachmentListener.onAttachmentChanged(); @@ -92,26 +95,39 @@ public class AttachmentManager { } public void cleanup() { - if (captureUri != null) CaptureProvider.getInstance(context).delete(captureUri); + cleanup(captureUri); + cleanup(getSlideUri()); + captureUri = null; + slide = Optional.absent(); } - public void setMedia(@NonNull final MasterSecret masterSecret, - @NonNull final Uri uri, - @NonNull final MediaType mediaType, - @NonNull final MediaConstraints constraints, - final boolean isCapture) + 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, + @NonNull final Uri uri, + @NonNull final MediaType mediaType, + @NonNull final MediaConstraints constraints) { new AsyncTask() { @Override protected void onPreExecute() { - slideDeck.clear(); thumbnail.clear(); thumbnail.showProgressSpinner(); attachmentView.setVisibility(View.VISIBLE); - - if (isCapture) captureUri = uri; - if (!uri.equals(captureUri)) cleanup(); } @Override @@ -141,7 +157,7 @@ public class AttachmentManager { R.string.ConversationActivity_attachment_exceeds_size_limits, Toast.LENGTH_SHORT).show(); } else { - slideDeck.addSlide(slide); + setSlide(slide); attachmentView.setVisibility(View.VISIBLE); if (slide.hasAudio()) { @@ -162,9 +178,10 @@ public class AttachmentManager { return attachmentView.getVisibility() == View.VISIBLE; } - - public @NonNull SlideDeck getSlideDeck() { - return slideDeck; + public @NonNull SlideDeck buildSlideDeck() { + SlideDeck deck = new SlideDeck(); + if (slide.isPresent()) deck.addSlide(slide.get()); + return deck; } public static void selectVideo(Activity activity, int requestCode) { @@ -184,7 +201,11 @@ public class AttachmentManager { activity.startActivityForResult(intent, requestCode); } - public Uri getCaptureUri() { + private @Nullable Uri getSlideUri() { + return slide.isPresent() ? slide.get().getUri() : null; + } + + public @Nullable Uri getCaptureUri() { return captureUri; } @@ -192,7 +213,10 @@ public class AttachmentManager { try { Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 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); activity.startActivityForResult(captureIntent, requestCode); } @@ -237,8 +261,8 @@ public class AttachmentManager { private class RemoveButtonListener implements View.OnClickListener { @Override public void onClick(View v) { - clear(); cleanup(); + clear(); } } diff --git a/src/org/thoughtcrime/securesms/mms/PartAuthority.java b/src/org/thoughtcrime/securesms/mms/PartAuthority.java index 55b8b9888b..53224c2025 100644 --- a/src/org/thoughtcrime/securesms/mms/PartAuthority.java +++ b/src/org/thoughtcrime/securesms/mms/PartAuthority.java @@ -9,7 +9,7 @@ import android.support.annotation.NonNull; import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.crypto.MasterSecret; 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.SingleUseBlobProvider; @@ -25,7 +25,7 @@ public class PartAuthority { private static final int PART_ROW = 1; 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 UriMatcher uriMatcher; @@ -34,7 +34,7 @@ public class PartAuthority { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI("org.thoughtcrime.securesms", "part/*/#", PART_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); } @@ -50,8 +50,8 @@ public class PartAuthority { case THUMB_ROW: partUri = new PartUriParser(uri); return DatabaseFactory.getAttachmentDatabase(context).getThumbnailStream(masterSecret, partUri.getPartId()); - case CAPTURE_ROW: - return CaptureProvider.getInstance(context).getStream(masterSecret, ContentUris.parseId(uri)); + case PERSISTENT_ROW: + return PersistentBlobProvider.getInstance(context).getStream(masterSecret, ContentUris.parseId(uri)); case SINGLE_USE_ROW: return SingleUseBlobProvider.getInstance().getStream(ContentUris.parseId(uri)); default: diff --git a/src/org/thoughtcrime/securesms/providers/CaptureProvider.java b/src/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java similarity index 57% rename from src/org/thoughtcrime/securesms/providers/CaptureProvider.java rename to src/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java index 1211de6685..4f5d9c8bcf 100644 --- a/src/org/thoughtcrime/securesms/providers/CaptureProvider.java +++ b/src/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java @@ -6,7 +6,6 @@ import android.content.UriMatcher; import android.net.Uri; import android.os.AsyncTask; import android.support.annotation.NonNull; -import android.support.v4.util.SparseArrayCompat; import android.util.Log; import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream; @@ -21,25 +20,27 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; -public class CaptureProvider { - private static final String TAG = CaptureProvider.class.getSimpleName(); - private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture"; - public static final Uri CONTENT_URI = Uri.parse(URI_STRING); - public static final String AUTHORITY = "org.thoughtcrime.securesms"; - public static final String EXPECTED_PATH = "capture/*/#"; - private static final int MATCH = 1; - public static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH) {{ +public class PersistentBlobProvider { + private static final String TAG = PersistentBlobProvider.class.getSimpleName(); + private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture"; + public static final Uri CONTENT_URI = Uri.parse(URI_STRING); + public static final String AUTHORITY = "org.thoughtcrime.securesms"; + public static final String EXPECTED_PATH = "capture/*/#"; + private static final int MATCH = 1; + private static final UriMatcher MATCHER = new UriMatcher(UriMatcher.NO_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) { - synchronized (CaptureProvider.class) { + synchronized (PersistentBlobProvider.class) { if (instance == null) { - instance = new CaptureProvider(context); + instance = new PersistentBlobProvider(context); } } } @@ -47,9 +48,9 @@ public class CaptureProvider { } private final Context context; - private final SparseArrayCompat cache = new SparseArrayCompat<>(); + private final Map cache = new HashMap<>(); - private CaptureProvider(Context context) { + private PersistentBlobProvider(Context context) { this.context = context.getApplicationContext(); } @@ -57,19 +58,31 @@ public class CaptureProvider { @NonNull Recipients recipients, @NonNull byte[] imageBytes) { - final int id = generateId(recipients); + final long id = generateId(recipients); 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())); 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() { @Override protected Void doInBackground(Void... params) { try { final OutputStream output = new EncryptingPartOutputStream(getFile(id), masterSecret); - Util.copy(new ByteArrayInputStream(imageBytes), output); + Util.copy(input, output); } catch (IOException e) { Log.w(TAG, e); } @@ -83,23 +96,21 @@ public class CaptureProvider { } public Uri createForExternal(@NonNull Recipients recipients) throws IOException { - final File externalDir = context.getExternalFilesDir(null); - if (externalDir == null) throw new IOException("no external files directory"); - return Uri.fromFile(new File(externalDir, String.valueOf(generateId(recipients)) + ".jpg")) + return Uri.fromFile(new File(getExternalDir(context), String.valueOf(generateId(recipients)) + ".jpg")) .buildUpon() .appendQueryParameter("unique", String.valueOf(System.currentTimeMillis())) .build(); } public boolean delete(@NonNull Uri uri) { - switch (uriMatcher.match(uri)) { + switch (MATCHER.match(uri)) { case MATCH: return getFile(ContentUris.parseId(uri)).delete(); default: return new File(uri.getPath()).delete(); } } 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) : new DecryptingPartInputStream(getFile(id), masterSecret); } @@ -111,4 +122,18 @@ public class CaptureProvider { private File getFile(long id) { 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; + } + } } diff --git a/src/org/thoughtcrime/securesms/providers/SingleUseBlobProvider.java b/src/org/thoughtcrime/securesms/providers/SingleUseBlobProvider.java index 9f2368c272..08d1d054f5 100644 --- a/src/org/thoughtcrime/securesms/providers/SingleUseBlobProvider.java +++ b/src/org/thoughtcrime/securesms/providers/SingleUseBlobProvider.java @@ -27,7 +27,7 @@ import java.util.Map; 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 PATH = "memory/*/#";