From a122bb48996b75752ed8498eedfa2933a60bbdf7 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Mon, 25 Feb 2019 17:47:30 -0800 Subject: [PATCH] Created new BlobProvider. One unified place to create blobs for different lifespans. --- .../securesms/ApplicationContext.java | 8 + .../thoughtcrime/securesms/ShareActivity.java | 10 +- .../securesms/audio/AudioRecorder.java | 16 +- .../securesms/camera/CameraActivity.java | 8 +- .../contactshare/ContactRepository.java | 6 +- .../conversation/ConversationActivity.java | 15 +- .../securesms/giph/ui/GiphyActivity.java | 11 +- .../securesms/groups/GroupManager.java | 4 +- .../securesms/jobs/MmsDownloadJob.java | 6 +- .../linkpreview/LinkPreviewRepository.java | 4 +- .../linkpreview/LinkPreviewViewModel.java | 10 - .../mediasend/MediaSendFragment.java | 11 +- .../securesms/mms/AttachmentManager.java | 28 +- .../securesms/mms/PartAuthority.java | 33 +- .../securesms/providers/BlobProvider.java | 386 ++++++++++++++++++ ... => DeprecatedPersistentBlobProvider.java} | 100 +---- .../providers/MemoryBlobProvider.java | 105 ----- .../securesms/scribbles/ScribbleFragment.java | 38 +- .../securesms/util/MediaUtil.java | 1 - 19 files changed, 526 insertions(+), 274 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/providers/BlobProvider.java rename src/org/thoughtcrime/securesms/providers/{PersistentBlobProvider.java => DeprecatedPersistentBlobProvider.java} (67%) delete mode 100644 src/org/thoughtcrime/securesms/providers/MemoryBlobProvider.java diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index b98ffe087e..68f5e89b0e 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -49,6 +49,7 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.PersistentLogger; import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger; import org.thoughtcrime.securesms.notifications.NotificationChannels; +import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.service.DirectoryRefreshListener; import org.thoughtcrime.securesms.service.ExpiringMessageManager; @@ -119,6 +120,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc initializeWebRtc(); initializePendingMessages(); initializeUnidentifiedDeliveryAbilityRefresh(); + initializeBlobProvider(); NotificationChannels.create(this); ProcessLifecycleOwner.get().getLifecycle().addObserver(this); } @@ -312,4 +314,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc jobManager.add(new RefreshUnidentifiedDeliveryAbilityJob(this)); } } + + private void initializeBlobProvider() { + AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { + BlobProvider.getInstance().onSessionStart(this); + }); + } } diff --git a/src/org/thoughtcrime/securesms/ShareActivity.java b/src/org/thoughtcrime/securesms/ShareActivity.java index 872cc5e7c9..1035722b1a 100644 --- a/src/org/thoughtcrime/securesms/ShareActivity.java +++ b/src/org/thoughtcrime/securesms/ShareActivity.java @@ -46,7 +46,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.providers.PersistentBlobProvider; +import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; @@ -132,7 +132,7 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity public void onPause() { super.onPause(); if (!isPassingAlongMedia && resolvedExtra != null) { - PersistentBlobProvider.getInstance(this).delete(this, resolvedExtra); + BlobProvider.getInstance().delete(this, resolvedExtra); if (!isFinishing()) { finish(); @@ -324,7 +324,11 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity if (cursor != null) cursor.close(); } - return PersistentBlobProvider.getInstance(context).create(context, inputStream, mimeType, fileName, fileSize); + return BlobProvider.getInstance() + .forData(inputStream, fileSize == null ? 0 : fileSize) + .withMimeType(mimeType) + .withFileName(fileName) + .createForMultipleSessionsOnDisk(context); } catch (IOException ioe) { Log.w(TAG, ioe); return null; diff --git a/src/org/thoughtcrime/securesms/audio/AudioRecorder.java b/src/org/thoughtcrime/securesms/audio/AudioRecorder.java index 39cb5a3735..9a9568a31a 100644 --- a/src/org/thoughtcrime/securesms/audio/AudioRecorder.java +++ b/src/org/thoughtcrime/securesms/audio/AudioRecorder.java @@ -9,7 +9,7 @@ import android.support.annotation.NonNull; import org.thoughtcrime.securesms.logging.Log; import android.util.Pair; -import org.thoughtcrime.securesms.providers.PersistentBlobProvider; +import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.ThreadUtil; import org.thoughtcrime.securesms.util.Util; @@ -26,15 +26,13 @@ public class AudioRecorder { private static final ExecutorService executor = ThreadUtil.newDynamicSingleThreadedExecutor(); - private final Context context; - private final PersistentBlobProvider blobProvider; + private final Context context; private AudioCodec audioCodec; private Uri captureUri; public AudioRecorder(@NonNull Context context) { - this.context = context; - this.blobProvider = PersistentBlobProvider.getInstance(context.getApplicationContext()); + this.context = context; } public void startRecording() { @@ -49,9 +47,11 @@ public class AudioRecorder { ParcelFileDescriptor fds[] = ParcelFileDescriptor.createPipe(); - captureUri = blobProvider.create(context, new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), - MediaUtil.AUDIO_AAC, null, null); - audioCodec = new AudioCodec(); + captureUri = BlobProvider.getInstance() + .forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0) + .withMimeType(MediaUtil.AUDIO_AAC) + .createForSingleSessionOnDisk(context); + audioCodec = new AudioCodec(); audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1])); } catch (IOException e) { diff --git a/src/org/thoughtcrime/securesms/camera/CameraActivity.java b/src/org/thoughtcrime/securesms/camera/CameraActivity.java index 66316ed2f8..a002482618 100644 --- a/src/org/thoughtcrime/securesms/camera/CameraActivity.java +++ b/src/org/thoughtcrime/securesms/camera/CameraActivity.java @@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; import org.thoughtcrime.securesms.mms.GlideApp; -import org.thoughtcrime.securesms.providers.MemoryBlobProvider; +import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.scribbles.ScribbleFragment; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener; @@ -85,7 +85,7 @@ public class CameraActivity extends PassphraseRequiredActionBarActivity implemen } else { if (editorFragment != null && captureUri != null) { Log.i(TAG, "Cleaning up unused capture: " + captureUri); - MemoryBlobProvider.getInstance().delete(captureUri); + BlobProvider.getInstance().delete(this, captureUri); captureUri = null; } super.onBackPressed(); @@ -99,7 +99,7 @@ public class CameraActivity extends PassphraseRequiredActionBarActivity implemen if (captureUri != null) { Log.i(TAG, "Cleaning up capture in onDestroy: " + captureUri); - MemoryBlobProvider.getInstance().delete(captureUri); + BlobProvider.getInstance().delete(this, captureUri); } } @@ -114,7 +114,7 @@ public class CameraActivity extends PassphraseRequiredActionBarActivity implemen public void onImageCaptured(@NonNull byte[] data) { Log.i(TAG, "Fast image captured."); - captureUri = MemoryBlobProvider.getInstance().createUri(data); + captureUri = BlobProvider.getInstance().forData(data).createForSingleSessionInMemory(); Log.i(TAG, "Fast image stored: " + captureUri.toString()); SettableFuture result = new SettableFuture<>(); diff --git a/src/org/thoughtcrime/securesms/contactshare/ContactRepository.java b/src/org/thoughtcrime/securesms/contactshare/ContactRepository.java index cdef7db39b..01b688e004 100644 --- a/src/org/thoughtcrime/securesms/contactshare/ContactRepository.java +++ b/src/org/thoughtcrime/securesms/contactshare/ContactRepository.java @@ -18,7 +18,7 @@ import org.thoughtcrime.securesms.contactshare.Contact.PostalAddress; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.providers.PersistentBlobProvider; +import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.recipients.Recipient; import java.io.IOException; @@ -153,8 +153,8 @@ public class ContactRepository { Log.w(TAG, "Failed to parse the vcard.", e); } - if (PersistentBlobProvider.AUTHORITY.equals(uri.getAuthority())) { - PersistentBlobProvider.getInstance(context).delete(context, uri); + if (BlobProvider.AUTHORITY.equals(uri.getAuthority())) { + BlobProvider.getInstance().delete(context, uri); } return contact; diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index f5c8bdc439..43f7901bd2 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -177,8 +177,7 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.profiles.GroupShareProfileView; -import org.thoughtcrime.securesms.providers.MemoryBlobProvider; -import org.thoughtcrime.securesms.providers.PersistentBlobProvider; +import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; @@ -592,7 +591,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity Stream.of(slideDeck.getSlides()) .map(Slide::getUri) .withoutNulls() - .forEach(uri -> PersistentBlobProvider.getInstance(context).delete(context, uri)); + .forEach(uri -> BlobProvider.getInstance().delete(context, uri)); }); } }); @@ -1974,9 +1973,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity bodyText = rawText.substring(0, maxPrimaryMessageSize); byte[] textData = rawText.getBytes(); - Uri textUri = MemoryBlobProvider.getInstance().createUri(textData); String timestamp = new SimpleDateFormat("yyyy-MM-dd-HHmmss", Locale.US).format(new Date()); String filename = String.format("signal-%s.txt", timestamp); + Uri textUri = BlobProvider.getInstance() + .forData(textData) + .withMimeType(MediaUtil.LONG_TEXT) + .withFileName(filename) + .createForSingleSessionInMemory(); textSlide = Optional.of(new TextSlide(this, textUri, filename, textData.length)); } @@ -2299,7 +2302,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity new AsyncTask() { @Override protected Void doInBackground(Void... params) { - PersistentBlobProvider.getInstance(ConversationActivity.this).delete(ConversationActivity.this, result.first); + BlobProvider.getInstance().delete(ConversationActivity.this, result.first); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); @@ -2328,7 +2331,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity new AsyncTask() { @Override protected Void doInBackground(Void... params) { - PersistentBlobProvider.getInstance(ConversationActivity.this).delete(ConversationActivity.this, result.first); + BlobProvider.getInstance().delete(ConversationActivity.this, result.first); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); diff --git a/src/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java b/src/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java index c8936e575b..18a7a36319 100644 --- a/src/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java +++ b/src/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java @@ -20,12 +20,14 @@ import android.widget.Toast; import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.providers.PersistentBlobProvider; +import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.DynamicTheme; +import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.ViewUtil; +import java.io.IOException; import java.util.concurrent.ExecutionException; public class GiphyActivity extends PassphraseRequiredActionBarActivity @@ -115,8 +117,11 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity try { byte[] data = viewHolder.getData(forMms); - return PersistentBlobProvider.getInstance(GiphyActivity.this).create(GiphyActivity.this, data, "image/gif", null); - } catch (InterruptedException | ExecutionException e) { + return BlobProvider.getInstance() + .forData(data) + .withMimeType(MediaUtil.IMAGE_GIF) + .createForSingleSessionOnDisk(GiphyActivity.this); + } catch (InterruptedException | ExecutionException | IOException e) { Log.w(TAG, e); return null; } diff --git a/src/org/thoughtcrime/securesms/groups/GroupManager.java b/src/org/thoughtcrime/securesms/groups/GroupManager.java index efe8f96cb6..c342d5f9ff 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/src/org/thoughtcrime/securesms/groups/GroupManager.java @@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; -import org.thoughtcrime.securesms.providers.MemoryBlobProvider; +import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.util.BitmapUtil; @@ -111,7 +111,7 @@ public class GroupManager { GroupContext groupContext = groupContextBuilder.build(); if (avatar != null) { - Uri avatarUri = MemoryBlobProvider.getInstance().createSingleUseUri(avatar); + Uri avatarUri = BlobProvider.getInstance().forData(avatar).createForSingleUseInMemory(); avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null); } diff --git a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java index ccaae9ba4b..603f250261 100644 --- a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java @@ -16,7 +16,6 @@ import com.google.android.mms.pdu_alt.RetrieveConf; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.UriAttachment; -import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; @@ -30,7 +29,7 @@ import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsRadioException; import org.thoughtcrime.securesms.mms.PartParser; import org.thoughtcrime.securesms.notifications.MessageNotifier; -import org.thoughtcrime.securesms.providers.MemoryBlobProvider; +import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; @@ -196,7 +195,6 @@ public class MmsDownloadJob extends ContextJob { LegacyMessageException { MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - MemoryBlobProvider provider = MemoryBlobProvider.getInstance(); Optional
group = Optional.absent(); Set
members = new HashSet<>(); String body = null; @@ -235,7 +233,7 @@ public class MmsDownloadJob extends ContextJob { PduPart part = media.getPart(i); if (part.getData() != null) { - Uri uri = provider.createSingleUseUri(part.getData()); + Uri uri = BlobProvider.getInstance().forData(part.getData()).createForSingleUseInMemory(); String name = null; if (part.getName() != null) name = Util.toIsoString(part.getName()); diff --git a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java index 2e76b55bf7..57273bced7 100644 --- a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java +++ b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java @@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.net.CallRequestController; import org.thoughtcrime.securesms.net.CompositeRequestController; import org.thoughtcrime.securesms.net.ContentProxySelector; import org.thoughtcrime.securesms.net.RequestController; -import org.thoughtcrime.securesms.providers.MemoryBlobProvider; +import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.whispersystems.libsignal.util.guava.Optional; @@ -145,7 +145,7 @@ public class LinkPreviewRepository { bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos); byte[] bytes = baos.toByteArray(); - Uri uri = MemoryBlobProvider.getInstance().createUri(bytes); + Uri uri = BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory(); Optional thumbnail = Optional.of(new UriAttachment(uri, uri, MediaUtil.IMAGE_JPEG, diff --git a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java index fcdd08c5ba..a7231a44b1 100644 --- a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java +++ b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java @@ -5,21 +5,11 @@ import android.arch.lifecycle.MutableLiveData; import android.arch.lifecycle.ViewModel; import android.arch.lifecycle.ViewModelProvider; import android.content.Context; -import android.net.Uri; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.Spannable; -import android.text.SpannableString; import android.text.TextUtils; -import org.thoughtcrime.securesms.attachments.Attachment; -import org.thoughtcrime.securesms.attachments.UriAttachment; -import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.net.RequestController; -import org.thoughtcrime.securesms.providers.MemoryBlobProvider; -import org.thoughtcrime.securesms.providers.PersistentBlobProvider; import org.thoughtcrime.securesms.util.Debouncer; -import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java index eff3c50d71..c96ad9f069 100644 --- a/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java +++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java @@ -41,7 +41,7 @@ import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter; import org.thoughtcrime.securesms.mms.GlideApp; -import org.thoughtcrime.securesms.providers.PersistentBlobProvider; +import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.scribbles.widget.ScribbleView; import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState; import org.thoughtcrime.securesms.util.MediaUtil; @@ -53,6 +53,7 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.thoughtcrime.securesms.util.views.Stub; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -433,13 +434,17 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl ByteArrayOutputStream baos = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos); - Uri uri = PersistentBlobProvider.getInstance(context).create(context, baos.toByteArray(), MediaUtil.IMAGE_JPEG, null); + Uri uri = BlobProvider.getInstance() + .forData(baos.toByteArray()) + .withMimeType(MediaUtil.IMAGE_JPEG) + .createForSingleSessionOnDisk(context); + Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), baos.size(), media.getBucketId(), media.getCaption()); updatedMedia.add(updated); renderTimer.split("item"); - } catch (InterruptedException | ExecutionException e) { + } catch (InterruptedException | ExecutionException | IOException e) { Log.w(TAG, "Failed to render image. Using base image."); updatedMedia.add(media); } diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java index 6855152025..b1308a8974 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -57,12 +57,14 @@ import org.thoughtcrime.securesms.components.location.SignalMapView; import org.thoughtcrime.securesms.components.location.SignalPlace; import org.thoughtcrime.securesms.giph.ui.GiphyActivity; import org.thoughtcrime.securesms.permissions.Permissions; -import org.thoughtcrime.securesms.providers.PersistentBlobProvider; +import org.thoughtcrime.securesms.providers.BlobProvider; +import org.thoughtcrime.securesms.providers.DeprecatedPersistentBlobProvider; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.scribbles.ScribbleActivity; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.ThemeUtil; +import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; @@ -165,14 +167,16 @@ public class AttachmentManager { } private void cleanup(final @Nullable Uri uri) { - if (uri != null && PersistentBlobProvider.isAuthority(context, uri)) { + if (uri != null && DeprecatedPersistentBlobProvider.isAuthority(context, uri)) { Log.d(TAG, "cleaning up " + uri); - PersistentBlobProvider.getInstance(context).delete(context, uri); + DeprecatedPersistentBlobProvider.getInstance(context).delete(context, uri); + } else if (uri != null && BlobProvider.isAuthority(uri)) { + BlobProvider.getInstance().delete(context, uri); } } private void markGarbage(@Nullable Uri uri) { - if (uri != null && PersistentBlobProvider.isAuthority(context, uri)) { + if (uri != null && (DeprecatedPersistentBlobProvider.isAuthority(context, uri) || BlobProvider.isAuthority(uri))) { Log.d(TAG, "Marking garbage that needs cleaning: " + uri); garbage.add(uri); } @@ -206,13 +210,17 @@ public class AttachmentManager { @Override public void onSuccess(@NonNull Bitmap result) { byte[] blob = BitmapUtil.toByteArray(result); - Uri uri = PersistentBlobProvider.getInstance(context) - .create(context, blob, MediaUtil.IMAGE_PNG, null); + Uri uri = BlobProvider.getInstance() + .forData(blob) + .withMimeType(MediaUtil.IMAGE_JPEG) + .createForSingleSessionInMemory(); LocationSlide locationSlide = new LocationSlide(context, uri, blob.length, place); - setSlide(locationSlide); - attachmentListener.onAttachmentChanged(); - returnResult.set(true); + Util.runOnMain(() -> { + setSlide(locationSlide); + attachmentListener.onAttachmentChanged(); + returnResult.set(true); + }); } }); @@ -443,7 +451,7 @@ public class AttachmentManager { Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (captureIntent.resolveActivity(activity.getPackageManager()) != null) { if (captureUri == null) { - captureUri = PersistentBlobProvider.getInstance(context).createForExternal(context, MediaUtil.IMAGE_JPEG); + captureUri = DeprecatedPersistentBlobProvider.getInstance(context).createForExternal(context, MediaUtil.IMAGE_JPEG); } Log.d(TAG, "captureUri path is " + captureUri.getPath()); captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, captureUri); diff --git a/src/org/thoughtcrime/securesms/mms/PartAuthority.java b/src/org/thoughtcrime/securesms/mms/PartAuthority.java index 78fd2610b1..df852a8f1e 100644 --- a/src/org/thoughtcrime/securesms/mms/PartAuthority.java +++ b/src/org/thoughtcrime/securesms/mms/PartAuthority.java @@ -10,9 +10,9 @@ import android.support.annotation.Nullable; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.providers.PersistentBlobProvider; +import org.thoughtcrime.securesms.providers.BlobProvider; +import org.thoughtcrime.securesms.providers.DeprecatedPersistentBlobProvider; import org.thoughtcrime.securesms.providers.PartProvider; -import org.thoughtcrime.securesms.providers.MemoryBlobProvider; import java.io.IOException; import java.io.InputStream; @@ -27,7 +27,7 @@ public class PartAuthority { private static final int PART_ROW = 1; private static final int THUMB_ROW = 2; private static final int PERSISTENT_ROW = 3; - private static final int SINGLE_USE_ROW = 4; + private static final int BLOB_ROW = 4; private static final UriMatcher uriMatcher; @@ -35,9 +35,9 @@ 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(PersistentBlobProvider.AUTHORITY, PersistentBlobProvider.EXPECTED_PATH_OLD, PERSISTENT_ROW); - uriMatcher.addURI(PersistentBlobProvider.AUTHORITY, PersistentBlobProvider.EXPECTED_PATH_NEW, PERSISTENT_ROW); - uriMatcher.addURI(MemoryBlobProvider.AUTHORITY, MemoryBlobProvider.PATH, SINGLE_USE_ROW); + uriMatcher.addURI(DeprecatedPersistentBlobProvider.AUTHORITY, DeprecatedPersistentBlobProvider.EXPECTED_PATH_OLD, PERSISTENT_ROW); + uriMatcher.addURI(DeprecatedPersistentBlobProvider.AUTHORITY, DeprecatedPersistentBlobProvider.EXPECTED_PATH_NEW, PERSISTENT_ROW); + uriMatcher.addURI(BlobProvider.AUTHORITY, BlobProvider.PATH, BLOB_ROW); } public static InputStream getAttachmentStream(@NonNull Context context, @NonNull Uri uri) @@ -48,8 +48,8 @@ public class PartAuthority { switch (match) { case PART_ROW: return DatabaseFactory.getAttachmentDatabase(context).getAttachmentStream(new PartUriParser(uri).getPartId(), 0); case THUMB_ROW: return DatabaseFactory.getAttachmentDatabase(context).getThumbnailStream(new PartUriParser(uri).getPartId()); - case PERSISTENT_ROW: return PersistentBlobProvider.getInstance(context).getStream(context, ContentUris.parseId(uri)); - case SINGLE_USE_ROW: return MemoryBlobProvider.getInstance().getStream(ContentUris.parseId(uri)); + case PERSISTENT_ROW: return DeprecatedPersistentBlobProvider.getInstance(context).getStream(context, ContentUris.parseId(uri)); + case BLOB_ROW: return BlobProvider.getInstance().getStream(context, uri); default: return context.getContentResolver().openInputStream(uri); } } catch (SecurityException se) { @@ -68,8 +68,9 @@ public class PartAuthority { if (attachment != null) return attachment.getFileName(); else return null; case PERSISTENT_ROW: - return PersistentBlobProvider.getFileName(context, uri); - case SINGLE_USE_ROW: + return DeprecatedPersistentBlobProvider.getFileName(context, uri); + case BLOB_ROW: + return BlobProvider.getFileName(uri); default: return null; } @@ -86,8 +87,9 @@ public class PartAuthority { if (attachment != null) return attachment.getSize(); else return null; case PERSISTENT_ROW: - return PersistentBlobProvider.getFileSize(context, uri); - case SINGLE_USE_ROW: + return DeprecatedPersistentBlobProvider.getFileSize(context, uri); + case BLOB_ROW: + return BlobProvider.getFileSize(uri); default: return null; } @@ -104,8 +106,9 @@ public class PartAuthority { if (attachment != null) return attachment.getContentType(); else return null; case PERSISTENT_ROW: - return PersistentBlobProvider.getMimeType(context, uri); - case SINGLE_USE_ROW: + return DeprecatedPersistentBlobProvider.getMimeType(context, uri); + case BLOB_ROW: + return BlobProvider.getMimeType(uri); default: return null; } @@ -132,7 +135,7 @@ public class PartAuthority { case PART_ROW: case THUMB_ROW: case PERSISTENT_ROW: - case SINGLE_USE_ROW: + case BLOB_ROW: return true; } return false; diff --git a/src/org/thoughtcrime/securesms/providers/BlobProvider.java b/src/org/thoughtcrime/securesms/providers/BlobProvider.java new file mode 100644 index 0000000000..34f2c065dd --- /dev/null +++ b/src/org/thoughtcrime/securesms/providers/BlobProvider.java @@ -0,0 +1,386 @@ +package org.thoughtcrime.securesms.providers; + +import android.app.Application; +import android.content.Context; +import android.content.UriMatcher; +import android.net.Uri; +import android.support.annotation.IntRange; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; + +import org.thoughtcrime.securesms.crypto.AttachmentSecret; +import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; +import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; +import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.util.Util; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Allows for the creation and retrieval of blobs. + */ +public class BlobProvider { + + private static final String TAG = BlobProvider.class.getSimpleName(); + + private static final String MULTI_SESSION_DIRECTORY = "multi_session_blobs"; + private static final String SINGLE_SESSION_DIRECTORY = "single_session_blobs"; + + public static final Uri CONTENT_URI = Uri.parse("content://org.thoughtcrime.securesms/blob"); + public static final String AUTHORITY = "org.thoughtcrime.securesms"; + public static final String PATH = "blob/*/*/*/*/*"; + + private static final int STORAGE_TYPE_PATH_SEGMENT = 1; + private static final int MIMETYPE_PATH_SEGMENT = 2; + private static final int FILENAME_PATH_SEGMENT = 3; + private static final int FILESIZE_PATH_SEGMENT = 4; + private static final int ID_PATH_SEGMENT = 5; + + private static final int MATCH = 1; + private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH) {{ + addURI(AUTHORITY, PATH, MATCH); + }}; + + private static final BlobProvider INSTANCE = new BlobProvider(); + + private final Map memoryBlobs = new HashMap<>(); + + + public static BlobProvider getInstance() { + return INSTANCE; + } + + /** + * Begin building a blob for the provided data. Allows for the creation of in-memory blobs. + */ + public MemoryBlobBuilder forData(@NonNull byte[] data) { + return new MemoryBlobBuilder(data); + } + + /** + * Begin building a blob for the provided input stream. + */ + public BlobBuilder forData(@NonNull InputStream data, long fileSize) { + return new BlobBuilder(data, fileSize); + } + + /** + * Retrieve a stream for the content with the specified URI. + * @throws IOException If the stream fails to open or the spec of the URI doesn't match. + */ + public synchronized @NonNull InputStream getStream(@NonNull Context context, @NonNull Uri uri) throws IOException { + if (isAuthority(uri)) { + StorageType storageType = StorageType.decode(uri.getPathSegments().get(STORAGE_TYPE_PATH_SEGMENT)); + + if (storageType.isMemory()) { + byte[] data = memoryBlobs.get(uri); + + if (data != null) { + if (storageType == StorageType.SINGLE_USE_MEMORY) { + memoryBlobs.remove(uri); + } + return new ByteArrayInputStream(data); + } else { + throw new IOException("Failed to find in-memory blob for: " + uri); + } + } else { + String id = uri.getPathSegments().get(ID_PATH_SEGMENT); + String directory = getDirectory(storageType); + File file = new File(getOrCreateCacheDirectory(context, directory), buildFileName(id)); + + return ModernDecryptingPartInputStream.createFor(AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(), file, 0); + } + } else { + throw new IOException("Provided URI does not match this spec. Uri: " + uri); + } + } + + /** + * Delete the content with the specified URI. + */ + public synchronized void delete(@NonNull Context context, @NonNull Uri uri) { + if (!isAuthority(uri)) { + Log.d(TAG, "Can't delete. Not the authority for uri: " + uri); + return; + } + + try { + StorageType storageType = StorageType.decode(uri.getPathSegments().get(STORAGE_TYPE_PATH_SEGMENT)); + + if (storageType.isMemory()) { + memoryBlobs.remove(uri); + } else { + String id = uri.getPathSegments().get(ID_PATH_SEGMENT); + String directory = getDirectory(storageType); + File file = new File(getOrCreateCacheDirectory(context, directory), buildFileName(id)); + + if (!file.delete()) { + throw new IOException("File wasn't deleted."); + } + } + } catch (IOException e) { + Log.w(TAG, "Failed to delete uri: " + uri, e); + } + } + + /** + * Indicates a new app session has started, allowing old single-session blobs to be deleted. + */ + public synchronized void onSessionStart(@NonNull Context context) { + File directory = getOrCreateCacheDirectory(context, SINGLE_SESSION_DIRECTORY); + for (File file : directory.listFiles()) { + file.delete(); + } + } + + public static @Nullable String getMimeType(@NonNull Uri uri) { + if (isAuthority(uri)) { + return uri.getPathSegments().get(MIMETYPE_PATH_SEGMENT); + } + return null; + } + + public static @Nullable String getFileName(@NonNull Uri uri) { + if (isAuthority(uri)) { + return uri.getPathSegments().get(FILENAME_PATH_SEGMENT); + } + return null; + } + + public static @Nullable Long getFileSize(@NonNull Uri uri) { + if (isAuthority(uri)) { + try { + return Long.parseLong(uri.getPathSegments().get(FILESIZE_PATH_SEGMENT)); + } catch (NumberFormatException e) { + return null; + } + } + return null; + } + + public static boolean isAuthority(@NonNull Uri uri) { + return URI_MATCHER.match(uri) == MATCH; + } + + @WorkerThread + private synchronized @NonNull Uri writeBlobSpecToDisk(@NonNull Context context, @NonNull BlobSpec blobSpec) throws IOException { + AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); + String directory = getDirectory(blobSpec.getStorageType()); + File outputFile = new File(getOrCreateCacheDirectory(context, directory), buildFileName(blobSpec.id)); + OutputStream outputStream = ModernEncryptingPartOutputStream.createFor(attachmentSecret, outputFile, true).second; + + Util.copy(blobSpec.getData(), outputStream); + + return buildUri(blobSpec); + } + + private synchronized @NonNull Uri writeBlobSpecToMemory(@NonNull BlobSpec blobSpec, @NonNull byte[] data) { + Uri uri = buildUri(blobSpec); + memoryBlobs.put(uri, data); + return uri; + } + + private static @NonNull String buildFileName(@NonNull String id) { + return id + ".blob"; + } + + private static @NonNull String getDirectory(@NonNull StorageType storageType) { + return storageType == StorageType.MULTI_SESSION_DISK ? MULTI_SESSION_DIRECTORY : SINGLE_SESSION_DIRECTORY; + } + + private static @NonNull Uri buildUri(@NonNull BlobSpec blobSpec) { + return CONTENT_URI.buildUpon() + .appendPath(blobSpec.getStorageType().encode()) + .appendPath(blobSpec.getMimeType()) + .appendPath(blobSpec.getFileName()) + .appendEncodedPath(String.valueOf(blobSpec.getFileSize())) + .appendPath(blobSpec.getId()) + .build(); + } + + private static File getOrCreateCacheDirectory(@NonNull Context context, @NonNull String directory) { + File file = new File(context.getCacheDir(), directory); + if (!file.exists()) { + file.mkdir(); + } + + return file; + } + + public class BlobBuilder { + + private InputStream data; + private String id; + private String mimeType; + private String fileName; + private long fileSize; + + private BlobBuilder(@NonNull InputStream data, long fileSize) { + this.id = UUID.randomUUID().toString(); + this.data = data; + this.fileSize = fileSize; + } + + public BlobBuilder withMimeType(@NonNull String mimeType) { + this.mimeType = mimeType; + return this; + } + + public BlobBuilder withFileName(@NonNull String fileName) { + this.fileName = fileName; + return this; + } + + protected BlobSpec buildBlobSpec(@NonNull StorageType storageType) { + return new BlobSpec(data, id, storageType, mimeType, fileName, fileSize); + } + + /** + * Create a blob that will exist for a single app session. An app session is defined as the + * period from one {@link Application#onCreate()} to the next. + */ + @WorkerThread + public Uri createForSingleSessionOnDisk(@NonNull Context context) throws IOException { + return writeBlobSpecToDisk(context, new BlobSpec(data, id, StorageType.SINGLE_SESSION_DISK, mimeType, fileName, fileSize)); + } + + /** + * Create a blob that will exist for multiple app sessions. It is the caller's responsibility to + * eventually call {@link BlobProvider#delete(Context, Uri)} when the blob is no longer in use. + */ + @WorkerThread + public Uri createForMultipleSessionsOnDisk(@NonNull Context context) throws IOException { + return writeBlobSpecToDisk(context, buildBlobSpec(StorageType.MULTI_SESSION_DISK)); + } + } + + public class MemoryBlobBuilder extends BlobBuilder { + + private byte[] data; + + private MemoryBlobBuilder(@NonNull byte[] data) { + super(new ByteArrayInputStream(data), data.length); + this.data = data; + } + + @Override + public MemoryBlobBuilder withMimeType(@NonNull String mimeType) { + super.withMimeType(mimeType); + return this; + } + + @Override + public MemoryBlobBuilder withFileName(@NonNull String fileName) { + super.withFileName(fileName); + return this; + } + + /** + * Create a blob that is stored in memory and can only be read a single time. After a single + * read, it will be removed from storage. Useful for when a Uri is needed to read transient data. + */ + public Uri createForSingleUseInMemory() { + return writeBlobSpecToMemory(buildBlobSpec(StorageType.SINGLE_USE_MEMORY), data); + } + + /** + * Create a blob that is stored in memory. Will persist for a single app session. You should + * always try to call {@link BlobProvider#delete(Context, Uri)} after you're done with the blob + * to free up memory. + */ + public Uri createForSingleSessionInMemory() { + return writeBlobSpecToMemory(buildBlobSpec(StorageType.SINGLE_SESSION_MEMORY), data); + } + } + + private static class BlobSpec { + + private final InputStream data; + private final String id; + private final StorageType storageType; + private final String mimeType; + private final String fileName; + private final long fileSize; + + private BlobSpec(@NonNull InputStream data, + @NonNull String id, + @NonNull StorageType storageType, + @NonNull String mimeType, + @Nullable String fileName, + @IntRange(from = 0) long fileSize) + { + this.data = data; + this.id = id; + this.storageType = storageType; + this.mimeType = mimeType; + this.fileName = fileName; + this.fileSize = fileSize; + } + + private @NonNull InputStream getData() { + return data; + } + + private @NonNull String getId() { + return id; + } + + private @NonNull StorageType getStorageType() { + return storageType; + } + + private @NonNull String getMimeType() { + return mimeType; + } + + private @Nullable String getFileName() { + return fileName; + } + + private long getFileSize() { + return fileSize; + } + } + + private enum StorageType { + + SINGLE_USE_MEMORY("single-use-memory", true), + SINGLE_SESSION_MEMORY("single-session-memory", true), + SINGLE_SESSION_DISK("single-session-disk", false), + MULTI_SESSION_DISK("multi-session-disk", false); + + private final String encoded; + private final boolean inMemory; + + StorageType(String encoded, boolean inMemory) { + this.encoded = encoded; + this.inMemory = inMemory; + } + + private String encode() { + return encoded; + } + + private boolean isMemory() { + return inMemory; + } + + private static StorageType decode(@NonNull String encoded) throws IOException { + for (StorageType storageType : StorageType.values()) { + if (storageType.encoded.equals(encoded)) { + return storageType; + } + } + throw new IOException("Failed to decode lifespan."); + } + } +} diff --git a/src/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java b/src/org/thoughtcrime/securesms/providers/DeprecatedPersistentBlobProvider.java similarity index 67% rename from src/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java rename to src/org/thoughtcrime/securesms/providers/DeprecatedPersistentBlobProvider.java index 8da3b63f24..2fb4cf2f0c 100644 --- a/src/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java +++ b/src/org/thoughtcrime/securesms/providers/DeprecatedPersistentBlobProvider.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.providers; -import android.annotation.SuppressLint; import android.content.ContentUris; import android.content.Context; import android.content.UriMatcher; @@ -9,31 +8,28 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import org.thoughtcrime.securesms.logging.Log; -import android.util.Pair; import android.webkit.MimeTypeMap; import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; -import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream; import org.thoughtcrime.securesms.util.FileProviderUtil; -import org.thoughtcrime.securesms.util.Util; -import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -public class PersistentBlobProvider { +/** + * @deprecated Use {@link BlobProvider} instead. Keeping in read-only mode due to the number of + * legacy URIs it handles. Given that this was largely used for drafts, and that files were stored + * in the cache directory, it's possible that we could remove this class after a reasonable amount + * of time has passed. + */ +@Deprecated +public class DeprecatedPersistentBlobProvider { - private static final String TAG = PersistentBlobProvider.class.getSimpleName(); + private static final String TAG = DeprecatedPersistentBlobProvider.class.getSimpleName(); private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture-new"; public static final Uri CONTENT_URI = Uri.parse(URI_STRING); @@ -54,82 +50,29 @@ public class PersistentBlobProvider { addURI(AUTHORITY, EXPECTED_PATH_NEW, MATCH_NEW); }}; - private static volatile PersistentBlobProvider instance; + private static volatile DeprecatedPersistentBlobProvider instance; - public static PersistentBlobProvider getInstance(Context context) { + /** + * @deprecated Use {@link BlobProvider} instead. + */ + @Deprecated + public static DeprecatedPersistentBlobProvider getInstance(Context context) { if (instance == null) { - synchronized (PersistentBlobProvider.class) { + synchronized (DeprecatedPersistentBlobProvider.class) { if (instance == null) { - instance = new PersistentBlobProvider(context); + instance = new DeprecatedPersistentBlobProvider(context); } } } return instance; } - @SuppressLint("UseSparseArrays") - private final Map cache = Collections.synchronizedMap(new HashMap()); - private final ExecutorService executor = Executors.newCachedThreadPool(); - private final AttachmentSecret attachmentSecret; - private PersistentBlobProvider(@NonNull Context context) { + private DeprecatedPersistentBlobProvider(@NonNull Context context) { this.attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); } - public Uri create(@NonNull Context context, - @NonNull byte[] blobBytes, - @NonNull String mimeType, - @Nullable String fileName) - { - final long id = System.currentTimeMillis(); - cache.put(id, blobBytes); - return create(context, attachmentSecret, new ByteArrayInputStream(blobBytes), id, mimeType, fileName, (long) blobBytes.length); - } - - public Uri create(@NonNull Context context, - @NonNull InputStream input, - @NonNull String mimeType, - @Nullable String fileName, - @Nullable Long fileSize) - { - return create(context, attachmentSecret, input, System.currentTimeMillis(), mimeType, fileName, fileSize); - } - - private Uri create(@NonNull Context context, - @NonNull AttachmentSecret attachmentSecret, - @NonNull InputStream input, - long id, - @NonNull String mimeType, - @Nullable String fileName, - @Nullable Long fileSize) - { - persistToDisk(context, attachmentSecret, id, input); - final Uri uniqueUri = CONTENT_URI.buildUpon() - .appendPath(mimeType) - .appendPath(fileName) - .appendEncodedPath(String.valueOf(fileSize)) - .appendEncodedPath(String.valueOf(System.currentTimeMillis())) - .build(); - return ContentUris.withAppendedId(uniqueUri, id); - } - - private void persistToDisk(@NonNull Context context, - @NonNull AttachmentSecret attachmentSecret, - final long id, final InputStream input) - { - executor.submit(() -> { - try { - Pair output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, getFile(context, id).file, true); - Util.copy(input, output.second); - } catch (IOException e) { - Log.w(TAG, e); - } - - cache.remove(id); - }); - } - public Uri createForExternal(@NonNull Context context, @NonNull String mimeType) throws IOException { File target = new File(getExternalDir(context), String.valueOf(System.currentTimeMillis()) + "." + getExtensionFromMimeType(mimeType)); return FileProviderUtil.getUriFor(context, target); @@ -140,7 +83,6 @@ public class PersistentBlobProvider { case MATCH_OLD: case MATCH_NEW: long id = ContentUris.parseId(uri); - cache.remove(id); return getFile(context, ContentUris.parseId(uri)).file.delete(); } @@ -153,12 +95,6 @@ public class PersistentBlobProvider { } public @NonNull InputStream getStream(@NonNull Context context, long id) throws IOException { - final byte[] cached = cache.get(id); - - if (cached != null) { - return new ByteArrayInputStream(cached); - } - FileData fileData = getFile(context, id); if (fileData.modern) return ModernDecryptingPartInputStream.createFor(attachmentSecret, fileData.file, 0); diff --git a/src/org/thoughtcrime/securesms/providers/MemoryBlobProvider.java b/src/org/thoughtcrime/securesms/providers/MemoryBlobProvider.java deleted file mode 100644 index c7a97c89cf..0000000000 --- a/src/org/thoughtcrime/securesms/providers/MemoryBlobProvider.java +++ /dev/null @@ -1,105 +0,0 @@ -package org.thoughtcrime.securesms.providers; - -import android.content.ContentUris; -import android.net.Uri; -import android.support.annotation.NonNull; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.HashMap; -import java.util.Map; - -public class MemoryBlobProvider { - - @SuppressWarnings("unused") - private static final String TAG = MemoryBlobProvider.class.getSimpleName(); - - public static final String AUTHORITY = "org.thoughtcrime.securesms"; - public static final String PATH = "memory/*/#"; - private static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/memory"); - - private final Map cache = new HashMap<>(); - - private static final MemoryBlobProvider instance = new MemoryBlobProvider(); - - public static MemoryBlobProvider getInstance() { - return instance; - } - - private MemoryBlobProvider() {} - - public synchronized Uri createSingleUseUri(@NonNull byte[] blob) { - return createUriInternal(blob, true); - } - - public synchronized Uri createUri(@NonNull byte[] blob) { - return createUriInternal(blob, false); - } - - public synchronized void delete(@NonNull Uri uri) { - cache.remove(ContentUris.parseId(uri)); - } - - public synchronized @NonNull byte[] getBlob(@NonNull Uri uri) { - long id = ContentUris.parseId(uri); - Entry entry = cache.get(ContentUris.parseId(uri)); - - if (entry == null) { - throw new IllegalArgumentException("ID not found: " + id); - } - - if (entry.isSingleUse()) { - cache.remove(id); - } - - return entry.getBlob(); - } - - public synchronized @NonNull InputStream getStream(long id) throws IOException { - Entry entry = cache.get(id); - - if (entry == null) { - throw new IOException("ID not found: " + id); - } - - if (entry.isSingleUse()) { - cache.remove(id); - } - - return new ByteArrayInputStream(entry.getBlob()); - } - - private Uri createUriInternal(@NonNull byte[] blob, boolean singleUse) { - try { - long id = Math.abs(SecureRandom.getInstance("SHA1PRNG").nextLong()); - cache.put(id, new Entry(blob, singleUse)); - - Uri uniqueUri = Uri.withAppendedPath(CONTENT_URI, String.valueOf(System.currentTimeMillis())); - return ContentUris.withAppendedId(uniqueUri, id); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } - } - - private static class Entry { - - private final byte[] blob; - private final boolean singleUse; - - private Entry(@NonNull byte[] blob, boolean singleUse) { - this.blob = blob; - this.singleUse = singleUse; - } - - public byte[] getBlob() { - return blob; - } - - public boolean isSingleUse() { - return singleUse; - } - } -} diff --git a/src/org/thoughtcrime/securesms/scribbles/ScribbleFragment.java b/src/org/thoughtcrime/securesms/scribbles/ScribbleFragment.java index 56e16d6fb8..9c8dd215df 100644 --- a/src/org/thoughtcrime/securesms/scribbles/ScribbleFragment.java +++ b/src/org/thoughtcrime/securesms/scribbles/ScribbleFragment.java @@ -7,6 +7,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.PointF; import android.net.Uri; +import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; @@ -23,7 +24,8 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mediasend.MediaSendPageFragment; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequests; -import org.thoughtcrime.securesms.providers.PersistentBlobProvider; +import org.thoughtcrime.securesms.providers.BlobProvider; +import org.thoughtcrime.securesms.providers.DeprecatedPersistentBlobProvider; import org.thoughtcrime.securesms.scribbles.viewmodel.Font; import org.thoughtcrime.securesms.scribbles.viewmodel.Layer; import org.thoughtcrime.securesms.scribbles.viewmodel.TextLayer; @@ -300,30 +302,40 @@ public class ScribbleFragment extends Fragment implements ScribbleHud.EventListe } @Override - public void onEditComplete(@NonNull Optional message, Optional transport) { + public void onEditComplete(@NonNull Optional message, @NonNull Optional transport) { ListenableFuture future = scribbleView.getRenderedImage(glideRequests); future.addListener(new ListenableFuture.Listener() { @Override public void onSuccess(Bitmap result) { - PersistentBlobProvider provider = PersistentBlobProvider.getInstance(getContext()); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - result.compress(Bitmap.CompressFormat.JPEG, 80, baos); + AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + result.compress(Bitmap.CompressFormat.JPEG, 80, baos); - byte[] data = baos.toByteArray(); + byte[] data = baos.toByteArray(); + Uri uri = BlobProvider.getInstance() + .forData(data) + .withMimeType(MediaUtil.IMAGE_JPEG) + .createForSingleSessionOnDisk(requireContext()); - controller.onImageEditComplete(provider.create(getContext(), data, MediaUtil.IMAGE_JPEG, null), - result.getWidth(), - result.getHeight(), - data.length, - message, - transport); + controller.onImageEditComplete(uri, + result.getWidth(), + result.getHeight(), + data.length, + message, + transport); + } catch (IOException e) { + Log.w(TAG, "Failed to persist image.", e); + Util.runOnMain(() -> controller.onImageEditFailure()); + } + }); } @Override public void onFailure(ExecutionException e) { Log.w(TAG, e); - controller.onImageEditFailure(); + Util.runOnMain(() -> controller.onImageEditFailure()); } }); } diff --git a/src/org/thoughtcrime/securesms/util/MediaUtil.java b/src/org/thoughtcrime/securesms/util/MediaUtil.java index b0afcce392..afddb45e16 100644 --- a/src/org/thoughtcrime/securesms/util/MediaUtil.java +++ b/src/org/thoughtcrime/securesms/util/MediaUtil.java @@ -28,7 +28,6 @@ import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.TextSlide; import org.thoughtcrime.securesms.mms.VideoSlide; -import org.thoughtcrime.securesms.providers.PersistentBlobProvider; import java.io.FileNotFoundException; import java.io.IOException;