diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2ef7d3e906..62ba85dab3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -36,7 +36,8 @@
-
+
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java
index 3e19317a39..294ce5437d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java
@@ -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();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java b/app/src/main/java/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java
index f9d67bac84..b4eff84f51 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java
@@ -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);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java
index 71c506d417..836c04bec3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java
@@ -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) {
- if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
+ if (StorageUtil.canReadFromMediaStore()) {
mediaAdapter.setMedia(media);
permissionButton.setVisibility(GONE);
permissionText.setVisibility(GONE);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
index 65f6396c51..0d5a24169c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
@@ -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();
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
index 4939f3a2f3..5ac355938e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
@@ -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 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 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);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/RecentPhotosLoader.java b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/RecentPhotosLoader.java
index 3c24a0ae11..0dd5c48f1c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/RecentPhotosLoader.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/RecentPhotosLoader.java
@@ -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;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaActions.java b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaActions.java
index c32cada086..1cb4460c01 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaActions.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaActions.java
@@ -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>(context,
- R.string.MediaOverviewActivity_collecting_attachments,
- R.string.please_wait)
- {
- @Override
- protected List doInBackground(Void... params) {
- List 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 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 mediaRecords, @Nullable Runnable postExecute) {
+ new ProgressDialogAsyncTask>(context,
+ R.string.MediaOverviewActivity_collecting_attachments,
+ R.string.please_wait)
+ {
+ @Override
+ protected List doInBackground(Void... params) {
+ List 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 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();
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java
index 5c05d5e406..867fb0434c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java
@@ -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 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 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 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 getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean isImage) {
List 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 getPopulatedMedia(@NonNull Context context, @NonNull List 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 getMostRecentItem(@NonNull Context context) {
- if (!Permissions.hasAll(context, Manifest.permission.READ_EXTERNAL_STORAGE)) {
- return Optional.absent();
- }
-
List 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();
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java
index 5e7eb8e101..10989189b3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java
@@ -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);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java
index 259955a1d1..e6bcc3d0da 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java
@@ -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());
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.java
index 5eed8f7eb9..5863ad6b33 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.java
@@ -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() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.java b/app/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.java
index e44280e700..101b4dd39d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.java
@@ -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(WRITE_ACCESS_FAILURE, null);
}
@@ -76,14 +81,13 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask 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 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 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) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java
index 77c7c2859b..a84457401f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java
@@ -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) {