mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-08 00:02:23 +00:00
restructure and unite service android/java to libsignal
This commit is contained in:
@@ -0,0 +1,400 @@
|
||||
package org.thoughtcrime.securesms.providers;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.UriMatcher;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.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 org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
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://network.loki.provider.securesms/blob");
|
||||
public static final String AUTHORITY = "network.loki.provider.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, @Nullable ErrorListener errorListener) 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;
|
||||
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
try {
|
||||
Util.copy(blobSpec.getData(), outputStream);
|
||||
} catch (IOException e) {
|
||||
if (errorListener != null) {
|
||||
errorListener.onError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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, @Nullable ErrorListener errorListener) throws IOException {
|
||||
return writeBlobSpecToDisk(context, buildBlobSpec(StorageType.SINGLE_SESSION_DISK), errorListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, @Nullable ErrorListener errorListener) throws IOException {
|
||||
return writeBlobSpecToDisk(context, buildBlobSpec(StorageType.MULTI_SESSION_DISK), errorListener);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public interface ErrorListener {
|
||||
@WorkerThread
|
||||
void onError(IOException e);
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package org.thoughtcrime.securesms.providers;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.UriMatcher;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
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.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.FileProviderUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @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 = DeprecatedPersistentBlobProvider.class.getSimpleName();
|
||||
|
||||
private static final String URI_STRING = "content://network.loki.provider.securesms/capture-new";
|
||||
public static final Uri CONTENT_URI = Uri.parse(URI_STRING);
|
||||
public static final String AUTHORITY = "org.thoughtcrime.securesms";
|
||||
public static final String EXPECTED_PATH_OLD = "capture/*/*/#";
|
||||
public static final String EXPECTED_PATH_NEW = "capture-new/*/*/*/*/#";
|
||||
|
||||
private static final int MIMETYPE_PATH_SEGMENT = 1;
|
||||
private static final int FILENAME_PATH_SEGMENT = 2;
|
||||
private static final int FILESIZE_PATH_SEGMENT = 3;
|
||||
|
||||
private static final String BLOB_EXTENSION = "blob";
|
||||
private static final int MATCH_OLD = 1;
|
||||
private static final int MATCH_NEW = 2;
|
||||
|
||||
private static final UriMatcher MATCHER = new UriMatcher(UriMatcher.NO_MATCH) {{
|
||||
addURI(AUTHORITY, EXPECTED_PATH_OLD, MATCH_OLD);
|
||||
addURI(AUTHORITY, EXPECTED_PATH_NEW, MATCH_NEW);
|
||||
}};
|
||||
|
||||
private static volatile DeprecatedPersistentBlobProvider instance;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link BlobProvider} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public static DeprecatedPersistentBlobProvider getInstance(Context context) {
|
||||
if (instance == null) {
|
||||
synchronized (DeprecatedPersistentBlobProvider.class) {
|
||||
if (instance == null) {
|
||||
instance = new DeprecatedPersistentBlobProvider(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private final AttachmentSecret attachmentSecret;
|
||||
|
||||
private DeprecatedPersistentBlobProvider(@NonNull Context context) {
|
||||
this.attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public boolean delete(@NonNull Context context, @NonNull Uri uri) {
|
||||
switch (MATCHER.match(uri)) {
|
||||
case MATCH_OLD:
|
||||
case MATCH_NEW:
|
||||
long id = ContentUris.parseId(uri);
|
||||
return getFile(context, ContentUris.parseId(uri)).file.delete();
|
||||
}
|
||||
|
||||
//noinspection SimplifiableIfStatement
|
||||
if (isExternalBlobUri(context, uri)) {
|
||||
return FileProviderUtil.delete(context, uri);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public @NonNull InputStream getStream(@NonNull Context context, long id) throws IOException {
|
||||
FileData fileData = getFile(context, id);
|
||||
|
||||
if (fileData.modern) return ModernDecryptingPartInputStream.createFor(attachmentSecret, fileData.file, 0);
|
||||
else return ClassicDecryptingPartInputStream.createFor(attachmentSecret, fileData.file);
|
||||
}
|
||||
|
||||
private FileData getFile(@NonNull Context context, long id) {
|
||||
File legacy = getLegacyFile(context, id);
|
||||
File cache = getCacheFile(context, id);
|
||||
File modernCache = getModernCacheFile(context, id);
|
||||
|
||||
if (legacy.exists()) return new FileData(legacy, false);
|
||||
else if (cache.exists()) return new FileData(cache, false);
|
||||
else return new FileData(modernCache, true);
|
||||
}
|
||||
|
||||
private File getLegacyFile(@NonNull Context context, long id) {
|
||||
return new File(context.getDir("captures", Context.MODE_PRIVATE), id + "." + BLOB_EXTENSION);
|
||||
}
|
||||
|
||||
private File getCacheFile(@NonNull Context context, long id) {
|
||||
return new File(context.getCacheDir(), "capture-" + id + "." + BLOB_EXTENSION);
|
||||
}
|
||||
|
||||
private File getModernCacheFile(@NonNull Context context, long id) {
|
||||
return new File(context.getCacheDir(), "capture-m-" + id + "." + BLOB_EXTENSION);
|
||||
}
|
||||
|
||||
public static @Nullable String getMimeType(@NonNull Context context, @NonNull Uri persistentBlobUri) {
|
||||
if (!isAuthority(context, persistentBlobUri)) return null;
|
||||
return isExternalBlobUri(context, persistentBlobUri)
|
||||
? getMimeTypeFromExtension(persistentBlobUri)
|
||||
: persistentBlobUri.getPathSegments().get(MIMETYPE_PATH_SEGMENT);
|
||||
}
|
||||
|
||||
public static @Nullable String getFileName(@NonNull Context context, @NonNull Uri persistentBlobUri) {
|
||||
if (!isAuthority(context, persistentBlobUri)) return null;
|
||||
if (isExternalBlobUri(context, persistentBlobUri)) return null;
|
||||
if (MATCHER.match(persistentBlobUri) == MATCH_OLD) return null;
|
||||
|
||||
return persistentBlobUri.getPathSegments().get(FILENAME_PATH_SEGMENT);
|
||||
}
|
||||
|
||||
public static @Nullable Long getFileSize(@NonNull Context context, Uri persistentBlobUri) {
|
||||
if (!isAuthority(context, persistentBlobUri)) return null;
|
||||
if (isExternalBlobUri(context, persistentBlobUri)) return null;
|
||||
if (MATCHER.match(persistentBlobUri) == MATCH_OLD) return null;
|
||||
|
||||
try {
|
||||
return Long.valueOf(persistentBlobUri.getPathSegments().get(FILESIZE_PATH_SEGMENT));
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull String getExtensionFromMimeType(String mimeType) {
|
||||
final String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
|
||||
return extension != null ? extension : BLOB_EXTENSION;
|
||||
}
|
||||
|
||||
private static @NonNull String getMimeTypeFromExtension(@NonNull Uri uri) {
|
||||
final String mimeType = MimeTypeMap.getSingleton()
|
||||
.getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(uri.toString()));
|
||||
return mimeType != null ? mimeType : "application/octet-stream";
|
||||
}
|
||||
|
||||
private static @NonNull File getExternalDir(Context context) throws IOException {
|
||||
final File externalDir = context.getExternalCacheDir();
|
||||
if (externalDir == null) throw new IOException("no external files directory");
|
||||
return externalDir;
|
||||
}
|
||||
|
||||
public static boolean isAuthority(@NonNull Context context, @NonNull Uri uri) {
|
||||
int matchResult = MATCHER.match(uri);
|
||||
return matchResult == MATCH_NEW || matchResult == MATCH_OLD || isExternalBlobUri(context, uri);
|
||||
}
|
||||
|
||||
private static boolean isExternalBlobUri(@NonNull Context context, @NonNull Uri uri) {
|
||||
try {
|
||||
return uri.getPath().startsWith(getExternalDir(context).getAbsolutePath()) || FileProviderUtil.isAuthority(uri);
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Failed to determine if it's an external blob URI.", ioe);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static class FileData {
|
||||
private final File file;
|
||||
private final boolean modern;
|
||||
|
||||
private FileData(File file, boolean modern) {
|
||||
this.file = file;
|
||||
this.modern = modern;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.providers;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class MmsBodyProvider extends ContentProvider {
|
||||
private static final String TAG = MmsBodyProvider.class.getSimpleName();
|
||||
private static final String CONTENT_URI_STRING = "content://network.loki.provider.securesms.mms/mms";
|
||||
public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);
|
||||
private static final int SINGLE_ROW = 1;
|
||||
|
||||
private static final UriMatcher uriMatcher;
|
||||
|
||||
static {
|
||||
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
uriMatcher.addURI("network.loki.provider.securesms.mms", "mms/#", SINGLE_ROW);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private File getFile(Uri uri) {
|
||||
long id = Long.parseLong(uri.getPathSegments().get(1));
|
||||
return new File(getContext().getCacheDir(), id + ".mmsbody");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
|
||||
Log.i(TAG, "openFile(" + uri + ", " + mode + ")");
|
||||
|
||||
switch (uriMatcher.match(uri)) {
|
||||
case SINGLE_ROW:
|
||||
Log.i(TAG, "Fetching message body for a single row...");
|
||||
File tmpFile = getFile(uri);
|
||||
|
||||
final int fileMode;
|
||||
switch (mode) {
|
||||
case "w": fileMode = ParcelFileDescriptor.MODE_TRUNCATE |
|
||||
ParcelFileDescriptor.MODE_CREATE |
|
||||
ParcelFileDescriptor.MODE_WRITE_ONLY; break;
|
||||
case "r": fileMode = ParcelFileDescriptor.MODE_READ_ONLY; break;
|
||||
default: throw new IllegalArgumentException("requested file mode unsupported");
|
||||
}
|
||||
|
||||
Log.i(TAG, "returning file " + tmpFile.getAbsolutePath());
|
||||
return ParcelFileDescriptor.open(tmpFile, fileMode);
|
||||
}
|
||||
|
||||
throw new FileNotFoundException("Request for bad message.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, String arg1, String[] arg2) {
|
||||
switch (uriMatcher.match(uri)) {
|
||||
case SINGLE_ROW:
|
||||
return getFile(uri).delete() ? 1 : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(@NonNull Uri arg0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri arg0, ContentValues arg1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri arg0, String[] arg1, String arg2, String[] arg3, String arg4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
|
||||
return 0;
|
||||
}
|
||||
public static Pointer makeTemporaryPointer(Context context) {
|
||||
return new Pointer(context, ContentUris.withAppendedId(MmsBodyProvider.CONTENT_URI, System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
public static class Pointer {
|
||||
private final Context context;
|
||||
private final Uri uri;
|
||||
|
||||
public Pointer(Context context, Uri uri) {
|
||||
this.context = context;
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public OutputStream getOutputStream() throws FileNotFoundException {
|
||||
return context.getContentResolver().openOutputStream(uri, "w");
|
||||
}
|
||||
|
||||
public InputStream getInputStream() throws FileNotFoundException {
|
||||
return context.getContentResolver().openInputStream(uri);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
context.getContentResolver().delete(uri, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.providers;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.MemoryFile;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.OpenableColumns;
|
||||
import androidx.annotation.NonNull;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.mms.PartUriParser;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.MemoryFileUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class PartProvider extends ContentProvider {
|
||||
|
||||
private static final String TAG = PartProvider.class.getSimpleName();
|
||||
|
||||
private static final String CONTENT_URI_STRING = "content://network.loki.provider.securesms/part";
|
||||
private static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);
|
||||
private static final int SINGLE_ROW = 1;
|
||||
|
||||
private static final UriMatcher uriMatcher;
|
||||
|
||||
static {
|
||||
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
uriMatcher.addURI("network.loki.provider.securesms", "part/*/#", SINGLE_ROW);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
Log.i(TAG, "onCreate()");
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Uri getContentUri(AttachmentId attachmentId) {
|
||||
Uri uri = Uri.withAppendedPath(CONTENT_URI, String.valueOf(attachmentId.getUniqueId()));
|
||||
return ContentUris.withAppendedId(uri, attachmentId.getRowId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
|
||||
Log.i(TAG, "openFile() called!");
|
||||
|
||||
if (KeyCachingService.isLocked(getContext())) {
|
||||
Log.w(TAG, "masterSecret was null, abandoning.");
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (uriMatcher.match(uri)) {
|
||||
case SINGLE_ROW:
|
||||
Log.i(TAG, "Parting out a single row...");
|
||||
try {
|
||||
final PartUriParser partUri = new PartUriParser(uri);
|
||||
return getParcelStreamForAttachment(partUri.getPartId());
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
throw new FileNotFoundException("Error opening file");
|
||||
}
|
||||
}
|
||||
|
||||
throw new FileNotFoundException("Request for bad part.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri arg0, String arg1, String[] arg2) {
|
||||
Log.i(TAG, "delete() called");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
Log.i(TAG, "getType() called: " + uri);
|
||||
|
||||
switch (uriMatcher.match(uri)) {
|
||||
case SINGLE_ROW:
|
||||
PartUriParser partUriParser = new PartUriParser(uri);
|
||||
DatabaseAttachment attachment = DatabaseFactory.getAttachmentDatabase(getContext())
|
||||
.getAttachment(partUriParser.getPartId());
|
||||
|
||||
if (attachment != null) {
|
||||
return attachment.getContentType();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri arg0, ContentValues arg1) {
|
||||
Log.i(TAG, "insert() called");
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri url, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
Log.i(TAG, "query() called: " + url);
|
||||
|
||||
if (projection == null || projection.length <= 0) return null;
|
||||
|
||||
switch (uriMatcher.match(url)) {
|
||||
case SINGLE_ROW:
|
||||
PartUriParser partUri = new PartUriParser(url);
|
||||
DatabaseAttachment attachment = DatabaseFactory.getAttachmentDatabase(getContext()).getAttachment(partUri.getPartId());
|
||||
|
||||
if (attachment == null) return null;
|
||||
|
||||
MatrixCursor matrixCursor = new MatrixCursor(projection, 1);
|
||||
Object[] resultRow = new Object[projection.length];
|
||||
|
||||
for (int i=0;i<projection.length;i++) {
|
||||
if (OpenableColumns.DISPLAY_NAME.equals(projection[i])) {
|
||||
resultRow[i] = attachment.getFileName();
|
||||
}
|
||||
}
|
||||
|
||||
matrixCursor.addRow(resultRow);
|
||||
return matrixCursor;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
|
||||
Log.i(TAG, "update() called");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private ParcelFileDescriptor getParcelStreamForAttachment(AttachmentId attachmentId) throws IOException {
|
||||
long plaintextLength = Util.getStreamLength(DatabaseFactory.getAttachmentDatabase(getContext()).getAttachmentStream(attachmentId, 0));
|
||||
MemoryFile memoryFile = new MemoryFile(attachmentId.toString(), Util.toIntExact(plaintextLength));
|
||||
|
||||
InputStream in = DatabaseFactory.getAttachmentDatabase(getContext()).getAttachmentStream(attachmentId, 0);
|
||||
OutputStream out = memoryFile.getOutputStream();
|
||||
|
||||
Util.copy(in, out);
|
||||
Util.close(out);
|
||||
Util.close(in);
|
||||
|
||||
return MemoryFileUtil.getParcelFileDescriptor(memoryFile);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user