Fix mediastore access for Android Q.

This commit is contained in:
Alex Hart 2020-10-19 18:16:29 -03:00 committed by GitHub
parent 3163e09b98
commit dc64a186d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 247 additions and 200 deletions

View file

@ -36,7 +36,8 @@
<uses-permission android:name="android.permission.WRITE_SMS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.CAMERA" />

View file

@ -70,6 +70,7 @@ import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import org.thoughtcrime.securesms.util.StorageUtil;
import java.util.HashMap;
import java.util.Locale;
@ -384,21 +385,30 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
if (mediaItem != null) {
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
if (StorageUtil.canWriteToMediaStore()) {
performSavetoDisk(mediaItem);
return;
}
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
.onAllGranted(() -> {
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
performSavetoDisk(mediaItem);
})
.execute();
});
}
}
private void performSavetoDisk(@NonNull MediaItem mediaItem) {
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
}
@SuppressLint("StaticFieldLeak")
private void deleteMedia() {
MediaItem mediaItem = getCurrentMediaItem();

View file

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components;
import android.annotation.TargetApi;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
@ -106,7 +107,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
public void onBindItemViewHolder(RecentPhotoViewHolder viewHolder, @NonNull Cursor cursor) {
viewHolder.imageView.setImageDrawable(null);
String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATA));
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID));
long dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN));
long dateModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_MODIFIED));
String mimeType = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.MIME_TYPE));
@ -116,7 +117,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
int width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation)));
int height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation)));
final Uri uri = Uri.fromFile(new File(path));
final Uri uri = ContentUris.withAppendedId(RecentPhotosLoader.BASE_URL, rowId);
Key signature = new MediaStoreSignature(mimeType, dateModified, orientation);

View file

@ -1,8 +1,6 @@
package org.thoughtcrime.securesms.conversation;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
@ -10,7 +8,6 @@ import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@ -19,6 +16,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.InputAwareLayout;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.util.StorageUtil;
import java.util.Arrays;
import java.util.List;
@ -84,7 +82,7 @@ public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout.
}
public void onMediaChanged(@NonNull List<Media> media) {
if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
if (StorageUtil.canReadFromMediaStore()) {
mediaAdapter.setMedia(media);
permissionButton.setVisibility(GONE);
permissionText.setVisibility(GONE);

View file

@ -1034,6 +1034,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
Permissions.with(this)
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
.onAllGranted(() -> viewModel.onAttachmentKeyboardOpen())
.withPermanentDenialDialog(getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
.execute();
}

View file

@ -16,6 +16,7 @@
*/
package org.thoughtcrime.securesms.conversation;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ClipData;
@ -102,6 +103,7 @@ import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.UnknownSenderView;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
@ -123,6 +125,7 @@ import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.StorageUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
@ -854,26 +857,40 @@ public class ConversationFragment extends LoggingFragment {
throw new AssertionError("Cannot save a view-once message.");
}
SaveAttachmentTask.showWarningDialog(getActivity(), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
List<SaveAttachmentTask.Attachment> attachments = Stream.of(message.getSlideDeck().getSlides())
.filter(s -> s.getUri() != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()))
.map(s -> new SaveAttachmentTask.Attachment(s.getUri(), s.getContentType(), message.getDateReceived(), s.getFileName().orNull()))
.toList();
if (!Util.isEmpty(attachments)) {
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity());
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0]));
return;
}
Log.w(TAG, "No slide with attachable media found, failing nicely.");
Toast.makeText(getActivity(),
getResources().getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1),
Toast.LENGTH_LONG).show();
SaveAttachmentTask.showWarningDialog(getActivity(), (dialog, which) -> {
if (StorageUtil.canWriteToMediaStore()) {
performSave(message);
return;
}
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
.onAnyDenied(() -> Toast.makeText(requireContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
.onAllGranted(() -> performSave(message))
.execute();
});
}
private void performSave(final MediaMmsMessageRecord message) {
List<SaveAttachmentTask.Attachment> attachments = Stream.of(message.getSlideDeck().getSlides())
.filter(s -> s.getUri() != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()))
.map(s -> new SaveAttachmentTask.Attachment(s.getUri(), s.getContentType(), message.getDateReceived(), s.getFileName().orNull()))
.toList();
if (!Util.isEmpty(attachments)) {
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity());
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0]));
return;
}
Log.w(TAG, "No slide with attachable media found, failing nicely.");
Toast.makeText(getActivity(),
getResources().getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1),
Toast.LENGTH_LONG).show();
}
private void clearHeaderIfNotTyping(ConversationAdapter adapter) {
if (adapter.getHeaderView() != typingView) {
adapter.setHeaderView(null);

View file

@ -5,6 +5,7 @@ import android.Manifest;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import androidx.loader.content.CursorLoader;
@ -15,7 +16,7 @@ public class RecentPhotosLoader extends CursorLoader {
public static Uri BASE_URL = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
private static final String[] PROJECTION = new String[] {
MediaStore.Images.ImageColumns.DATA,
MediaStore.Images.ImageColumns._ID,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns.DATE_MODIFIED,
MediaStore.Images.ImageColumns.ORIENTATION,
@ -26,7 +27,8 @@ public class RecentPhotosLoader extends CursorLoader {
MediaStore.Images.ImageColumns.HEIGHT
};
private static final String SELECTION = MediaStore.Images.Media.DATA + " NOT NULL";
private static final String SELECTION = Build.VERSION.SDK_INT > 28 ? MediaStore.Images.Media.IS_PENDING + " != 1"
: MediaStore.Images.Media.DATA + " IS NULL";
private final Context context;

View file

@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.database.MediaDatabase;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.StorageUtil;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import java.util.Collection;
@ -32,43 +33,18 @@ final class MediaActions {
{
Context context = fragment.requireContext();
if (StorageUtil.canWriteToMediaStore()) {
performSaveToDisk(context, mediaRecords, postExecute);
return;
}
SaveAttachmentTask.showWarningDialog(context, (dialogInterface, which) -> Permissions.with(fragment)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(fragment.getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
.onAnyDenied(() -> Toast.makeText(context, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
.onAllGranted(() ->
new ProgressDialogAsyncTask<Void, Void, List<SaveAttachmentTask.Attachment>>(context,
R.string.MediaOverviewActivity_collecting_attachments,
R.string.please_wait)
{
@Override
protected List<SaveAttachmentTask.Attachment> doInBackground(Void... params) {
List<SaveAttachmentTask.Attachment> attachments = new LinkedList<>();
for (MediaDatabase.MediaRecord mediaRecord : mediaRecords) {
if (mediaRecord.getAttachment().getUri() != null) {
attachments.add(new SaveAttachmentTask.Attachment(mediaRecord.getAttachment().getUri(),
mediaRecord.getContentType(),
mediaRecord.getDate(),
mediaRecord.getAttachment().getFileName()));
}
}
return attachments;
}
@Override
protected void onPostExecute(List<SaveAttachmentTask.Attachment> attachments) {
super.onPostExecute(attachments);
SaveAttachmentTask saveTask = new SaveAttachmentTask(context, attachments.size());
saveTask.executeOnExecutor(THREAD_POOL_EXECUTOR,
attachments.toArray(new SaveAttachmentTask.Attachment[0]));
if (postExecute != null) postExecute.run();
}
}.execute()
).execute(), mediaRecords.size());
.onAllGranted(() -> performSaveToDisk(context, mediaRecords, postExecute))
.execute(), mediaRecords.size());
}
static void handleDeleteMedia(@NonNull Context context,
@ -111,4 +87,37 @@ final class MediaActions {
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
private static void performSaveToDisk(@NonNull Context context, @NonNull Collection<MediaDatabase.MediaRecord> mediaRecords, @Nullable Runnable postExecute) {
new ProgressDialogAsyncTask<Void, Void, List<SaveAttachmentTask.Attachment>>(context,
R.string.MediaOverviewActivity_collecting_attachments,
R.string.please_wait)
{
@Override
protected List<SaveAttachmentTask.Attachment> doInBackground(Void... params) {
List<SaveAttachmentTask.Attachment> attachments = new LinkedList<>();
for (MediaDatabase.MediaRecord mediaRecord : mediaRecords) {
if (mediaRecord.getAttachment().getUri() != null) {
attachments.add(new SaveAttachmentTask.Attachment(mediaRecord.getAttachment().getUri(),
mediaRecord.getContentType(),
mediaRecord.getDate(),
mediaRecord.getAttachment().getFileName()));
}
}
return attachments;
}
@Override
protected void onPostExecute(List<SaveAttachmentTask.Attachment> attachments) {
super.onPostExecute(attachments);
SaveAttachmentTask saveTask = new SaveAttachmentTask(context, attachments.size());
saveTask.executeOnExecutor(THREAD_POOL_EXECUTOR,
attachments.toArray(new SaveAttachmentTask.Attachment[0]));
if (postExecute != null) postExecute.run();
}
}.execute();
}
}

View file

@ -1,11 +1,12 @@
package org.thoughtcrime.securesms.mediasend;
import android.Manifest;
import android.annotation.TargetApi;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.os.Build;
import android.provider.MediaStore;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Video;
import android.provider.OpenableColumns;
@ -20,13 +21,12 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.StorageUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
@ -84,7 +84,7 @@ public class MediaRepository {
@WorkerThread
private @NonNull List<MediaFolder> getFolders(@NonNull Context context) {
if (!Permissions.hasAll(context, Manifest.permission.READ_EXTERNAL_STORAGE)) {
if (!StorageUtil.canReadFromMediaStore()) {
return Collections.emptyList();
}
@ -132,20 +132,19 @@ public class MediaRepository {
@WorkerThread
private @NonNull FolderResult getFolders(@NonNull Context context, @NonNull Uri contentUri) {
String cameraPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() + File.separator + "Camera";
String cameraBucketId = null;
Uri globalThumbnail = null;
long thumbnailTimestamp = 0;
Map<String, FolderData> folders = new HashMap<>();
String[] projection = new String[] { Images.Media.DATA, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_MODIFIED };
String selection = Images.Media.DATA + " NOT NULL";
String[] projection = new String[] { Images.Media._ID, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_MODIFIED };
String selection = isNotPending();
String sortBy = Images.Media.BUCKET_DISPLAY_NAME + " COLLATE NOCASE ASC, " + Images.Media.DATE_MODIFIED + " DESC";
try (Cursor cursor = context.getContentResolver().query(contentUri, projection, selection, null, sortBy)) {
while (cursor != null && cursor.moveToNext()) {
String path = cursor.getString(cursor.getColumnIndexOrThrow(projection[0]));
Uri thumbnail = Uri.fromFile(new File(path));
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0]));
Uri thumbnail = ContentUris.withAppendedId(contentUri, rowId);
String bucketId = cursor.getString(cursor.getColumnIndexOrThrow(projection[1]));
String title = cursor.getString(cursor.getColumnIndexOrThrow(projection[2]));
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(projection[3]));
@ -154,7 +153,7 @@ public class MediaRepository {
folder.incrementCount();
folders.put(bucketId, folder);
if (cameraBucketId == null && path.startsWith(cameraPath)) {
if (cameraBucketId == null && "Camera".equals(title)) {
cameraBucketId = bucketId;
}
@ -170,7 +169,7 @@ public class MediaRepository {
@WorkerThread
private @NonNull List<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId) {
if (!Permissions.hasAll(context, Manifest.permission.READ_EXTERNAL_STORAGE)) {
if (!StorageUtil.canReadFromMediaStore()) {
return Collections.emptyList();
}
@ -188,27 +187,27 @@ public class MediaRepository {
@WorkerThread
private @NonNull List<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean isImage) {
List<Media> media = new LinkedList<>();
String selection = Images.Media.BUCKET_ID + " = ? AND " + Images.Media.DATA + " NOT NULL";
String selection = Images.Media.BUCKET_ID + " = ? AND " + isNotPending();
String[] selectionArgs = new String[] { bucketId };
String sortBy = Images.Media.DATE_MODIFIED + " DESC";
String[] projection;
if (isImage) {
projection = new String[]{Images.Media.DATA, Images.Media.MIME_TYPE, Images.Media.DATE_MODIFIED, Images.Media.ORIENTATION, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE};
projection = new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_MODIFIED, Images.Media.ORIENTATION, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE};
} else {
projection = new String[]{Images.Media.DATA, Images.Media.MIME_TYPE, Images.Media.DATE_MODIFIED, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE, Video.Media.DURATION};
projection = new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_MODIFIED, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE, Video.Media.DURATION};
}
if (Media.ALL_MEDIA_BUCKET_ID.equals(bucketId)) {
selection = Images.Media.DATA + " NOT NULL";
selection = isNotPending();
selectionArgs = null;
}
try (Cursor cursor = context.getContentResolver().query(contentUri, projection, selection, selectionArgs, sortBy)) {
while (cursor != null && cursor.moveToNext()) {
String path = cursor.getString(cursor.getColumnIndexOrThrow(projection[0]));
Uri uri = Uri.fromFile(new File(path));
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0]));
Uri uri = ContentUris.withAppendedId(contentUri, rowId);
String mimetype = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.MIME_TYPE));
long date = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.DATE_MODIFIED));
int orientation = isImage ? cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) : 0;
@ -224,12 +223,12 @@ public class MediaRepository {
return media;
}
private @NonNull String isNotPending() {
return Build.VERSION.SDK_INT <= 28 ? Images.Media.DATA + " NOT NULL" : MediaStore.MediaColumns.IS_PENDING + " != 1";
}
@WorkerThread
private List<Media> getPopulatedMedia(@NonNull Context context, @NonNull List<Media> media) {
if (!Permissions.hasAll(context, Manifest.permission.READ_EXTERNAL_STORAGE)) {
return media;
}
return Stream.of(media).map(m -> {
try {
if (isPopulated(m)) {
@ -265,10 +264,6 @@ public class MediaRepository {
@WorkerThread
private Optional<Media> getMostRecentItem(@NonNull Context context) {
if (!Permissions.hasAll(context, Manifest.permission.READ_EXTERNAL_STORAGE)) {
return Optional.absent();
}
List<Media> media = getMediaInBucket(context, Media.ALL_MEDIA_BUCKET_ID, Images.Media.EXTERNAL_CONTENT_URI, true);
return media.size() > 0 ? Optional.of(media.get(0)) : Optional.absent();
}

View file

@ -367,36 +367,21 @@ public class AttachmentManager {
}
public static void selectDocument(Activity activity, int requestCode) {
Permissions.with(activity)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
.onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode))
.execute();
selectMediaType(activity, "*/*", null, requestCode);
}
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull CharSequence body, @NonNull TransportOption transport) {
Permissions.with(activity)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
.onAllGranted(() -> selectMediaType(activity, "image/*", new String[] {"image/*", "video/*"}, requestCode))
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body, transport), requestCode))
.execute();
}
public static void selectAudio(Activity activity, int requestCode) {
Permissions.with(activity)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
.onAllGranted(() -> selectMediaType(activity, "audio/*", null, requestCode))
.execute();
}
public static void selectContactInfo(Activity activity, int requestCode) {
Permissions.with(activity)
.request(Manifest.permission.WRITE_CONTACTS)
.request(Manifest.permission.READ_CONTACTS)
.ifNecessary()
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information))
.onAllGranted(() -> {
@ -430,29 +415,6 @@ public class AttachmentManager {
return captureUri;
}
public void capturePhoto(Activity activity, int requestCode) {
Permissions.with(activity)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied))
.onAllGranted(() -> {
try {
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
if (captureUri == null) {
captureUri = DeprecatedPersistentBlobProvider.getInstance(context).createForExternal(context, MediaUtil.IMAGE_JPEG);
}
Log.d(TAG, "captureUri path is " + captureUri.getPath());
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, captureUri);
activity.startActivityForResult(captureIntent, requestCode);
}
} catch (IOException ioe) {
Log.w(TAG, ioe);
}
})
.execute();
}
private static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) {
final Intent intent = new Intent();
intent.setType(type);

View file

@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ParcelUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.StorageUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
@ -412,29 +413,17 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
@Override
public void onSave() {
SaveAttachmentTask.showWarningDialog(requireContext(), (dialogInterface, i) -> {
if (StorageUtil.canWriteToMediaStore()) {
performSaveToDisk();
return;
}
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
.onAnyDenied(() -> Toast.makeText(requireContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
.onAllGranted(() -> {
SimpleTask.run(() -> {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Bitmap image = imageEditorView.getModel().render(requireContext());
image.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
return BlobProvider.getInstance()
.forData(outputStream.toByteArray())
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleUseInMemory();
}, uri -> {
SaveAttachmentTask saveTask = new SaveAttachmentTask(requireContext());
SaveAttachmentTask.Attachment attachment = new SaveAttachmentTask.Attachment(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), null);
saveTask.executeOnExecutor(SignalExecutors.BOUNDED, attachment);
});
})
.onAllGranted(this::performSaveToDisk)
.execute();
});
}
@ -469,6 +458,25 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
controller.onDoneEditing();
}
private void performSaveToDisk() {
SimpleTask.run(() -> {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Bitmap image = imageEditorView.getModel().render(requireContext());
image.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
return BlobProvider.getInstance()
.forData(outputStream.toByteArray())
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleUseInMemory();
}, uri -> {
SaveAttachmentTask saveTask = new SaveAttachmentTask(requireContext());
SaveAttachmentTask.Attachment attachment = new SaveAttachmentTask.Attachment(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), null);
saveTask.executeOnExecutor(SignalExecutors.BOUNDED, attachment);
});
}
private void refreshUniqueColors() {
imageEditorHud.setColorPalette(imageEditorView.getModel().getUniqueColorsIgnoringAlpha());
}

View file

@ -244,12 +244,12 @@ public class BackupUtil {
private final long timestamp;
private final long size;
private final Uri uri;
private final Uri uri;
BackupInfo(long timestamp, long size, Uri uri) {
this.timestamp = timestamp;
this.size = size;
this.uri = uri;
this.size = size;
this.uri = uri;
}
public long getTimestamp() {

View file

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.util;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface.OnClickListener;
import android.media.MediaScannerConnection;
@ -7,6 +8,10 @@ import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.documentfile.provider.DocumentFile;
import android.os.Build;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
import android.widget.Toast;
@ -59,7 +64,7 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
Context context = contextReference.get();
String directory = null;
if (!StorageUtil.canWriteInSignalStorageDir()) {
if (!StorageUtil.canWriteToMediaStore()) {
return new Pair<>(WRITE_ACCESS_FAILURE, null);
}
@ -76,14 +81,13 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
if (attachments.length > 1) return new Pair<>(SUCCESS, null);
else return new Pair<>(SUCCESS, directory);
} catch (NoExternalStorageException|IOException ioe) {
} catch (IOException ioe) {
Log.w(TAG, ioe);
return new Pair<>(FAILURE, null);
}
}
private @Nullable String saveAttachment(Context context, Attachment attachment)
throws NoExternalStorageException, IOException
private @Nullable String saveAttachment(Context context, Attachment attachment) throws IOException
{
String contentType = MediaUtil.getCorrectedMimeType(attachment.contentType);
String fileName = attachment.fileName;
@ -91,40 +95,46 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
if (fileName == null) fileName = generateOutputFileName(contentType, attachment.date);
fileName = sanitizeOutputFileName(fileName);
File outputDirectory = createOutputDirectoryFromContentType(contentType);
File mediaFile = createOutputFile(outputDirectory, fileName);
InputStream inputStream = PartAuthority.getAttachmentStream(context, attachment.uri);
Uri outputUri = getMediaStoreContentUriForType(contentType);
Uri mediaUri = createOutputUri(outputUri, fileName);
if (inputStream == null) {
return null;
try (InputStream inputStream = PartAuthority.getAttachmentStream(context, attachment.uri)) {
if (inputStream == null) {
return null;
}
if (outputUri.equals(StorageUtil.getLegacyDownloadUri())) {
try (OutputStream outputStream = new FileOutputStream(mediaUri.getPath())) {
Util.copy(inputStream, outputStream);
MediaScannerConnection.scanFile(context, new String[]{mediaUri.getPath()}, new String[]{contentType}, null);
}
}
try (OutputStream outputStream = context.getContentResolver().openOutputStream(mediaUri)) {
Util.copy(inputStream, outputStream);
}
}
OutputStream outputStream = new FileOutputStream(mediaFile);
Util.copy(inputStream, outputStream);
if (Build.VERSION.SDK_INT > 28) {
ContentValues updatePendingValues = new ContentValues();
updatePendingValues.put(MediaStore.MediaColumns.IS_PENDING, 0);
getContext().getContentResolver().update(mediaUri, updatePendingValues, null, null);
}
MediaScannerConnection.scanFile(context, new String[]{mediaFile.getAbsolutePath()},
new String[]{contentType}, null);
return outputDirectory.getName();
return outputUri.getLastPathSegment();
}
private File createOutputDirectoryFromContentType(@NonNull String contentType)
throws NoExternalStorageException
{
File outputDirectory;
private @NonNull Uri getMediaStoreContentUriForType(@NonNull String contentType) {
if (contentType.startsWith("video/")) {
outputDirectory = StorageUtil.getVideoDir();
return StorageUtil.getVideoUri();
} else if (contentType.startsWith("audio/")) {
outputDirectory = StorageUtil.getAudioDir();
return StorageUtil.getAudioUri();
} else if (contentType.startsWith("image/")) {
outputDirectory = StorageUtil.getImageDir();
return StorageUtil.getImageUri();
} else {
outputDirectory = StorageUtil.getDownloadDir();
return StorageUtil.getDownloadUri();
}
if (!outputDirectory.mkdirs()) Log.w(TAG, "mkdirs() returned false, attempting to continue");
return outputDirectory;
}
private String generateOutputFileName(@NonNull String contentType, long timestamp) {
@ -142,25 +152,34 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
return new File(fileName).getName();
}
private File createOutputFile(@NonNull File outputDirectory, @NonNull String fileName)
throws IOException
{
String[] fileParts = getFileNameParts(fileName);
String base = fileParts[0];
String extension = fileParts[1];
private Uri createOutputUri(@NonNull Uri outputUri, @NonNull String fileName) throws IOException {
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
File outputFile = new File(outputDirectory, base + "." + extension);
int i = 0;
while (outputFile.exists()) {
outputFile = new File(outputDirectory, base + "-" + (++i) + "." + extension);
if (Build.VERSION.SDK_INT > 28) {
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 1);
}
if (outputFile.isHidden()) {
throw new IOException("Specified name would not be visible");
if (Build.VERSION.SDK_INT <= 28 && outputUri.equals(StorageUtil.getLegacyDownloadUri())) {
String[] fileParts = getFileNameParts(fileName);
String base = fileParts[0];
String extension = fileParts[1];
File outputDirectory = new File(outputUri.getPath());
File outputFile = new File(outputDirectory, base + "." + extension);
int i = 0;
while (outputFile.exists()) {
outputFile = new File(outputDirectory, base + "-" + (++i) + "." + extension);
}
if (outputFile.isHidden()) {
throw new IOException("Specified name would not be visible");
}
return Uri.fromFile(outputFile);
}
return outputFile;
return getContext().getContentResolver().insert(outputUri, contentValues);
}
private String[] getFileNameParts(String fileName) {

View file

@ -1,12 +1,19 @@
package org.thoughtcrime.securesms.util;
import android.Manifest;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.database.NoExternalStorageException;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.permissions.Permissions;
import java.io.File;
@ -68,20 +75,37 @@ public class StorageUtil {
return getSignalStorageDir();
}
public static File getVideoDir() throws NoExternalStorageException {
return new File(getSignalStorageDir(), Environment.DIRECTORY_MOVIES);
public static boolean canWriteToMediaStore() {
return Build.VERSION.SDK_INT > 28 ||
Permissions.hasAll(ApplicationDependencies.getApplication(), Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
public static File getAudioDir() throws NoExternalStorageException {
return new File(getSignalStorageDir(), Environment.DIRECTORY_MUSIC);
public static boolean canReadFromMediaStore() {
return Permissions.hasAll(ApplicationDependencies.getApplication(), Manifest.permission.READ_EXTERNAL_STORAGE);
}
public static File getImageDir() throws NoExternalStorageException {
return new File(getSignalStorageDir(), Environment.DIRECTORY_PICTURES);
public static @NonNull Uri getVideoUri() {
return MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
}
public static File getDownloadDir() throws NoExternalStorageException {
return new File(getSignalStorageDir(), Environment.DIRECTORY_DOWNLOADS);
public static @NonNull Uri getAudioUri() {
return MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
public static @NonNull Uri getImageUri() {
return MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
}
public static @NonNull Uri getDownloadUri() {
if (Build.VERSION.SDK_INT > 28) {
return MediaStore.Downloads.EXTERNAL_CONTENT_URI;
} else {
return getLegacyDownloadUri();
}
}
public static @NonNull Uri getLegacyDownloadUri() {
return Uri.fromFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS));
}
public static @Nullable String getCleanFileName(@Nullable String fileName) {