Allow share intents for arbitrary file types

Fixes #6608
// FREEBIE
This commit is contained in:
Moxie Marlinspike 2017-05-10 15:21:52 -07:00
parent 1c8c6d5f85
commit e96bf2bdc7
8 changed files with 183 additions and 41 deletions

View File

@ -165,6 +165,9 @@
<data android:mimeType="image/*" /> <data android:mimeType="image/*" />
<data android:mimeType="text/plain" /> <data android:mimeType="text/plain" />
<data android:mimeType="video/*" /> <data android:mimeType="video/*" />
<data android:mimeType="application/*"/>
<data android:mimeType="text/*"/>
<data android:mimeType="*/*"/>
</intent-filter> </intent-filter>
<meta-data <meta-data

View File

@ -876,7 +876,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
final String draftText = getIntent().getStringExtra(TEXT_EXTRA); final String draftText = getIntent().getStringExtra(TEXT_EXTRA);
final Uri draftMedia = getIntent().getData(); final Uri draftMedia = getIntent().getData();
final MediaType draftMediaType = MediaType.from(getIntent().getType()); final MediaType draftMediaType = MediaType.from(getIntent().getType());
if (draftText != null) composeText.setText(draftText); if (draftText != null) composeText.setText(draftText);
if (draftMedia != null && draftMediaType != null) setMedia(draftMedia, draftMediaType); if (draftMedia != null && draftMediaType != null) setMedia(draftMedia, draftMediaType);
@ -1605,7 +1605,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override @Override
public void onImageCapture(@NonNull final byte[] imageBytes) { public void onImageCapture(@NonNull final byte[] imageBytes) {
setMedia(PersistentBlobProvider.getInstance(this) setMedia(PersistentBlobProvider.getInstance(this)
.create(masterSecret, imageBytes, MediaUtil.IMAGE_JPEG), .create(masterSecret, imageBytes, MediaUtil.IMAGE_JPEG, null),
MediaType.IMAGE); MediaType.IMAGE);
quickAttachmentDrawer.hide(false); quickAttachmentDrawer.hide(false);
} }

View File

@ -19,10 +19,12 @@ package org.thoughtcrime.securesms;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.Process; import android.os.Process;
import android.provider.OpenableColumns;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
@ -235,7 +237,24 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
return null; return null;
} }
return PersistentBlobProvider.getInstance(context).create(masterSecret, inputStream, mimeType); Cursor cursor = getContentResolver().query(uris[0], new String[] {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}, null, null, null);
String fileName = null;
Long fileSize = null;
try {
if (cursor != null && cursor.moveToFirst()) {
try {
fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
} catch (IllegalArgumentException e) {
Log.w(TAG, e);
}
}
} finally {
if (cursor != null) cursor.close();
}
return PersistentBlobProvider.getInstance(context).create(masterSecret, inputStream, mimeType, fileName, fileSize);
} catch (IOException ioe) { } catch (IOException ioe) {
Log.w(TAG, ioe); Log.w(TAG, ioe);
return null; return null;

View File

@ -56,7 +56,7 @@ public class AudioRecorder {
captureUri = blobProvider.create(masterSecret, captureUri = blobProvider.create(masterSecret,
new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), new ParcelFileDescriptor.AutoCloseInputStream(fds[0]),
MediaUtil.AUDIO_AAC); MediaUtil.AUDIO_AAC, null, null);
audioCodec = new AudioCodec(); audioCodec = new AudioCodec();
audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1])); audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1]));

View File

@ -190,7 +190,7 @@ public class AttachmentManager {
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 = PersistentBlobProvider.getInstance(context)
.create(masterSecret, blob, MediaUtil.IMAGE_PNG); .create(masterSecret, blob, MediaUtil.IMAGE_PNG, null);
LocationSlide locationSlide = new LocationSlide(context, uri, blob.length, place); LocationSlide locationSlide = new LocationSlide(context, uri, blob.length, place);
setSlide(locationSlide); setSlide(locationSlide);
@ -206,7 +206,7 @@ public class AttachmentManager {
{ {
inflateStub(); inflateStub();
new AsyncTask<Void, Void, Slide>() { new AsyncTask<Void, Void, Slide>() {
@Override @Override
protected void onPreExecute() { protected void onPreExecute() {
thumbnail.clear(); thumbnail.clear();
@ -285,11 +285,23 @@ public class AttachmentManager {
} }
private @NonNull Slide getManuallyCalculatedSlideInfo(Uri uri) throws IOException { private @NonNull Slide getManuallyCalculatedSlideInfo(Uri uri) throws IOException {
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
long mediaSize = MediaUtil.getMediaSize(context, masterSecret, uri); Long mediaSize = null;
String fileName = null;
String mimeType = null;
if (PartAuthority.isLocalUri(uri)) {
mediaSize = PartAuthority.getAttachmentSize(context, masterSecret, uri);
fileName = PartAuthority.getAttachmentFileName(context, masterSecret, uri);
mimeType = PartAuthority.getAttachmentContentType(context, masterSecret, uri);
}
if (mediaSize == null) {
mediaSize = MediaUtil.getMediaSize(context, masterSecret, uri);
}
Log.w(TAG, "local slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms"); Log.w(TAG, "local slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms");
return mediaType.createSlide(context, uri, null, null, mediaSize); return mediaType.createSlide(context, uri, fileName, mimeType, mediaSize);
} }
}.execute(); }.execute();
} }
@ -466,7 +478,8 @@ public class AttachmentManager {
if (MediaUtil.isImageType(mimeType)) return IMAGE; if (MediaUtil.isImageType(mimeType)) return IMAGE;
if (MediaUtil.isAudioType(mimeType)) return AUDIO; if (MediaUtil.isAudioType(mimeType)) return AUDIO;
if (MediaUtil.isVideoType(mimeType)) return VIDEO; if (MediaUtil.isVideoType(mimeType)) return VIDEO;
return null;
return DOCUMENT;
} }
} }

View File

@ -5,7 +5,9 @@ import android.content.Context;
import android.content.UriMatcher; import android.content.UriMatcher;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -34,7 +36,8 @@ 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, PERSISTENT_ROW); uriMatcher.addURI(PersistentBlobProvider.AUTHORITY, PersistentBlobProvider.EXPECTED_PATH_OLD, PERSISTENT_ROW);
uriMatcher.addURI(PersistentBlobProvider.AUTHORITY, PersistentBlobProvider.EXPECTED_PATH_NEW, PERSISTENT_ROW);
uriMatcher.addURI(SingleUseBlobProvider.AUTHORITY, SingleUseBlobProvider.PATH, SINGLE_USE_ROW); uriMatcher.addURI(SingleUseBlobProvider.AUTHORITY, SingleUseBlobProvider.PATH, SINGLE_USE_ROW);
} }
@ -44,24 +47,71 @@ public class PartAuthority {
int match = uriMatcher.match(uri); int match = uriMatcher.match(uri);
try { try {
switch (match) { switch (match) {
case PART_ROW: case PART_ROW: return DatabaseFactory.getAttachmentDatabase(context).getAttachmentStream(masterSecret, new PartUriParser(uri).getPartId());
PartUriParser partUri = new PartUriParser(uri); case THUMB_ROW: return DatabaseFactory.getAttachmentDatabase(context).getThumbnailStream(masterSecret, new PartUriParser(uri).getPartId());
return DatabaseFactory.getAttachmentDatabase(context).getAttachmentStream(masterSecret, partUri.getPartId()); case PERSISTENT_ROW: return PersistentBlobProvider.getInstance(context).getStream(masterSecret, ContentUris.parseId(uri));
case THUMB_ROW: case SINGLE_USE_ROW: return SingleUseBlobProvider.getInstance().getStream(ContentUris.parseId(uri));
partUri = new PartUriParser(uri); default: return context.getContentResolver().openInputStream(uri);
return DatabaseFactory.getAttachmentDatabase(context).getThumbnailStream(masterSecret, partUri.getPartId());
case PERSISTENT_ROW:
return PersistentBlobProvider.getInstance(context).getStream(masterSecret, ContentUris.parseId(uri));
case SINGLE_USE_ROW:
return SingleUseBlobProvider.getInstance().getStream(ContentUris.parseId(uri));
default:
return context.getContentResolver().openInputStream(uri);
} }
} catch (SecurityException se) { } catch (SecurityException se) {
throw new IOException(se); throw new IOException(se);
} }
} }
public static @Nullable String getAttachmentFileName(@NonNull Context context, @NonNull MasterSecret masterSecret, @NonNull Uri uri) {
int match = uriMatcher.match(uri);
switch (match) {
case THUMB_ROW:
case PART_ROW:
Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(masterSecret, new PartUriParser(uri).getPartId());
if (attachment != null) return attachment.getFileName();
else return null;
case PERSISTENT_ROW:
return PersistentBlobProvider.getFileName(context, masterSecret, uri);
case SINGLE_USE_ROW:
default:
return null;
}
}
public static @Nullable Long getAttachmentSize(@NonNull Context context, @NonNull MasterSecret masterSecret, @NonNull Uri uri) {
int match = uriMatcher.match(uri);
switch (match) {
case THUMB_ROW:
case PART_ROW:
Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(masterSecret, new PartUriParser(uri).getPartId());
if (attachment != null) return attachment.getSize();
else return null;
case PERSISTENT_ROW:
return PersistentBlobProvider.getFileSize(context, uri);
case SINGLE_USE_ROW:
default:
return null;
}
}
public static @Nullable String getAttachmentContentType(@NonNull Context context, @NonNull MasterSecret masterSecret, @NonNull Uri uri) {
int match = uriMatcher.match(uri);
switch (match) {
case THUMB_ROW:
case PART_ROW:
Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(masterSecret, new PartUriParser(uri).getPartId());
if (attachment != null) return attachment.getContentType();
else return null;
case PERSISTENT_ROW:
return PersistentBlobProvider.getMimeType(context, uri);
case SINGLE_USE_ROW:
default:
return null;
}
}
public static Uri getAttachmentPublicUri(Uri uri) { public static Uri getAttachmentPublicUri(Uri uri) {
PartUriParser partUri = new PartUriParser(uri); PartUriParser partUri = new PartUriParser(uri);
return PartProvider.getContentUri(partUri.getPartId()); return PartProvider.getContentUri(partUri.getPartId());

View File

@ -12,8 +12,10 @@ import android.webkit.MimeTypeMap;
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream; import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.InvalidMessageException;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
@ -30,15 +32,23 @@ public class PersistentBlobProvider {
private static final String TAG = PersistentBlobProvider.class.getSimpleName(); private static final String TAG = PersistentBlobProvider.class.getSimpleName();
private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture"; 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);
public static final String AUTHORITY = "org.thoughtcrime.securesms"; public static final String AUTHORITY = "org.thoughtcrime.securesms";
public static final String EXPECTED_PATH = "capture/*/*/#"; 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 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 String BLOB_EXTENSION = "blob";
private static final int MATCH = 1; private static final int MATCH_OLD = 1;
private static final int MATCH_NEW = 2;
private static final UriMatcher MATCHER = new UriMatcher(UriMatcher.NO_MATCH) {{ private static final UriMatcher MATCHER = new UriMatcher(UriMatcher.NO_MATCH) {{
addURI(AUTHORITY, EXPECTED_PATH, MATCH); addURI(AUTHORITY, EXPECTED_PATH_OLD, MATCH_OLD);
addURI(AUTHORITY, EXPECTED_PATH_NEW, MATCH_NEW);
}}; }};
private static volatile PersistentBlobProvider instance; private static volatile PersistentBlobProvider instance;
@ -63,26 +73,37 @@ public class PersistentBlobProvider {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
} }
public Uri create(@NonNull MasterSecret masterSecret, public Uri create(@NonNull MasterSecret masterSecret,
@NonNull byte[] blobBytes, @NonNull byte[] blobBytes,
@NonNull String mimeType) @NonNull String mimeType,
@Nullable String fileName)
{ {
final long id = System.currentTimeMillis(); final long id = System.currentTimeMillis();
cache.put(id, blobBytes); cache.put(id, blobBytes);
return create(masterSecret, new ByteArrayInputStream(blobBytes), id, mimeType); return create(masterSecret, new ByteArrayInputStream(blobBytes), id, mimeType, fileName, (long) blobBytes.length);
} }
public Uri create(@NonNull MasterSecret masterSecret, public Uri create(@NonNull MasterSecret masterSecret,
@NonNull InputStream input, @NonNull InputStream input,
@NonNull String mimeType) @NonNull String mimeType,
@Nullable String fileName,
@Nullable Long fileSize)
{ {
return create(masterSecret, input, System.currentTimeMillis(), mimeType); return create(masterSecret, input, System.currentTimeMillis(), mimeType, fileName, fileSize);
} }
private Uri create(MasterSecret masterSecret, InputStream input, long id, String mimeType) { private Uri create(@NonNull MasterSecret masterSecret,
@NonNull InputStream input,
long id,
@NonNull String mimeType,
@Nullable String fileName,
@Nullable Long fileSize)
{
persistToDisk(masterSecret, id, input); persistToDisk(masterSecret, id, input);
final Uri uniqueUri = CONTENT_URI.buildUpon() final Uri uniqueUri = CONTENT_URI.buildUpon()
.appendPath(mimeType) .appendPath(mimeType)
.appendPath(getEncryptedFileName(masterSecret, fileName))
.appendEncodedPath(String.valueOf(fileSize))
.appendEncodedPath(String.valueOf(System.currentTimeMillis())) .appendEncodedPath(String.valueOf(System.currentTimeMillis()))
.build(); .build();
return ContentUris.withAppendedId(uniqueUri, id); return ContentUris.withAppendedId(uniqueUri, id);
@ -113,13 +134,14 @@ public class PersistentBlobProvider {
public boolean delete(@NonNull Uri uri) { public boolean delete(@NonNull Uri uri) {
switch (MATCHER.match(uri)) { switch (MATCHER.match(uri)) {
case MATCH: case MATCH_OLD:
case MATCH_NEW:
long id = ContentUris.parseId(uri); long id = ContentUris.parseId(uri);
cache.remove(id); cache.remove(id);
return getFile(ContentUris.parseId(uri)).delete(); return getFile(ContentUris.parseId(uri)).delete();
default:
return new File(uri.getPath()).delete();
} }
return false;
} }
public @NonNull InputStream getStream(MasterSecret masterSecret, long id) throws IOException { public @NonNull InputStream getStream(MasterSecret masterSecret, long id) throws IOException {
@ -132,6 +154,11 @@ public class PersistentBlobProvider {
return new File(context.getDir("captures", Context.MODE_PRIVATE), id + "." + BLOB_EXTENSION); return new File(context.getDir("captures", Context.MODE_PRIVATE), id + "." + BLOB_EXTENSION);
} }
private @Nullable String getEncryptedFileName(@NonNull MasterSecret masterSecret, @Nullable String fileName) {
if (fileName == null) return null;
return new MasterCipher(masterSecret).encryptBody(fileName);
}
public static @Nullable String getMimeType(@NonNull Context context, @NonNull Uri persistentBlobUri) { public static @Nullable String getMimeType(@NonNull Context context, @NonNull Uri persistentBlobUri) {
if (!isAuthority(context, persistentBlobUri)) return null; if (!isAuthority(context, persistentBlobUri)) return null;
return isExternalBlobUri(context, persistentBlobUri) return isExternalBlobUri(context, persistentBlobUri)
@ -139,6 +166,35 @@ public class PersistentBlobProvider {
: persistentBlobUri.getPathSegments().get(MIMETYPE_PATH_SEGMENT); : persistentBlobUri.getPathSegments().get(MIMETYPE_PATH_SEGMENT);
} }
public static @Nullable String getFileName(@NonNull Context context, @NonNull MasterSecret masterSecret, @NonNull Uri persistentBlobUri) {
if (!isAuthority(context, persistentBlobUri)) return null;
if (isExternalBlobUri(context, persistentBlobUri)) return null;
if (MATCHER.match(persistentBlobUri) == MATCH_OLD) return null;
String fileName = persistentBlobUri.getPathSegments().get(FILENAME_PATH_SEGMENT);
try {
return new MasterCipher(masterSecret).decryptBody(fileName);
} catch (InvalidMessageException e) {
Log.w(TAG, "No valid filename for URI");
}
return null;
}
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) { private static @NonNull String getExtensionFromMimeType(String mimeType) {
final String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); final String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
return extension != null ? extension : BLOB_EXTENSION; return extension != null ? extension : BLOB_EXTENSION;
@ -157,7 +213,8 @@ public class PersistentBlobProvider {
} }
public static boolean isAuthority(@NonNull Context context, @NonNull Uri uri) { public static boolean isAuthority(@NonNull Context context, @NonNull Uri uri) {
return MATCHER.match(uri) == MATCH || isExternalBlobUri(context, 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) { private static boolean isExternalBlobUri(@NonNull Context context, @NonNull Uri uri) {

View File

@ -229,7 +229,7 @@ public class ScribbleActivity extends PassphraseRequiredActionBarActivity implem
baos = null; baos = null;
result = null; result = null;
Uri uri = provider.create(masterSecret, data, MediaUtil.IMAGE_JPEG); Uri uri = provider.create(masterSecret, data, MediaUtil.IMAGE_JPEG, null);
Intent intent = new Intent(); Intent intent = new Intent();
intent.setData(uri); intent.setData(uri);
setResult(RESULT_OK, intent); setResult(RESULT_OK, intent);