Created new BlobProvider.
One unified place to create blobs for different lifespans.pull/9/head
parent
22ed8caed3
commit
a122bb4899
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue