restructure and unite service android/java to libsignal

This commit is contained in:
Ryan ZHAO
2020-11-26 09:46:52 +11:00
parent 673d35625b
commit 7a66a47520
3790 changed files with 101955 additions and 74 deletions

View File

@@ -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.");
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}