mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-08 18:48:34 +00:00
Fix mediastore access for Android Q.
This commit is contained in:
parent
3163e09b98
commit
dc64a186d5
@ -36,7 +36,8 @@
|
|||||||
<uses-permission android:name="android.permission.WRITE_SMS"/>
|
<uses-permission android:name="android.permission.WRITE_SMS"/>
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
<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" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|
||||||
|
@ -70,6 +70,7 @@ import org.thoughtcrime.securesms.util.DateUtils;
|
|||||||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
||||||
|
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@ -384,21 +385,30 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
|
|
||||||
if (mediaItem != null) {
|
if (mediaItem != null) {
|
||||||
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
|
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
|
||||||
|
if (StorageUtil.canWriteToMediaStore()) {
|
||||||
|
performSavetoDisk(mediaItem);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
.ifNecessary()
|
.ifNecessary()
|
||||||
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
.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())
|
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
|
performSavetoDisk(mediaItem);
|
||||||
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
|
|
||||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
|
|
||||||
})
|
})
|
||||||
.execute();
|
.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")
|
@SuppressLint("StaticFieldLeak")
|
||||||
private void deleteMedia() {
|
private void deleteMedia() {
|
||||||
MediaItem mediaItem = getCurrentMediaItem();
|
MediaItem mediaItem = getCurrentMediaItem();
|
||||||
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components;
|
|||||||
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.ContentUris;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@ -106,7 +107,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
|
|||||||
public void onBindItemViewHolder(RecentPhotoViewHolder viewHolder, @NonNull Cursor cursor) {
|
public void onBindItemViewHolder(RecentPhotoViewHolder viewHolder, @NonNull Cursor cursor) {
|
||||||
viewHolder.imageView.setImageDrawable(null);
|
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 dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN));
|
||||||
long dateModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_MODIFIED));
|
long dateModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_MODIFIED));
|
||||||
String mimeType = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.MIME_TYPE));
|
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 width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation)));
|
||||||
int height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(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);
|
Key signature = new MediaStoreSignature(mimeType, dateModified, orientation);
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package org.thoughtcrime.securesms.conversation;
|
package org.thoughtcrime.securesms.conversation;
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@ -10,7 +8,6 @@ import android.widget.FrameLayout;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
@ -19,6 +16,7 @@ import org.thoughtcrime.securesms.R;
|
|||||||
import org.thoughtcrime.securesms.components.InputAwareLayout;
|
import org.thoughtcrime.securesms.components.InputAwareLayout;
|
||||||
import org.thoughtcrime.securesms.mediasend.Media;
|
import org.thoughtcrime.securesms.mediasend.Media;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
|
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -84,7 +82,7 @@ public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout.
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onMediaChanged(@NonNull List<Media> media) {
|
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);
|
mediaAdapter.setMedia(media);
|
||||||
permissionButton.setVisibility(GONE);
|
permissionButton.setVisibility(GONE);
|
||||||
permissionText.setVisibility(GONE);
|
permissionText.setVisibility(GONE);
|
||||||
|
@ -1034,6 +1034,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
.onAllGranted(() -> viewModel.onAttachmentKeyboardOpen())
|
.onAllGranted(() -> viewModel.onAttachmentKeyboardOpen())
|
||||||
|
.withPermanentDenialDialog(getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.thoughtcrime.securesms.conversation;
|
package org.thoughtcrime.securesms.conversation;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.ClipData;
|
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.OutgoingMediaMessage;
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.profiles.UnknownSenderView;
|
import org.thoughtcrime.securesms.profiles.UnknownSenderView;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
|
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.SaveAttachmentTask;
|
||||||
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
|
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
|
||||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||||
|
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
@ -854,26 +857,40 @@ public class ConversationFragment extends LoggingFragment {
|
|||||||
throw new AssertionError("Cannot save a view-once message.");
|
throw new AssertionError("Cannot save a view-once message.");
|
||||||
}
|
}
|
||||||
|
|
||||||
SaveAttachmentTask.showWarningDialog(getActivity(), new DialogInterface.OnClickListener() {
|
SaveAttachmentTask.showWarningDialog(getActivity(), (dialog, which) -> {
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
if (StorageUtil.canWriteToMediaStore()) {
|
||||||
List<SaveAttachmentTask.Attachment> attachments = Stream.of(message.getSlideDeck().getSlides())
|
performSave(message);
|
||||||
.filter(s -> s.getUri() != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()))
|
return;
|
||||||
.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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
private void clearHeaderIfNotTyping(ConversationAdapter adapter) {
|
||||||
if (adapter.getHeaderView() != typingView) {
|
if (adapter.getHeaderView() != typingView) {
|
||||||
adapter.setHeaderView(null);
|
adapter.setHeaderView(null);
|
||||||
|
@ -5,6 +5,7 @@ import android.Manifest;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import androidx.loader.content.CursorLoader;
|
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;
|
public static Uri BASE_URL = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||||
|
|
||||||
private static final String[] PROJECTION = new String[] {
|
private static final String[] PROJECTION = new String[] {
|
||||||
MediaStore.Images.ImageColumns.DATA,
|
MediaStore.Images.ImageColumns._ID,
|
||||||
MediaStore.Images.ImageColumns.DATE_TAKEN,
|
MediaStore.Images.ImageColumns.DATE_TAKEN,
|
||||||
MediaStore.Images.ImageColumns.DATE_MODIFIED,
|
MediaStore.Images.ImageColumns.DATE_MODIFIED,
|
||||||
MediaStore.Images.ImageColumns.ORIENTATION,
|
MediaStore.Images.ImageColumns.ORIENTATION,
|
||||||
@ -26,7 +27,8 @@ public class RecentPhotosLoader extends CursorLoader {
|
|||||||
MediaStore.Images.ImageColumns.HEIGHT
|
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;
|
private final Context context;
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.database.MediaDatabase;
|
|||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||||
|
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@ -32,43 +33,18 @@ final class MediaActions {
|
|||||||
{
|
{
|
||||||
Context context = fragment.requireContext();
|
Context context = fragment.requireContext();
|
||||||
|
|
||||||
|
if (StorageUtil.canWriteToMediaStore()) {
|
||||||
|
performSaveToDisk(context, mediaRecords, postExecute);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
SaveAttachmentTask.showWarningDialog(context, (dialogInterface, which) -> Permissions.with(fragment)
|
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()
|
.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))
|
.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())
|
.onAnyDenied(() -> Toast.makeText(context, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||||
.onAllGranted(() ->
|
.onAllGranted(() -> performSaveToDisk(context, mediaRecords, postExecute))
|
||||||
new ProgressDialogAsyncTask<Void, Void, List<SaveAttachmentTask.Attachment>>(context,
|
.execute(), mediaRecords.size());
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void handleDeleteMedia(@NonNull Context context,
|
static void handleDeleteMedia(@NonNull Context context,
|
||||||
@ -111,4 +87,37 @@ final class MediaActions {
|
|||||||
builder.setNegativeButton(android.R.string.cancel, null);
|
builder.setNegativeButton(android.R.string.cancel, null);
|
||||||
builder.show();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
package org.thoughtcrime.securesms.mediasend;
|
package org.thoughtcrime.securesms.mediasend;
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.ContentUris;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
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.Images;
|
||||||
import android.provider.MediaStore.Video;
|
import android.provider.MediaStore.Video;
|
||||||
import android.provider.OpenableColumns;
|
import android.provider.OpenableColumns;
|
||||||
@ -20,13 +21,12 @@ import com.annimon.stream.Stream;
|
|||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -84,7 +84,7 @@ public class MediaRepository {
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private @NonNull List<MediaFolder> getFolders(@NonNull Context context) {
|
private @NonNull List<MediaFolder> getFolders(@NonNull Context context) {
|
||||||
if (!Permissions.hasAll(context, Manifest.permission.READ_EXTERNAL_STORAGE)) {
|
if (!StorageUtil.canReadFromMediaStore()) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,20 +132,19 @@ public class MediaRepository {
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private @NonNull FolderResult getFolders(@NonNull Context context, @NonNull Uri contentUri) {
|
private @NonNull FolderResult getFolders(@NonNull Context context, @NonNull Uri contentUri) {
|
||||||
String cameraPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() + File.separator + "Camera";
|
|
||||||
String cameraBucketId = null;
|
String cameraBucketId = null;
|
||||||
Uri globalThumbnail = null;
|
Uri globalThumbnail = null;
|
||||||
long thumbnailTimestamp = 0;
|
long thumbnailTimestamp = 0;
|
||||||
Map<String, FolderData> folders = new HashMap<>();
|
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[] projection = new String[] { Images.Media._ID, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_MODIFIED };
|
||||||
String selection = Images.Media.DATA + " NOT NULL";
|
String selection = isNotPending();
|
||||||
String sortBy = Images.Media.BUCKET_DISPLAY_NAME + " COLLATE NOCASE ASC, " + Images.Media.DATE_MODIFIED + " DESC";
|
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)) {
|
try (Cursor cursor = context.getContentResolver().query(contentUri, projection, selection, null, sortBy)) {
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
String path = cursor.getString(cursor.getColumnIndexOrThrow(projection[0]));
|
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0]));
|
||||||
Uri thumbnail = Uri.fromFile(new File(path));
|
Uri thumbnail = ContentUris.withAppendedId(contentUri, rowId);
|
||||||
String bucketId = cursor.getString(cursor.getColumnIndexOrThrow(projection[1]));
|
String bucketId = cursor.getString(cursor.getColumnIndexOrThrow(projection[1]));
|
||||||
String title = cursor.getString(cursor.getColumnIndexOrThrow(projection[2]));
|
String title = cursor.getString(cursor.getColumnIndexOrThrow(projection[2]));
|
||||||
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(projection[3]));
|
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(projection[3]));
|
||||||
@ -154,7 +153,7 @@ public class MediaRepository {
|
|||||||
folder.incrementCount();
|
folder.incrementCount();
|
||||||
folders.put(bucketId, folder);
|
folders.put(bucketId, folder);
|
||||||
|
|
||||||
if (cameraBucketId == null && path.startsWith(cameraPath)) {
|
if (cameraBucketId == null && "Camera".equals(title)) {
|
||||||
cameraBucketId = bucketId;
|
cameraBucketId = bucketId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,7 +169,7 @@ public class MediaRepository {
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private @NonNull List<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId) {
|
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();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,27 +187,27 @@ public class MediaRepository {
|
|||||||
@WorkerThread
|
@WorkerThread
|
||||||
private @NonNull List<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean isImage) {
|
private @NonNull List<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean isImage) {
|
||||||
List<Media> media = new LinkedList<>();
|
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[] selectionArgs = new String[] { bucketId };
|
||||||
String sortBy = Images.Media.DATE_MODIFIED + " DESC";
|
String sortBy = Images.Media.DATE_MODIFIED + " DESC";
|
||||||
|
|
||||||
String[] projection;
|
String[] projection;
|
||||||
|
|
||||||
if (isImage) {
|
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 {
|
} 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)) {
|
if (Media.ALL_MEDIA_BUCKET_ID.equals(bucketId)) {
|
||||||
selection = Images.Media.DATA + " NOT NULL";
|
selection = isNotPending();
|
||||||
selectionArgs = null;
|
selectionArgs = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try (Cursor cursor = context.getContentResolver().query(contentUri, projection, selection, selectionArgs, sortBy)) {
|
try (Cursor cursor = context.getContentResolver().query(contentUri, projection, selection, selectionArgs, sortBy)) {
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
String path = cursor.getString(cursor.getColumnIndexOrThrow(projection[0]));
|
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0]));
|
||||||
Uri uri = Uri.fromFile(new File(path));
|
Uri uri = ContentUris.withAppendedId(contentUri, rowId);
|
||||||
String mimetype = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.MIME_TYPE));
|
String mimetype = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.MIME_TYPE));
|
||||||
long date = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.DATE_MODIFIED));
|
long date = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.DATE_MODIFIED));
|
||||||
int orientation = isImage ? cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) : 0;
|
int orientation = isImage ? cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) : 0;
|
||||||
@ -224,12 +223,12 @@ public class MediaRepository {
|
|||||||
return media;
|
return media;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private @NonNull String isNotPending() {
|
||||||
|
return Build.VERSION.SDK_INT <= 28 ? Images.Media.DATA + " NOT NULL" : MediaStore.MediaColumns.IS_PENDING + " != 1";
|
||||||
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private List<Media> getPopulatedMedia(@NonNull Context context, @NonNull List<Media> media) {
|
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 -> {
|
return Stream.of(media).map(m -> {
|
||||||
try {
|
try {
|
||||||
if (isPopulated(m)) {
|
if (isPopulated(m)) {
|
||||||
@ -265,10 +264,6 @@ public class MediaRepository {
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private Optional<Media> getMostRecentItem(@NonNull Context context) {
|
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);
|
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();
|
return media.size() > 0 ? Optional.of(media.get(0)) : Optional.absent();
|
||||||
}
|
}
|
||||||
|
@ -367,36 +367,21 @@ public class AttachmentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void selectDocument(Activity activity, int requestCode) {
|
public static void selectDocument(Activity activity, int requestCode) {
|
||||||
Permissions.with(activity)
|
selectMediaType(activity, "*/*", null, requestCode);
|
||||||
.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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull CharSequence body, @NonNull TransportOption transport) {
|
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull CharSequence body, @NonNull TransportOption transport) {
|
||||||
Permissions.with(activity)
|
Permissions.with(activity)
|
||||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
.ifNecessary()
|
.ifNecessary()
|
||||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
.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))
|
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body, transport), requestCode))
|
||||||
.execute();
|
.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) {
|
public static void selectContactInfo(Activity activity, int requestCode) {
|
||||||
Permissions.with(activity)
|
Permissions.with(activity)
|
||||||
.request(Manifest.permission.WRITE_CONTACTS)
|
.request(Manifest.permission.READ_CONTACTS)
|
||||||
.ifNecessary()
|
.ifNecessary()
|
||||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information))
|
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information))
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
@ -430,29 +415,6 @@ public class AttachmentManager {
|
|||||||
return captureUri;
|
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) {
|
private static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) {
|
||||||
final Intent intent = new Intent();
|
final Intent intent = new Intent();
|
||||||
intent.setType(type);
|
intent.setType(type);
|
||||||
|
@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker;
|
|||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
import org.thoughtcrime.securesms.util.ParcelUtil;
|
import org.thoughtcrime.securesms.util.ParcelUtil;
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||||
|
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
@ -412,29 +413,17 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
|||||||
@Override
|
@Override
|
||||||
public void onSave() {
|
public void onSave() {
|
||||||
SaveAttachmentTask.showWarningDialog(requireContext(), (dialogInterface, i) -> {
|
SaveAttachmentTask.showWarningDialog(requireContext(), (dialogInterface, i) -> {
|
||||||
|
if (StorageUtil.canWriteToMediaStore()) {
|
||||||
|
performSaveToDisk();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
.ifNecessary()
|
.ifNecessary()
|
||||||
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
.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())
|
.onAnyDenied(() -> Toast.makeText(requireContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(this::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);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.execute();
|
.execute();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -469,6 +458,25 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
|||||||
controller.onDoneEditing();
|
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() {
|
private void refreshUniqueColors() {
|
||||||
imageEditorHud.setColorPalette(imageEditorView.getModel().getUniqueColorsIgnoringAlpha());
|
imageEditorHud.setColorPalette(imageEditorView.getModel().getUniqueColorsIgnoringAlpha());
|
||||||
}
|
}
|
||||||
|
@ -244,12 +244,12 @@ public class BackupUtil {
|
|||||||
|
|
||||||
private final long timestamp;
|
private final long timestamp;
|
||||||
private final long size;
|
private final long size;
|
||||||
private final Uri uri;
|
private final Uri uri;
|
||||||
|
|
||||||
BackupInfo(long timestamp, long size, Uri uri) {
|
BackupInfo(long timestamp, long size, Uri uri) {
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
this.size = size;
|
this.size = size;
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getTimestamp() {
|
public long getTimestamp() {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.thoughtcrime.securesms.util;
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface.OnClickListener;
|
import android.content.DialogInterface.OnClickListener;
|
||||||
import android.media.MediaScannerConnection;
|
import android.media.MediaScannerConnection;
|
||||||
@ -7,6 +8,10 @@ import android.net.Uri;
|
|||||||
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.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
import android.provider.MediaStore;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
@ -59,7 +64,7 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
|
|||||||
Context context = contextReference.get();
|
Context context = contextReference.get();
|
||||||
String directory = null;
|
String directory = null;
|
||||||
|
|
||||||
if (!StorageUtil.canWriteInSignalStorageDir()) {
|
if (!StorageUtil.canWriteToMediaStore()) {
|
||||||
return new Pair<>(WRITE_ACCESS_FAILURE, null);
|
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);
|
if (attachments.length > 1) return new Pair<>(SUCCESS, null);
|
||||||
else return new Pair<>(SUCCESS, directory);
|
else return new Pair<>(SUCCESS, directory);
|
||||||
} catch (NoExternalStorageException|IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
Log.w(TAG, ioe);
|
Log.w(TAG, ioe);
|
||||||
return new Pair<>(FAILURE, null);
|
return new Pair<>(FAILURE, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable String saveAttachment(Context context, Attachment attachment)
|
private @Nullable String saveAttachment(Context context, Attachment attachment) throws IOException
|
||||||
throws NoExternalStorageException, IOException
|
|
||||||
{
|
{
|
||||||
String contentType = MediaUtil.getCorrectedMimeType(attachment.contentType);
|
String contentType = MediaUtil.getCorrectedMimeType(attachment.contentType);
|
||||||
String fileName = attachment.fileName;
|
String fileName = attachment.fileName;
|
||||||
@ -91,40 +95,46 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
|
|||||||
if (fileName == null) fileName = generateOutputFileName(contentType, attachment.date);
|
if (fileName == null) fileName = generateOutputFileName(contentType, attachment.date);
|
||||||
fileName = sanitizeOutputFileName(fileName);
|
fileName = sanitizeOutputFileName(fileName);
|
||||||
|
|
||||||
File outputDirectory = createOutputDirectoryFromContentType(contentType);
|
Uri outputUri = getMediaStoreContentUriForType(contentType);
|
||||||
File mediaFile = createOutputFile(outputDirectory, fileName);
|
Uri mediaUri = createOutputUri(outputUri, fileName);
|
||||||
InputStream inputStream = PartAuthority.getAttachmentStream(context, attachment.uri);
|
|
||||||
|
|
||||||
if (inputStream == null) {
|
try (InputStream inputStream = PartAuthority.getAttachmentStream(context, attachment.uri)) {
|
||||||
return null;
|
|
||||||
|
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);
|
if (Build.VERSION.SDK_INT > 28) {
|
||||||
Util.copy(inputStream, outputStream);
|
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()},
|
return outputUri.getLastPathSegment();
|
||||||
new String[]{contentType}, null);
|
|
||||||
|
|
||||||
return outputDirectory.getName();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private File createOutputDirectoryFromContentType(@NonNull String contentType)
|
private @NonNull Uri getMediaStoreContentUriForType(@NonNull String contentType) {
|
||||||
throws NoExternalStorageException
|
|
||||||
{
|
|
||||||
File outputDirectory;
|
|
||||||
|
|
||||||
if (contentType.startsWith("video/")) {
|
if (contentType.startsWith("video/")) {
|
||||||
outputDirectory = StorageUtil.getVideoDir();
|
return StorageUtil.getVideoUri();
|
||||||
} else if (contentType.startsWith("audio/")) {
|
} else if (contentType.startsWith("audio/")) {
|
||||||
outputDirectory = StorageUtil.getAudioDir();
|
return StorageUtil.getAudioUri();
|
||||||
} else if (contentType.startsWith("image/")) {
|
} else if (contentType.startsWith("image/")) {
|
||||||
outputDirectory = StorageUtil.getImageDir();
|
return StorageUtil.getImageUri();
|
||||||
} else {
|
} 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) {
|
private String generateOutputFileName(@NonNull String contentType, long timestamp) {
|
||||||
@ -142,25 +152,34 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
|
|||||||
return new File(fileName).getName();
|
return new File(fileName).getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
private File createOutputFile(@NonNull File outputDirectory, @NonNull String fileName)
|
private Uri createOutputUri(@NonNull Uri outputUri, @NonNull String fileName) throws IOException {
|
||||||
throws IOException
|
ContentValues contentValues = new ContentValues();
|
||||||
{
|
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
|
||||||
String[] fileParts = getFileNameParts(fileName);
|
|
||||||
String base = fileParts[0];
|
|
||||||
String extension = fileParts[1];
|
|
||||||
|
|
||||||
File outputFile = new File(outputDirectory, base + "." + extension);
|
if (Build.VERSION.SDK_INT > 28) {
|
||||||
|
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 1);
|
||||||
int i = 0;
|
|
||||||
while (outputFile.exists()) {
|
|
||||||
outputFile = new File(outputDirectory, base + "-" + (++i) + "." + extension);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outputFile.isHidden()) {
|
if (Build.VERSION.SDK_INT <= 28 && outputUri.equals(StorageUtil.getLegacyDownloadUri())) {
|
||||||
throw new IOException("Specified name would not be visible");
|
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) {
|
private String[] getFileNameParts(String fileName) {
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
package org.thoughtcrime.securesms.util;
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.BuildConfig;
|
import org.thoughtcrime.securesms.BuildConfig;
|
||||||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
@ -68,20 +75,37 @@ public class StorageUtil {
|
|||||||
return getSignalStorageDir();
|
return getSignalStorageDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File getVideoDir() throws NoExternalStorageException {
|
public static boolean canWriteToMediaStore() {
|
||||||
return new File(getSignalStorageDir(), Environment.DIRECTORY_MOVIES);
|
return Build.VERSION.SDK_INT > 28 ||
|
||||||
|
Permissions.hasAll(ApplicationDependencies.getApplication(), Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File getAudioDir() throws NoExternalStorageException {
|
public static boolean canReadFromMediaStore() {
|
||||||
return new File(getSignalStorageDir(), Environment.DIRECTORY_MUSIC);
|
return Permissions.hasAll(ApplicationDependencies.getApplication(), Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File getImageDir() throws NoExternalStorageException {
|
public static @NonNull Uri getVideoUri() {
|
||||||
return new File(getSignalStorageDir(), Environment.DIRECTORY_PICTURES);
|
return MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File getDownloadDir() throws NoExternalStorageException {
|
public static @NonNull Uri getAudioUri() {
|
||||||
return new File(getSignalStorageDir(), Environment.DIRECTORY_DOWNLOADS);
|
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) {
|
public static @Nullable String getCleanFileName(@Nullable String fileName) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user