Created new BlobProvider.

One unified place to create blobs for different lifespans.
This commit is contained in:
Greyson Parrelli 2019-02-25 17:47:30 -08:00
parent 22ed8caed3
commit a122bb4899
19 changed files with 526 additions and 274 deletions

View File

@ -49,6 +49,7 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.logging.PersistentLogger; import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger; import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener; import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.service.ExpiringMessageManager;
@ -119,6 +120,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
initializeWebRtc(); initializeWebRtc();
initializePendingMessages(); initializePendingMessages();
initializeUnidentifiedDeliveryAbilityRefresh(); initializeUnidentifiedDeliveryAbilityRefresh();
initializeBlobProvider();
NotificationChannels.create(this); NotificationChannels.create(this);
ProcessLifecycleOwner.get().getLifecycle().addObserver(this); ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
} }
@ -312,4 +314,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
jobManager.add(new RefreshUnidentifiedDeliveryAbilityJob(this)); jobManager.add(new RefreshUnidentifiedDeliveryAbilityJob(this));
} }
} }
private void initializeBlobProvider() {
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
BlobProvider.getInstance().onSessionStart(this);
});
}
} }

View File

@ -46,7 +46,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.PartAuthority; 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.recipients.Recipient;
import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
@ -132,7 +132,7 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
if (!isPassingAlongMedia && resolvedExtra != null) { if (!isPassingAlongMedia && resolvedExtra != null) {
PersistentBlobProvider.getInstance(this).delete(this, resolvedExtra); BlobProvider.getInstance().delete(this, resolvedExtra);
if (!isFinishing()) { if (!isFinishing()) {
finish(); finish();
@ -324,7 +324,11 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
if (cursor != null) cursor.close(); 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) { } catch (IOException ioe) {
Log.w(TAG, ioe); Log.w(TAG, ioe);
return null; return null;

View File

@ -9,7 +9,7 @@ import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import android.util.Pair; 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.MediaUtil;
import org.thoughtcrime.securesms.util.ThreadUtil; import org.thoughtcrime.securesms.util.ThreadUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
@ -27,14 +27,12 @@ public class AudioRecorder {
private static final ExecutorService executor = ThreadUtil.newDynamicSingleThreadedExecutor(); private static final ExecutorService executor = ThreadUtil.newDynamicSingleThreadedExecutor();
private final Context context; private final Context context;
private final PersistentBlobProvider blobProvider;
private AudioCodec audioCodec; private AudioCodec audioCodec;
private Uri captureUri; private Uri captureUri;
public AudioRecorder(@NonNull Context context) { public AudioRecorder(@NonNull Context context) {
this.context = context; this.context = context;
this.blobProvider = PersistentBlobProvider.getInstance(context.getApplicationContext());
} }
public void startRecording() { public void startRecording() {
@ -49,8 +47,10 @@ public class AudioRecorder {
ParcelFileDescriptor fds[] = ParcelFileDescriptor.createPipe(); ParcelFileDescriptor fds[] = ParcelFileDescriptor.createPipe();
captureUri = blobProvider.create(context, new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), captureUri = BlobProvider.getInstance()
MediaUtil.AUDIO_AAC, null, null); .forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
.withMimeType(MediaUtil.AUDIO_AAC)
.createForSingleSessionOnDisk(context);
audioCodec = new AudioCodec(); audioCodec = new AudioCodec();
audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1])); audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1]));

View File

@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideApp; 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.scribbles.ScribbleFragment;
import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener; import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
@ -85,7 +85,7 @@ public class CameraActivity extends PassphraseRequiredActionBarActivity implemen
} else { } else {
if (editorFragment != null && captureUri != null) { if (editorFragment != null && captureUri != null) {
Log.i(TAG, "Cleaning up unused capture: " + captureUri); Log.i(TAG, "Cleaning up unused capture: " + captureUri);
MemoryBlobProvider.getInstance().delete(captureUri); BlobProvider.getInstance().delete(this, captureUri);
captureUri = null; captureUri = null;
} }
super.onBackPressed(); super.onBackPressed();
@ -99,7 +99,7 @@ public class CameraActivity extends PassphraseRequiredActionBarActivity implemen
if (captureUri != null) { if (captureUri != null) {
Log.i(TAG, "Cleaning up capture in onDestroy: " + captureUri); 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) { public void onImageCaptured(@NonNull byte[] data) {
Log.i(TAG, "Fast image captured."); 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()); Log.i(TAG, "Fast image stored: " + captureUri.toString());
SettableFuture<Boolean> result = new SettableFuture<>(); SettableFuture<Boolean> result = new SettableFuture<>();

View File

@ -18,7 +18,7 @@ import org.thoughtcrime.securesms.contactshare.Contact.PostalAddress;
import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.PartAuthority; 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.recipients.Recipient;
import java.io.IOException; import java.io.IOException;
@ -153,8 +153,8 @@ public class ContactRepository {
Log.w(TAG, "Failed to parse the vcard.", e); Log.w(TAG, "Failed to parse the vcard.", e);
} }
if (PersistentBlobProvider.AUTHORITY.equals(uri.getAuthority())) { if (BlobProvider.AUTHORITY.equals(uri.getAuthority())) {
PersistentBlobProvider.getInstance(context).delete(context, uri); BlobProvider.getInstance().delete(context, uri);
} }
return contact; return contact;

View File

@ -177,8 +177,7 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.GroupShareProfileView; import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
import org.thoughtcrime.securesms.providers.MemoryBlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
@ -592,7 +591,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
Stream.of(slideDeck.getSlides()) Stream.of(slideDeck.getSlides())
.map(Slide::getUri) .map(Slide::getUri)
.withoutNulls() .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); bodyText = rawText.substring(0, maxPrimaryMessageSize);
byte[] textData = rawText.getBytes(); byte[] textData = rawText.getBytes();
Uri textUri = MemoryBlobProvider.getInstance().createUri(textData);
String timestamp = new SimpleDateFormat("yyyy-MM-dd-HHmmss", Locale.US).format(new Date()); String timestamp = new SimpleDateFormat("yyyy-MM-dd-HHmmss", Locale.US).format(new Date());
String filename = String.format("signal-%s.txt", timestamp); 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)); textSlide = Optional.of(new TextSlide(this, textUri, filename, textData.length));
} }
@ -2299,7 +2302,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
new AsyncTask<Void, Void, Void>() { new AsyncTask<Void, Void, Void>() {
@Override @Override
protected Void doInBackground(Void... params) { protected Void doInBackground(Void... params) {
PersistentBlobProvider.getInstance(ConversationActivity.this).delete(ConversationActivity.this, result.first); BlobProvider.getInstance().delete(ConversationActivity.this, result.first);
return null; return null;
} }
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
@ -2328,7 +2331,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
new AsyncTask<Void, Void, Void>() { new AsyncTask<Void, Void, Void>() {
@Override @Override
protected Void doInBackground(Void... params) { protected Void doInBackground(Void... params) {
PersistentBlobProvider.getInstance(ConversationActivity.this).delete(ConversationActivity.this, result.first); BlobProvider.getInstance().delete(ConversationActivity.this, result.first);
return null; return null;
} }
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);

View File

@ -20,12 +20,14 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R; 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.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import java.io.IOException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
public class GiphyActivity extends PassphraseRequiredActionBarActivity public class GiphyActivity extends PassphraseRequiredActionBarActivity
@ -115,8 +117,11 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity
try { try {
byte[] data = viewHolder.getData(forMms); byte[] data = viewHolder.getData(forMms);
return PersistentBlobProvider.getInstance(GiphyActivity.this).create(GiphyActivity.this, data, "image/gif", null); return BlobProvider.getInstance()
} catch (InterruptedException | ExecutionException e) { .forData(data)
.withMimeType(MediaUtil.IMAGE_GIF)
.createForSingleSessionOnDisk(GiphyActivity.this);
} catch (InterruptedException | ExecutionException | IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
return null; return null;
} }

View File

@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; 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.recipients.Recipient;
import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.BitmapUtil;
@ -111,7 +111,7 @@ public class GroupManager {
GroupContext groupContext = groupContextBuilder.build(); GroupContext groupContext = groupContextBuilder.build();
if (avatar != null) { 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); avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null);
} }

View File

@ -16,7 +16,6 @@ import com.google.android.mms.pdu_alt.RetrieveConf;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.UriAttachment; import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory; 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.MmsRadioException;
import org.thoughtcrime.securesms.mms.PartParser; import org.thoughtcrime.securesms.mms.PartParser;
import org.thoughtcrime.securesms.notifications.MessageNotifier; 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.service.KeyCachingService;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
@ -196,7 +195,6 @@ public class MmsDownloadJob extends ContextJob {
LegacyMessageException LegacyMessageException
{ {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context); MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
MemoryBlobProvider provider = MemoryBlobProvider.getInstance();
Optional<Address> group = Optional.absent(); Optional<Address> group = Optional.absent();
Set<Address> members = new HashSet<>(); Set<Address> members = new HashSet<>();
String body = null; String body = null;
@ -235,7 +233,7 @@ public class MmsDownloadJob extends ContextJob {
PduPart part = media.getPart(i); PduPart part = media.getPart(i);
if (part.getData() != null) { if (part.getData() != null) {
Uri uri = provider.createSingleUseUri(part.getData()); Uri uri = BlobProvider.getInstance().forData(part.getData()).createForSingleUseInMemory();
String name = null; String name = null;
if (part.getName() != null) name = Util.toIsoString(part.getName()); if (part.getName() != null) name = Util.toIsoString(part.getName());

View File

@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.net.CallRequestController;
import org.thoughtcrime.securesms.net.CompositeRequestController; import org.thoughtcrime.securesms.net.CompositeRequestController;
import org.thoughtcrime.securesms.net.ContentProxySelector; import org.thoughtcrime.securesms.net.ContentProxySelector;
import org.thoughtcrime.securesms.net.RequestController; 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.MediaUtil;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
@ -145,7 +145,7 @@ public class LinkPreviewRepository {
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos); bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos);
byte[] bytes = baos.toByteArray(); byte[] bytes = baos.toByteArray();
Uri uri = MemoryBlobProvider.getInstance().createUri(bytes); Uri uri = BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory();
Optional<Attachment> thumbnail = Optional.of(new UriAttachment(uri, Optional<Attachment> thumbnail = Optional.of(new UriAttachment(uri,
uri, uri,
MediaUtil.IMAGE_JPEG, MediaUtil.IMAGE_JPEG,

View File

@ -5,21 +5,11 @@ import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.ViewModel; import android.arch.lifecycle.ViewModel;
import android.arch.lifecycle.ViewModelProvider; import android.arch.lifecycle.ViewModelProvider;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils; 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.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.Debouncer;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;

View File

@ -41,7 +41,7 @@ import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter; import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
import org.thoughtcrime.securesms.mms.GlideApp; 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.scribbles.widget.ScribbleView;
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState; import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
import org.thoughtcrime.securesms.util.MediaUtil; 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 org.thoughtcrime.securesms.util.views.Stub;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -433,13 +434,17 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos); 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()); Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), baos.size(), media.getBucketId(), media.getCaption());
updatedMedia.add(updated); updatedMedia.add(updated);
renderTimer.split("item"); renderTimer.split("item");
} catch (InterruptedException | ExecutionException e) { } catch (InterruptedException | ExecutionException | IOException e) {
Log.w(TAG, "Failed to render image. Using base image."); Log.w(TAG, "Failed to render image. Using base image.");
updatedMedia.add(media); updatedMedia.add(media);
} }

View File

@ -57,12 +57,14 @@ import org.thoughtcrime.securesms.components.location.SignalMapView;
import org.thoughtcrime.securesms.components.location.SignalPlace; import org.thoughtcrime.securesms.components.location.SignalPlace;
import org.thoughtcrime.securesms.giph.ui.GiphyActivity; import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
import org.thoughtcrime.securesms.permissions.Permissions; 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.recipients.Recipient;
import org.thoughtcrime.securesms.scribbles.ScribbleActivity; import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener; import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
@ -165,14 +167,16 @@ public class AttachmentManager {
} }
private void cleanup(final @Nullable Uri uri) { 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); 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) { 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); Log.d(TAG, "Marking garbage that needs cleaning: " + uri);
garbage.add(uri); garbage.add(uri);
} }
@ -206,13 +210,17 @@ public class AttachmentManager {
@Override @Override
public void onSuccess(@NonNull Bitmap result) { public void onSuccess(@NonNull Bitmap result) {
byte[] blob = BitmapUtil.toByteArray(result); byte[] blob = BitmapUtil.toByteArray(result);
Uri uri = PersistentBlobProvider.getInstance(context) Uri uri = BlobProvider.getInstance()
.create(context, blob, MediaUtil.IMAGE_PNG, null); .forData(blob)
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleSessionInMemory();
LocationSlide locationSlide = new LocationSlide(context, uri, blob.length, place); LocationSlide locationSlide = new LocationSlide(context, uri, blob.length, place);
Util.runOnMain(() -> {
setSlide(locationSlide); setSlide(locationSlide);
attachmentListener.onAttachmentChanged(); attachmentListener.onAttachmentChanged();
returnResult.set(true); returnResult.set(true);
});
} }
}); });
@ -443,7 +451,7 @@ public class AttachmentManager {
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) {
if (captureUri == 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()); Log.d(TAG, "captureUri path is " + captureUri.getPath());
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, captureUri); captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, captureUri);

View File

@ -10,9 +10,9 @@ import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.database.DatabaseFactory; 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.PartProvider;
import org.thoughtcrime.securesms.providers.MemoryBlobProvider;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -27,7 +27,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 PERSISTENT_ROW = 3; 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; private static final UriMatcher uriMatcher;
@ -35,9 +35,9 @@ 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(PersistentBlobProvider.AUTHORITY, PersistentBlobProvider.EXPECTED_PATH_OLD, PERSISTENT_ROW); uriMatcher.addURI(DeprecatedPersistentBlobProvider.AUTHORITY, DeprecatedPersistentBlobProvider.EXPECTED_PATH_OLD, PERSISTENT_ROW);
uriMatcher.addURI(PersistentBlobProvider.AUTHORITY, PersistentBlobProvider.EXPECTED_PATH_NEW, PERSISTENT_ROW); uriMatcher.addURI(DeprecatedPersistentBlobProvider.AUTHORITY, DeprecatedPersistentBlobProvider.EXPECTED_PATH_NEW, PERSISTENT_ROW);
uriMatcher.addURI(MemoryBlobProvider.AUTHORITY, MemoryBlobProvider.PATH, SINGLE_USE_ROW); uriMatcher.addURI(BlobProvider.AUTHORITY, BlobProvider.PATH, BLOB_ROW);
} }
public static InputStream getAttachmentStream(@NonNull Context context, @NonNull Uri uri) public static InputStream getAttachmentStream(@NonNull Context context, @NonNull Uri uri)
@ -48,8 +48,8 @@ public class PartAuthority {
switch (match) { switch (match) {
case PART_ROW: return DatabaseFactory.getAttachmentDatabase(context).getAttachmentStream(new PartUriParser(uri).getPartId(), 0); 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 THUMB_ROW: return DatabaseFactory.getAttachmentDatabase(context).getThumbnailStream(new PartUriParser(uri).getPartId());
case PERSISTENT_ROW: return PersistentBlobProvider.getInstance(context).getStream(context, ContentUris.parseId(uri)); case PERSISTENT_ROW: return DeprecatedPersistentBlobProvider.getInstance(context).getStream(context, ContentUris.parseId(uri));
case SINGLE_USE_ROW: return MemoryBlobProvider.getInstance().getStream(ContentUris.parseId(uri)); case BLOB_ROW: return BlobProvider.getInstance().getStream(context, uri);
default: return context.getContentResolver().openInputStream(uri); default: return context.getContentResolver().openInputStream(uri);
} }
} catch (SecurityException se) { } catch (SecurityException se) {
@ -68,8 +68,9 @@ public class PartAuthority {
if (attachment != null) return attachment.getFileName(); if (attachment != null) return attachment.getFileName();
else return null; else return null;
case PERSISTENT_ROW: case PERSISTENT_ROW:
return PersistentBlobProvider.getFileName(context, uri); return DeprecatedPersistentBlobProvider.getFileName(context, uri);
case SINGLE_USE_ROW: case BLOB_ROW:
return BlobProvider.getFileName(uri);
default: default:
return null; return null;
} }
@ -86,8 +87,9 @@ public class PartAuthority {
if (attachment != null) return attachment.getSize(); if (attachment != null) return attachment.getSize();
else return null; else return null;
case PERSISTENT_ROW: case PERSISTENT_ROW:
return PersistentBlobProvider.getFileSize(context, uri); return DeprecatedPersistentBlobProvider.getFileSize(context, uri);
case SINGLE_USE_ROW: case BLOB_ROW:
return BlobProvider.getFileSize(uri);
default: default:
return null; return null;
} }
@ -104,8 +106,9 @@ public class PartAuthority {
if (attachment != null) return attachment.getContentType(); if (attachment != null) return attachment.getContentType();
else return null; else return null;
case PERSISTENT_ROW: case PERSISTENT_ROW:
return PersistentBlobProvider.getMimeType(context, uri); return DeprecatedPersistentBlobProvider.getMimeType(context, uri);
case SINGLE_USE_ROW: case BLOB_ROW:
return BlobProvider.getMimeType(uri);
default: default:
return null; return null;
} }
@ -132,7 +135,7 @@ public class PartAuthority {
case PART_ROW: case PART_ROW:
case THUMB_ROW: case THUMB_ROW:
case PERSISTENT_ROW: case PERSISTENT_ROW:
case SINGLE_USE_ROW: case BLOB_ROW:
return true; return true;
} }
return false; return false;

View File

@ -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<Uri, byte[]> 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.");
}
}
}

View File

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.providers; package org.thoughtcrime.securesms.providers;
import android.annotation.SuppressLint;
import android.content.ContentUris; import android.content.ContentUris;
import android.content.Context; import android.content.Context;
import android.content.UriMatcher; import android.content.UriMatcher;
@ -9,31 +8,28 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import android.util.Pair;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.thoughtcrime.securesms.util.FileProviderUtil; import org.thoughtcrime.securesms.util.FileProviderUtil;
import org.thoughtcrime.securesms.util.Util;
import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; 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"; private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture-new";
public static final Uri CONTENT_URI = Uri.parse(URI_STRING); public static final Uri CONTENT_URI = Uri.parse(URI_STRING);
@ -54,82 +50,29 @@ public class PersistentBlobProvider {
addURI(AUTHORITY, EXPECTED_PATH_NEW, MATCH_NEW); 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) { if (instance == null) {
synchronized (PersistentBlobProvider.class) { synchronized (DeprecatedPersistentBlobProvider.class) {
if (instance == null) { if (instance == null) {
instance = new PersistentBlobProvider(context); instance = new DeprecatedPersistentBlobProvider(context);
} }
} }
} }
return instance; return instance;
} }
@SuppressLint("UseSparseArrays")
private final Map<Long, byte[]> cache = Collections.synchronizedMap(new HashMap<Long, byte[]>());
private final ExecutorService executor = Executors.newCachedThreadPool();
private final AttachmentSecret attachmentSecret; private final AttachmentSecret attachmentSecret;
private PersistentBlobProvider(@NonNull Context context) { private DeprecatedPersistentBlobProvider(@NonNull Context context) {
this.attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); 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<byte[], OutputStream> 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 { public Uri createForExternal(@NonNull Context context, @NonNull String mimeType) throws IOException {
File target = new File(getExternalDir(context), String.valueOf(System.currentTimeMillis()) + "." + getExtensionFromMimeType(mimeType)); File target = new File(getExternalDir(context), String.valueOf(System.currentTimeMillis()) + "." + getExtensionFromMimeType(mimeType));
return FileProviderUtil.getUriFor(context, target); return FileProviderUtil.getUriFor(context, target);
@ -140,7 +83,6 @@ public class PersistentBlobProvider {
case MATCH_OLD: case MATCH_OLD:
case MATCH_NEW: case MATCH_NEW:
long id = ContentUris.parseId(uri); long id = ContentUris.parseId(uri);
cache.remove(id);
return getFile(context, ContentUris.parseId(uri)).file.delete(); 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 { 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); FileData fileData = getFile(context, id);
if (fileData.modern) return ModernDecryptingPartInputStream.createFor(attachmentSecret, fileData.file, 0); if (fileData.modern) return ModernDecryptingPartInputStream.createFor(attachmentSecret, fileData.file, 0);

View File

@ -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<Long, Entry> 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;
}
}
}

View File

@ -7,6 +7,7 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.PointF; import android.graphics.PointF;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; 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.mediasend.MediaSendPageFragment;
import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests; 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.Font;
import org.thoughtcrime.securesms.scribbles.viewmodel.Layer; import org.thoughtcrime.securesms.scribbles.viewmodel.Layer;
import org.thoughtcrime.securesms.scribbles.viewmodel.TextLayer; import org.thoughtcrime.securesms.scribbles.viewmodel.TextLayer;
@ -300,30 +302,40 @@ public class ScribbleFragment extends Fragment implements ScribbleHud.EventListe
} }
@Override @Override
public void onEditComplete(@NonNull Optional<String> message, Optional<TransportOption> transport) { public void onEditComplete(@NonNull Optional<String> message, @NonNull Optional<TransportOption> transport) {
ListenableFuture<Bitmap> future = scribbleView.getRenderedImage(glideRequests); ListenableFuture<Bitmap> future = scribbleView.getRenderedImage(glideRequests);
future.addListener(new ListenableFuture.Listener<Bitmap>() { future.addListener(new ListenableFuture.Listener<Bitmap>() {
@Override @Override
public void onSuccess(Bitmap result) { public void onSuccess(Bitmap result) {
PersistentBlobProvider provider = PersistentBlobProvider.getInstance(getContext()); AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
result.compress(Bitmap.CompressFormat.JPEG, 80, baos); 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), controller.onImageEditComplete(uri,
result.getWidth(), result.getWidth(),
result.getHeight(), result.getHeight(),
data.length, data.length,
message, message,
transport); transport);
} catch (IOException e) {
Log.w(TAG, "Failed to persist image.", e);
Util.runOnMain(() -> controller.onImageEditFailure());
}
});
} }
@Override @Override
public void onFailure(ExecutionException e) { public void onFailure(ExecutionException e) {
Log.w(TAG, e); Log.w(TAG, e);
controller.onImageEditFailure(); Util.runOnMain(() -> controller.onImageEditFailure());
} }
}); });
} }

View File

@ -28,7 +28,6 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.TextSlide; import org.thoughtcrime.securesms.mms.TextSlide;
import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;