mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-15 16:40:53 +00:00
Share media from within Media Preview and share QR code image.
This commit is contained in:
parent
5e536c3fa5
commit
2f69a9c38e
@ -673,6 +673,11 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:authorities="${applicationId}.part" />
|
android:authorities="${applicationId}.part" />
|
||||||
|
|
||||||
|
<provider android:name=".providers.BlobContentProvider"
|
||||||
|
android:authorities="${applicationId}.blob"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true" />
|
||||||
|
|
||||||
<provider android:name=".providers.MmsBodyProvider"
|
<provider android:name=".providers.MmsBodyProvider"
|
||||||
android:grantUriPermissions="true"
|
android:grantUriPermissions="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
@ -18,12 +18,14 @@ package org.thoughtcrime.securesms;
|
|||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.ContentObserver;
|
import android.database.ContentObserver;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
@ -37,6 +39,7 @@ import android.widget.Toast;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.core.app.ShareCompat;
|
||||||
import androidx.core.util.Pair;
|
import androidx.core.util.Pair;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
@ -61,6 +64,7 @@ import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment;
|
|||||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
|
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
|
||||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
@ -378,6 +382,27 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void share() {
|
||||||
|
MediaItem mediaItem = getCurrentMediaItem();
|
||||||
|
|
||||||
|
if (mediaItem != null) {
|
||||||
|
Uri publicUri = PartAuthority.getAttachmentPublicUri(mediaItem.uri);
|
||||||
|
String mimeType = Intent.normalizeMimeType(mediaItem.type);
|
||||||
|
Intent shareIntent = ShareCompat.IntentBuilder.from(this)
|
||||||
|
.setStream(publicUri)
|
||||||
|
.setType(mimeType)
|
||||||
|
.createChooserIntent()
|
||||||
|
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivity(shareIntent);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
Log.w(TAG, "No activity existed to share the media.", e);
|
||||||
|
Toast.makeText(this, R.string.MediaPreviewActivity_cant_find_an_app_able_to_share_this_media, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("CodeBlock2Expr")
|
@SuppressWarnings("CodeBlock2Expr")
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
private void saveToDisk() {
|
private void saveToDisk() {
|
||||||
@ -455,6 +480,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
menu.findItem(R.id.delete).setVisible(false);
|
menu.findItem(R.id.delete).setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restricted to API26 because of MemoryFileUtil not supporting lower API levels well
|
||||||
|
menu.findItem(R.id.media_preview__share).setVisible(Build.VERSION.SDK_INT >= 26);
|
||||||
|
|
||||||
if (cameFromAllMedia) {
|
if (cameFromAllMedia) {
|
||||||
menu.findItem(R.id.media_preview__overview).setVisible(false);
|
menu.findItem(R.id.media_preview__overview).setVisible(false);
|
||||||
}
|
}
|
||||||
@ -464,16 +492,17 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||||
super.onOptionsItemSelected(item);
|
super.onOptionsItemSelected(item);
|
||||||
|
|
||||||
switch (item.getItemId()) {
|
int itemId = item.getItemId();
|
||||||
case R.id.media_preview__overview: showOverview(); return true;
|
|
||||||
case R.id.media_preview__forward: forward(); return true;
|
if (itemId == R.id.media_preview__overview) { showOverview(); return true; }
|
||||||
case R.id.save: saveToDisk(); return true;
|
if (itemId == R.id.media_preview__forward) { forward(); return true; }
|
||||||
case R.id.delete: deleteMedia(); return true;
|
if (itemId == R.id.media_preview__share) { share(); return true; }
|
||||||
case android.R.id.home: finish(); return true;
|
if (itemId == R.id.save) { saveToDisk(); return true; }
|
||||||
}
|
if (itemId == R.id.delete) { deleteMedia(); return true; }
|
||||||
|
if (itemId == android.R.id.home) { finish(); return true; }
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
package org.thoughtcrime.securesms.providers;
|
||||||
|
|
||||||
|
import android.content.ContentProvider;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.ProviderInfo;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.MatrixCursor;
|
||||||
|
import android.provider.OpenableColumns;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
abstract class BaseContentProvider extends ContentProvider {
|
||||||
|
|
||||||
|
private static final String[] COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanity checks the security like FileProvider does.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
|
||||||
|
super.attachInfo(context, info);
|
||||||
|
|
||||||
|
if (info.exported) {
|
||||||
|
throw new SecurityException("Provider must not be exported");
|
||||||
|
}
|
||||||
|
if (!info.grantUriPermissions) {
|
||||||
|
throw new SecurityException("Provider must grant uri permissions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Cursor createCursor(@Nullable String[] projection, @NonNull String fileName, long fileSize) {
|
||||||
|
if (projection == null || projection.length == 0) {
|
||||||
|
projection = COLUMNS;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<String> cols = new ArrayList<>(projection.length);
|
||||||
|
ArrayList<Object> values = new ArrayList<>(projection.length);
|
||||||
|
|
||||||
|
for (String col : projection) {
|
||||||
|
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
|
||||||
|
cols.add(OpenableColumns.DISPLAY_NAME);
|
||||||
|
values.add(fileName);
|
||||||
|
} else if (OpenableColumns.SIZE.equals(col)) {
|
||||||
|
cols.add(OpenableColumns.SIZE);
|
||||||
|
values.add(fileSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MatrixCursor cursor = new MatrixCursor(cols.toArray(new String[0]), 1);
|
||||||
|
|
||||||
|
cursor.addRow(values.toArray(new Object[0]));
|
||||||
|
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static String createFileNameForMimeType(String mimeType) {
|
||||||
|
return mimeType.replace('/', '.');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
package org.thoughtcrime.securesms.providers;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.MemoryFile;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
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 final class BlobContentProvider extends BaseContentProvider {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(BlobContentProvider.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreate() {
|
||||||
|
Log.i(TAG, "onCreate()");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
|
||||||
|
Log.i(TAG, "openFile() called: " + uri);
|
||||||
|
|
||||||
|
try {
|
||||||
|
try (InputStream stream = BlobProvider.getInstance().getStream(ApplicationDependencies.getApplication(), uri)) {
|
||||||
|
Long fileSize = BlobProvider.getFileSize(uri);
|
||||||
|
if (fileSize == null) {
|
||||||
|
Log.w(TAG, "No file size available");
|
||||||
|
throw new FileNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return getParcelStreamForStream(stream, Util.toIntExact(fileSize));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FileNotFoundException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NonNull ParcelFileDescriptor getParcelStreamForStream(@NonNull InputStream in, int fileSize) throws IOException {
|
||||||
|
MemoryFile memoryFile = new MemoryFile(null, fileSize);
|
||||||
|
|
||||||
|
try (OutputStream out = memoryFile.getOutputStream()) {
|
||||||
|
Util.copy(in, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
return MemoryFileUtil.getParcelFileDescriptor(memoryFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
|
||||||
|
Log.i(TAG, "query() called: " + uri);
|
||||||
|
|
||||||
|
if (projection == null || projection.length <= 0) return null;
|
||||||
|
|
||||||
|
String mimeType = BlobProvider.getMimeType(uri);
|
||||||
|
String fileName = BlobProvider.getFileName(uri);
|
||||||
|
Long fileSize = BlobProvider.getFileSize(uri);
|
||||||
|
|
||||||
|
if (fileSize == null) {
|
||||||
|
Log.w(TAG, "No file size");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mimeType == null) {
|
||||||
|
Log.w(TAG, "No mime type");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileName == null) {
|
||||||
|
fileName = createFileNameForMimeType(mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createCursor(projection, fileName, fileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public String getType(@NonNull Uri uri) {
|
||||||
|
return BlobProvider.getMimeType(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
@ -45,7 +45,7 @@ public class BlobProvider {
|
|||||||
private static final String MULTI_SESSION_DIRECTORY = "multi_session_blobs";
|
private static final String MULTI_SESSION_DIRECTORY = "multi_session_blobs";
|
||||||
private static final String SINGLE_SESSION_DIRECTORY = "single_session_blobs";
|
private static final String SINGLE_SESSION_DIRECTORY = "single_session_blobs";
|
||||||
|
|
||||||
public static final String AUTHORITY = BuildConfig.APPLICATION_ID;
|
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".blob";
|
||||||
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/blob");
|
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/blob");
|
||||||
public static final String PATH = "blob/*/*/*/*/*";
|
public static final String PATH = "blob/*/*/*/*/*";
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.thoughtcrime.securesms.providers;
|
package org.thoughtcrime.securesms.providers;
|
||||||
|
|
||||||
import android.content.ContentProvider;
|
|
||||||
import android.content.ContentUris;
|
import android.content.ContentUris;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -35,7 +34,7 @@ import java.io.FileNotFoundException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
public class MmsBodyProvider extends ContentProvider {
|
public final class MmsBodyProvider extends BaseContentProvider {
|
||||||
private static final String TAG = MmsBodyProvider.class.getSimpleName();
|
private static final String TAG = MmsBodyProvider.class.getSimpleName();
|
||||||
private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".mms";
|
private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".mms";
|
||||||
private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY + "/mms";
|
private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY + "/mms";
|
||||||
|
@ -16,18 +16,16 @@
|
|||||||
*/
|
*/
|
||||||
package org.thoughtcrime.securesms.providers;
|
package org.thoughtcrime.securesms.providers;
|
||||||
|
|
||||||
import android.content.ContentProvider;
|
|
||||||
import android.content.ContentUris;
|
import android.content.ContentUris;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.UriMatcher;
|
import android.content.UriMatcher;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.MatrixCursor;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.MemoryFile;
|
import android.os.MemoryFile;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.provider.OpenableColumns;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.BuildConfig;
|
import org.thoughtcrime.securesms.BuildConfig;
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||||
@ -44,9 +42,9 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
public class PartProvider extends ContentProvider {
|
public final class PartProvider extends BaseContentProvider {
|
||||||
|
|
||||||
private static final String TAG = PartProvider.class.getSimpleName();
|
private static final String TAG = Log.tag(PartProvider.class);
|
||||||
|
|
||||||
private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".part";
|
private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".part";
|
||||||
private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY + "/part";
|
private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY + "/part";
|
||||||
@ -80,8 +78,7 @@ public class PartProvider extends ContentProvider {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (uriMatcher.match(uri)) {
|
if (uriMatcher.match(uri) == SINGLE_ROW) {
|
||||||
case SINGLE_ROW:
|
|
||||||
Log.i(TAG, "Parting out a single row...");
|
Log.i(TAG, "Parting out a single row...");
|
||||||
try {
|
try {
|
||||||
final PartUriParser partUri = new PartUriParser(uri);
|
final PartUriParser partUri = new PartUriParser(uri);
|
||||||
@ -105,13 +102,12 @@ public class PartProvider extends ContentProvider {
|
|||||||
public String getType(@NonNull Uri uri) {
|
public String getType(@NonNull Uri uri) {
|
||||||
Log.i(TAG, "getType() called: " + uri);
|
Log.i(TAG, "getType() called: " + uri);
|
||||||
|
|
||||||
switch (uriMatcher.match(uri)) {
|
if (uriMatcher.match(uri) == SINGLE_ROW) {
|
||||||
case SINGLE_ROW:
|
|
||||||
PartUriParser partUriParser = new PartUriParser(uri);
|
PartUriParser partUriParser = new PartUriParser(uri);
|
||||||
DatabaseAttachment attachment = DatabaseFactory.getAttachmentDatabase(getContext())
|
DatabaseAttachment attachment = DatabaseFactory.getAttachmentDatabase(getContext()).getAttachment(partUriParser.getPartId());
|
||||||
.getAttachment(partUriParser.getPartId());
|
|
||||||
|
|
||||||
if (attachment != null) {
|
if (attachment != null) {
|
||||||
|
Log.i(TAG, "getType() called: " + uri + " It's " + attachment.getContentType());
|
||||||
return attachment.getContentType();
|
return attachment.getContentType();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,34 +122,31 @@ public class PartProvider extends ContentProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Cursor query(@NonNull Uri url, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
public Cursor query(@NonNull Uri url, @Nullable String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||||
Log.i(TAG, "query() called: " + url);
|
Log.i(TAG, "query() called: " + url);
|
||||||
|
|
||||||
if (projection == null || projection.length <= 0) return null;
|
if (uriMatcher.match(url) == SINGLE_ROW) {
|
||||||
|
|
||||||
switch (uriMatcher.match(url)) {
|
|
||||||
case SINGLE_ROW:
|
|
||||||
PartUriParser partUri = new PartUriParser(url);
|
PartUriParser partUri = new PartUriParser(url);
|
||||||
DatabaseAttachment attachment = DatabaseFactory.getAttachmentDatabase(getContext()).getAttachment(partUri.getPartId());
|
DatabaseAttachment attachment = DatabaseFactory.getAttachmentDatabase(getContext()).getAttachment(partUri.getPartId());
|
||||||
|
|
||||||
if (attachment == null) return null;
|
if (attachment == null) return null;
|
||||||
|
|
||||||
MatrixCursor matrixCursor = new MatrixCursor(projection, 1);
|
long fileSize = attachment.getSize();
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (fileSize <= 0) {
|
||||||
|
Log.w(TAG, "Empty file " + fileSize);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String fileName = attachment.getFileName() != null ? attachment.getFileName()
|
||||||
|
: createFileNameForMimeType(attachment.getContentType());
|
||||||
|
|
||||||
|
return createCursor(projection, fileName, fileSize);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int update(@NonNull Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
|
public int update(@NonNull Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
|
||||||
Log.i(TAG, "update() called");
|
Log.i(TAG, "update() called");
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
package org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.qr;
|
package org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.qr;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@ -8,6 +13,7 @@ import android.view.ViewGroup;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.core.app.ShareCompat;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
@ -16,9 +22,13 @@ import org.thoughtcrime.securesms.R;
|
|||||||
import org.thoughtcrime.securesms.components.qr.QrView;
|
import org.thoughtcrime.securesms.components.qr.QrView;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
|
import org.thoughtcrime.securesms.qr.QrCode;
|
||||||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public class GroupLinkShareQrDialogFragment extends DialogFragment {
|
public class GroupLinkShareQrDialogFragment extends DialogFragment {
|
||||||
@ -85,8 +95,49 @@ public class GroupLinkShareQrDialogFragment extends DialogFragment {
|
|||||||
private void presentUrl(@Nullable String url) {
|
private void presentUrl(@Nullable String url) {
|
||||||
qrImageView.setQrText(url);
|
qrImageView.setQrText(url);
|
||||||
|
|
||||||
|
// Restricted to API26 because of MemoryFileUtil not supporting lower API levels well
|
||||||
|
if (Build.VERSION.SDK_INT >= 26) {
|
||||||
|
shareCodeButton.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
shareCodeButton.setOnClickListener(v -> {
|
shareCodeButton.setOnClickListener(v -> {
|
||||||
// TODO [Alan] GV2 Allow qr image share
|
Uri shareUri;
|
||||||
|
|
||||||
|
try {
|
||||||
|
shareUri = createTemporaryPng(url);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent intent = ShareCompat.IntentBuilder.from(requireActivity())
|
||||||
|
.setType("image/png")
|
||||||
|
.setStream(shareUri)
|
||||||
|
.createChooserIntent()
|
||||||
|
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
|
||||||
|
requireContext().startActivity(intent);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
shareCodeButton.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Uri createTemporaryPng(@Nullable String url) throws IOException {
|
||||||
|
Bitmap qrBitmap = QrCode.create(url, Color.BLACK, Color.WHITE);
|
||||||
|
|
||||||
|
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
|
||||||
|
qrBitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
|
||||||
|
byteArrayOutputStream.flush();
|
||||||
|
|
||||||
|
byte[] bytes = byteArrayOutputStream.toByteArray();
|
||||||
|
|
||||||
|
return BlobProvider.getInstance()
|
||||||
|
.forData(bytes)
|
||||||
|
.withMimeType("image/png")
|
||||||
|
.withFileName("SignalGroupQr.png")
|
||||||
|
.createForSingleSessionInMemory();
|
||||||
|
} finally {
|
||||||
|
qrBitmap.recycle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.HandlerThread;
|
||||||
|
import android.os.MemoryFile;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.os.ProxyFileDescriptorCallback;
|
||||||
|
import android.os.storage.StorageManager;
|
||||||
|
import android.system.ErrnoException;
|
||||||
|
import android.system.OsConstants;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@RequiresApi(api = 26)
|
||||||
|
final class MemoryFileDescriptorProxy {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(MemoryFileDescriptorProxy.class);
|
||||||
|
|
||||||
|
public static ParcelFileDescriptor create(@NonNull Context context,
|
||||||
|
@NonNull MemoryFile file)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
StorageManager storageManager = Objects.requireNonNull(context.getSystemService(StorageManager.class));
|
||||||
|
HandlerThread thread = new HandlerThread("MemoryFile");
|
||||||
|
|
||||||
|
thread.start();
|
||||||
|
Log.i(TAG, "Thread started");
|
||||||
|
|
||||||
|
Handler handler = new Handler(thread.getLooper());
|
||||||
|
ProxyCallback proxyCallback = new ProxyCallback(file, () -> {
|
||||||
|
if (thread.quitSafely()) {
|
||||||
|
Log.i(TAG, "Thread quitSafely true");
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Thread quitSafely false");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ParcelFileDescriptor parcelFileDescriptor = storageManager.openProxyFileDescriptor(ParcelFileDescriptor.MODE_READ_ONLY,
|
||||||
|
proxyCallback,
|
||||||
|
handler);
|
||||||
|
|
||||||
|
Log.i(TAG, "Created");
|
||||||
|
return parcelFileDescriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ProxyCallback extends ProxyFileDescriptorCallback {
|
||||||
|
|
||||||
|
private final MemoryFile memoryFile;
|
||||||
|
private final Runnable onClose;
|
||||||
|
|
||||||
|
ProxyCallback(@NonNull MemoryFile memoryFile, Runnable onClose) {
|
||||||
|
this.memoryFile = memoryFile;
|
||||||
|
this.onClose = onClose;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long onGetSize() {
|
||||||
|
return memoryFile.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onRead(long offset, int size, byte[] data) throws ErrnoException {
|
||||||
|
try {
|
||||||
|
InputStream inputStream = memoryFile.getInputStream();
|
||||||
|
if(inputStream.skip(offset) != offset){
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
return inputStream.read(data, 0, size);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ErrnoException("onRead", OsConstants.EBADF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRelease() {
|
||||||
|
Log.i(TAG, "onRelease()");
|
||||||
|
memoryFile.close();
|
||||||
|
onClose.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,19 +1,39 @@
|
|||||||
package org.thoughtcrime.securesms.util;
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.MemoryFile;
|
import android.os.MemoryFile;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
|
||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
public class MemoryFileUtil {
|
public final class MemoryFileUtil {
|
||||||
|
|
||||||
public static ParcelFileDescriptor getParcelFileDescriptor(MemoryFile file) throws IOException {
|
private MemoryFileUtil() {}
|
||||||
|
|
||||||
|
public static ParcelFileDescriptor getParcelFileDescriptor(@NonNull MemoryFile file)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SDK_INT >= 26) {
|
||||||
|
return MemoryFileDescriptorProxy.create(ApplicationDependencies.getApplication(), file);
|
||||||
|
} else {
|
||||||
|
return getParcelFileDescriptorLegacy(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("JavaReflectionMemberAccess")
|
||||||
|
@SuppressLint("PrivateApi")
|
||||||
|
public static ParcelFileDescriptor getParcelFileDescriptorLegacy(@NonNull MemoryFile file)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
try {
|
try {
|
||||||
Method method = MemoryFile.class.getDeclaredMethod("getFileDescriptor");
|
Method method = MemoryFile.class.getDeclaredMethod("getFileDescriptor");
|
||||||
FileDescriptor fileDescriptor = (FileDescriptor) method.invoke(file);
|
FileDescriptor fileDescriptor = (FileDescriptor) method.invoke(file);
|
||||||
@ -24,13 +44,7 @@ public class MemoryFileUtil {
|
|||||||
int fd = field.getInt(fileDescriptor);
|
int fd = field.getInt(fileDescriptor);
|
||||||
|
|
||||||
return ParcelFileDescriptor.adoptFd(fd);
|
return ParcelFileDescriptor.adoptFd(fd);
|
||||||
} catch (IllegalAccessException e) {
|
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | NoSuchFieldException e) {
|
||||||
throw new IOException(e);
|
|
||||||
} catch (InvocationTargetException e) {
|
|
||||||
throw new IOException(e);
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
throw new IOException(e);
|
|
||||||
} catch (NoSuchFieldException e) {
|
|
||||||
throw new IOException(e);
|
throw new IOException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,9 @@
|
|||||||
android:title="@string/media_preview__forward_title"
|
android:title="@string/media_preview__forward_title"
|
||||||
android:icon="?menu_forward_icon"
|
android:icon="?menu_forward_icon"
|
||||||
app:showAsAction="never"/>
|
app:showAsAction="never"/>
|
||||||
|
<item android:id="@+id/media_preview__share"
|
||||||
|
android:title="@string/media_preview__share_title"
|
||||||
|
app:showAsAction="never"/>
|
||||||
<item android:id="@+id/save"
|
<item android:id="@+id/save"
|
||||||
android:title="@string/media_preview__save_title"
|
android:title="@string/media_preview__save_title"
|
||||||
android:icon="@drawable/ic_download_filled_white_24"
|
android:icon="@drawable/ic_download_filled_white_24"
|
||||||
|
@ -1562,6 +1562,7 @@
|
|||||||
<string name="MediaPreviewActivity_media_delete_confirmation_message">This will permanently delete this message.</string>
|
<string name="MediaPreviewActivity_media_delete_confirmation_message">This will permanently delete this message.</string>
|
||||||
<string name="MediaPreviewActivity_s_to_s">%1$s to %2$s</string>
|
<string name="MediaPreviewActivity_s_to_s">%1$s to %2$s</string>
|
||||||
<string name="MediaPreviewActivity_media_no_longer_available">Media no longer available.</string>
|
<string name="MediaPreviewActivity_media_no_longer_available">Media no longer available.</string>
|
||||||
|
<string name="MediaPreviewActivity_cant_find_an_app_able_to_share_this_media">Can\'t find an app able to share this media.</string>
|
||||||
|
|
||||||
<!-- MessageNotifier -->
|
<!-- MessageNotifier -->
|
||||||
<string name="MessageNotifier_d_new_messages_in_d_conversations">%1$d new messages in %2$d conversations</string>
|
<string name="MessageNotifier_d_new_messages_in_d_conversations">%1$d new messages in %2$d conversations</string>
|
||||||
@ -2438,6 +2439,7 @@
|
|||||||
<!-- media_preview -->
|
<!-- media_preview -->
|
||||||
<string name="media_preview__save_title">Save</string>
|
<string name="media_preview__save_title">Save</string>
|
||||||
<string name="media_preview__forward_title">Forward</string>
|
<string name="media_preview__forward_title">Forward</string>
|
||||||
|
<string name="media_preview__share_title">Share</string>
|
||||||
<string name="media_preview__all_media_title">All media</string>
|
<string name="media_preview__all_media_title">All media</string>
|
||||||
|
|
||||||
<!-- media_preview_activity -->
|
<!-- media_preview_activity -->
|
||||||
|
Loading…
x
Reference in New Issue
Block a user