diff --git a/res/drawable-hdpi/ic_folder_white_48dp.png b/res/drawable-hdpi/ic_folder_white_48dp.png deleted file mode 100644 index b93d5a1e4a..0000000000 Binary files a/res/drawable-hdpi/ic_folder_white_48dp.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_folder_white_48dp.png b/res/drawable-mdpi/ic_folder_white_48dp.png deleted file mode 100644 index 71a5a137c4..0000000000 Binary files a/res/drawable-mdpi/ic_folder_white_48dp.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_folder_white_48dp.png b/res/drawable-xhdpi/ic_folder_white_48dp.png deleted file mode 100644 index a1afbe9daf..0000000000 Binary files a/res/drawable-xhdpi/ic_folder_white_48dp.png and /dev/null differ diff --git a/res/drawable-xxhdpi/ic_folder_white_48dp.png b/res/drawable-xxhdpi/ic_folder_white_48dp.png deleted file mode 100644 index 0f95c75501..0000000000 Binary files a/res/drawable-xxhdpi/ic_folder_white_48dp.png and /dev/null differ diff --git a/res/drawable-xxxhdpi/ic_folder_white_48dp.png b/res/drawable-xxxhdpi/ic_folder_white_48dp.png deleted file mode 100644 index 862a359c65..0000000000 Binary files a/res/drawable-xxxhdpi/ic_folder_white_48dp.png and /dev/null differ diff --git a/res/drawable/ic_baseline_folder_24.xml b/res/drawable/ic_baseline_folder_24.xml new file mode 100644 index 0000000000..dc6b080235 --- /dev/null +++ b/res/drawable/ic_baseline_folder_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/res/layout/mediapicker_folder_item.xml b/res/layout/mediapicker_folder_item.xml index 9d1d729ebe..b52ace6f4a 100644 --- a/res/layout/mediapicker_folder_item.xml +++ b/res/layout/mediapicker_folder_item.xml @@ -33,7 +33,8 @@ android:layout_width="20dp" android:layout_height="20dp" android:layout_marginEnd="6dp" - android:src="@drawable/ic_folder_white_48dp"/> + android:tint="@android:color/white" + android:src="@drawable/ic_baseline_folder_24"/> Permissions.with(this) .request(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE) - .ifNecessary() .onAnyResult(this::startAvatarSelection) .execute()); diff --git a/src/org/thoughtcrime/securesms/DeviceActivity.java b/src/org/thoughtcrime/securesms/DeviceActivity.java index 5b68a6b478..c3c750c58b 100644 --- a/src/org/thoughtcrime/securesms/DeviceActivity.java +++ b/src/org/thoughtcrime/securesms/DeviceActivity.java @@ -98,7 +98,6 @@ public class DeviceActivity extends PassphraseRequiredActionBarActivity public void onClick(View v) { Permissions.with(this) .request(Manifest.permission.CAMERA) - .ifNecessary() .withPermanentDenialDialog(getString(R.string.DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code)) .onAllGranted(() -> { getSupportFragmentManager().beginTransaction() diff --git a/src/org/thoughtcrime/securesms/MediaOverviewActivity.java b/src/org/thoughtcrime/securesms/MediaOverviewActivity.java index 2b3f12012c..3bb2b6eb77 100644 --- a/src/org/thoughtcrime/securesms/MediaOverviewActivity.java +++ b/src/org/thoughtcrime/securesms/MediaOverviewActivity.java @@ -16,7 +16,6 @@ */ package org.thoughtcrime.securesms; -import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; @@ -324,47 +323,48 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity { } @SuppressWarnings("CodeBlock2Expr") - @SuppressLint({"InlinedApi","StaticFieldLeak"}) + @SuppressLint({"InlinedApi", "StaticFieldLeak"}) private void handleSaveMedia(@NonNull Collection mediaRecords) { final Context context = getContext(); + SaveAttachmentTask.showWarningDialog(context, (dialogInterface, which) -> { Permissions.with(this) - .request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_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(getContext(), 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<>(); + .request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + .maxSdkVersion(Build.VERSION_CODES.P) + .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(getContext(), 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().getDataUri() != null) { - attachments.add(new SaveAttachmentTask.Attachment(mediaRecord.getAttachment().getDataUri(), - mediaRecord.getContentType(), - mediaRecord.getDate(), - mediaRecord.getAttachment().getFileName())); - } - } + for (MediaDatabase.MediaRecord mediaRecord : mediaRecords) { + if (mediaRecord.getAttachment().getDataUri() != null) { + attachments.add(new SaveAttachmentTask.Attachment(mediaRecord.getAttachment().getDataUri(), + mediaRecord.getContentType(), + mediaRecord.getDate(), + mediaRecord.getAttachment().getFileName())); + } + } - return attachments; - } + 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[attachments.size()])); - actionMode.finish(); - } - }.execute(); - }) - .execute(); + @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[attachments.size()])); + actionMode.finish(); + } + }.execute(); + }) + .execute(); }, mediaRecords.size()); } diff --git a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java index 4593382bbe..429b39c00e 100644 --- a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -16,17 +16,16 @@ */ package org.thoughtcrime.securesms; -import android.Manifest; import android.annotation.SuppressLint; import android.annotation.TargetApi; -import androidx.appcompat.widget.Toolbar; import androidx.lifecycle.ViewModelProviders; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; +import android.os.Build; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Bundle; @@ -341,22 +340,23 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im @SuppressLint("InlinedApi") private void saveToDisk() { MediaItem mediaItem = getCurrentMediaItem(); + if (mediaItem == null) return; - if (mediaItem != null) { - SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> { - Permissions.with(this) - .request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_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)); - }) - .execute(); - }); - } + SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> { + Permissions.with(this) + .request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + .maxSdkVersion(Build.VERSION_CODES.P) + .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)); + }) + .execute(); + }); } @SuppressLint("StaticFieldLeak") diff --git a/src/org/thoughtcrime/securesms/RegistrationActivity.java b/src/org/thoughtcrime/securesms/RegistrationActivity.java index c96e433013..fdabe5d229 100644 --- a/src/org/thoughtcrime/securesms/RegistrationActivity.java +++ b/src/org/thoughtcrime/securesms/RegistrationActivity.java @@ -268,7 +268,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif @SuppressLint("StaticFieldLeak") private void initializeBackupDetection() { - if (!Permissions.hasAll(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + if (!Permissions.hasAll(this, Manifest.permission.READ_EXTERNAL_STORAGE)) { Log.i(TAG, "Skipping backup detection. We don't have the permission."); return; } diff --git a/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java b/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java index f78de0ac53..981a76ca50 100644 --- a/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java +++ b/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java @@ -172,7 +172,6 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity public void onClick(View v) { Permissions.with(this) .request(Manifest.permission.CAMERA) - .ifNecessary() .withPermanentDenialDialog(getString(R.string.VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied)) .onAllGranted(() -> { FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); diff --git a/src/org/thoughtcrime/securesms/WebRtcCallActivity.java b/src/org/thoughtcrime/securesms/WebRtcCallActivity.java index 311698578c..e17e4f631a 100644 --- a/src/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/src/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -164,7 +164,6 @@ public class WebRtcCallActivity extends Activity { if (event != null) { Permissions.with(this) .request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA) - .ifNecessary() .withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, event.getRecipient().toShortString()), R.drawable.ic_mic_white_48dp, R.drawable.ic_videocam_white_48dp) .withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls)) diff --git a/src/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java b/src/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java index 86dfa8b6ec..67d6405d3f 100644 --- a/src/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java +++ b/src/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java @@ -110,7 +110,7 @@ public class AttachmentTypeSelector extends PopupWindow { public void show(@NonNull Activity activity, final @NonNull View anchor) { updateHeight(); - if (Permissions.hasAll(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + if (Permissions.hasAll(activity, Manifest.permission.READ_EXTERNAL_STORAGE)) { recentRail.setVisibility(View.VISIBLE); loaderManager.restartLoader(1, null, recentRail); } else { diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 318a863313..8a98e52361 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -2564,7 +2564,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity public void onRecorderPermissionRequired() { Permissions.with(this) .request(Manifest.permission.RECORD_AUDIO) - .ifNecessary() .withRationaleDialog(getString(R.string.ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone), R.drawable.ic_baseline_mic_48) .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_requires_the_microphone_permission_in_order_to_send_audio_messages)) .execute(); @@ -2765,7 +2764,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity public void onClick(View v) { Permissions.with(ConversationActivity.this) .request(Manifest.permission.CAMERA) - .ifNecessary() .withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_baseline_photo_camera_48) .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video)) .onAllGranted(() -> { diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java index eeb1c3cc0a..8ee693adff 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -85,6 +85,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.recipients.Recipient; import org.thoughtcrime.securesms.sms.MessageSender; @@ -658,23 +659,30 @@ public class ConversationFragment extends Fragment } private void handleSaveAttachment(final MediaMmsMessageRecord 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; - } + SaveAttachmentTask.showWarningDialog(getActivity(), (dialog, which) -> { + Permissions.with(this) + .request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + .maxSdkVersion(Build.VERSION_CODES.P) + .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(getContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show()) + .onAllGranted(() -> { + 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(); - } + 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(); + }) + .execute(); }); } diff --git a/src/org/thoughtcrime/securesms/database/loaders/RecentPhotosLoader.java b/src/org/thoughtcrime/securesms/database/loaders/RecentPhotosLoader.java index 7ed0184e8e..21ed07ac66 100644 --- a/src/org/thoughtcrime/securesms/database/loaders/RecentPhotosLoader.java +++ b/src/org/thoughtcrime/securesms/database/loaders/RecentPhotosLoader.java @@ -35,7 +35,7 @@ public class RecentPhotosLoader extends CursorLoader { @Override public Cursor loadInBackground() { - if (Permissions.hasAll(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + if (Permissions.hasAll(context, Manifest.permission.READ_EXTERNAL_STORAGE)) { return context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, PROJECTION, null, null, MediaStore.Images.ImageColumns.DATE_MODIFIED + " DESC"); diff --git a/src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java b/src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java index 4110699e67..ba19d8e2a5 100644 --- a/src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java +++ b/src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java @@ -26,6 +26,7 @@ import java.util.Locale; import network.loki.messenger.R; +//TODO AC: Needs to be refactored to use Storage Access Framework or Media Store API. public class LocalBackupJob extends BaseJob { public static final String KEY = "LocalBackupJob"; diff --git a/src/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt index 2fcfb7d43d..7e84023c17 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt @@ -251,7 +251,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { // Ask for an optional camera permission. Permissions.with(this) .request(Manifest.permission.CAMERA) - .ifNecessary() .onAnyResult { tempFile = AvatarSelection.startAvatarSelection(this, false, true) } diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaFolder.java b/src/org/thoughtcrime/securesms/mediasend/MediaFolder.java index ab53c1204a..b84ebfd276 100644 --- a/src/org/thoughtcrime/securesms/mediasend/MediaFolder.java +++ b/src/org/thoughtcrime/securesms/mediasend/MediaFolder.java @@ -12,14 +12,12 @@ public class MediaFolder { private final String title; private final int itemCount; private final String bucketId; - private final FolderType folderType; - MediaFolder(@NonNull Uri thumbnailUri, @NonNull String title, int itemCount, @NonNull String bucketId, @NonNull FolderType folderType) { + MediaFolder(@NonNull Uri thumbnailUri, @NonNull String title, int itemCount, @NonNull String bucketId) { this.thumbnailUri = thumbnailUri; this.title = title; this.itemCount = itemCount; this.bucketId = bucketId; - this.folderType = folderType; } Uri getThumbnailUri() { @@ -38,10 +36,6 @@ public class MediaFolder { return bucketId; } - FolderType getFolderType() { - return folderType; - } - enum FolderType { NORMAL, CAMERA } diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaPickerFolderAdapter.java b/src/org/thoughtcrime/securesms/mediasend/MediaPickerFolderAdapter.java index de7bb04607..adaf54d00b 100644 --- a/src/org/thoughtcrime/securesms/mediasend/MediaPickerFolderAdapter.java +++ b/src/org/thoughtcrime/securesms/mediasend/MediaPickerFolderAdapter.java @@ -75,7 +75,6 @@ class MediaPickerFolderAdapter extends RecyclerView.Adapter - * The functionality of this class should be refactored to use - * MediaStore. */ class MediaRepository { @@ -82,30 +76,17 @@ class MediaRepository { } } - String cameraBucketId = imageFolders.getCameraBucketId() != null ? imageFolders.getCameraBucketId() : videoFolders.getCameraBucketId(); - FolderData cameraFolder = cameraBucketId != null ? folders.remove(cameraBucketId) : null; - List mediaFolders = Stream.of(folders.values()).map(folder -> new MediaFolder(folder.getThumbnail(), + List mediaFolders = Stream.of(folders.values()).map(folder -> new MediaFolder(folder.getThumbnail(), folder.getTitle(), folder.getCount(), - folder.getBucketId(), - MediaFolder.FolderType.NORMAL)) + folder.getBucketId())) .sorted((o1, o2) -> o1.getTitle().toLowerCase().compareTo(o2.getTitle().toLowerCase())) .toList(); Uri allMediaThumbnail = imageFolders.getThumbnailTimestamp() > videoFolders.getThumbnailTimestamp() ? imageFolders.getThumbnail() : videoFolders.getThumbnail(); - if (allMediaThumbnail != null) { int allMediaCount = Stream.of(mediaFolders).reduce(0, (count, folder) -> count + folder.getItemCount()); - - if (cameraFolder != null) { - allMediaCount += cameraFolder.getCount(); - } - - mediaFolders.add(0, new MediaFolder(allMediaThumbnail, context.getString(R.string.MediaRepository_all_media), allMediaCount, Media.ALL_MEDIA_BUCKET_ID, MediaFolder.FolderType.NORMAL)); - } - - if (cameraFolder != null) { - mediaFolders.add(0, new MediaFolder(cameraFolder.getThumbnail(), cameraFolder.getTitle(), cameraFolder.getCount(), cameraFolder.getBucketId(), MediaFolder.FolderType.CAMERA)); + mediaFolders.add(0, new MediaFolder(allMediaThumbnail, context.getString(R.string.MediaRepository_all_media), allMediaCount, Media.ALL_MEDIA_BUCKET_ID)); } return mediaFolders; @@ -113,8 +94,6 @@ 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<>(); @@ -135,10 +114,6 @@ class MediaRepository { folder.incrementCount(); folders.put(bucketId, folder); - if (cameraBucketId == null && path.startsWith(cameraPath)) { - cameraBucketId = bucketId; - } - if (timestamp > thumbnailTimestamp) { globalThumbnail = thumbnail; thumbnailTimestamp = timestamp; @@ -146,7 +121,7 @@ class MediaRepository { } } - return new FolderResult(cameraBucketId, globalThumbnail, thumbnailTimestamp, folders); + return new FolderResult(globalThumbnail, thumbnailTimestamp, folders); } @WorkerThread @@ -163,7 +138,8 @@ class MediaRepository { } @WorkerThread - private @NonNull List getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean hasOrienation) { + private @NonNull List getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean hasOrientation) { + //TODO Constrain media file size to match the Loki protocol limit. List media = new LinkedList<>(); String selection = Images.Media.BUCKET_ID + " = ? AND " + Images.Media.DATA + " NOT NULL"; String[] selectionArgs = new String[] { bucketId }; @@ -171,7 +147,7 @@ class MediaRepository { String[] projection; - if (hasOrienation) { + if (hasOrientation) { projection = new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.ORIENTATION, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE}; } else { projection = new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE}; @@ -187,7 +163,7 @@ class MediaRepository { Uri uri = Uri.withAppendedPath(contentUri, cursor.getString(cursor.getColumnIndexOrThrow(Images.Media._ID))); String mimetype = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.MIME_TYPE)); long dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.DATE_TAKEN)); - int orientation = hasOrienation ? cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) : 0; + int orientation = hasOrientation ? cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) : 0; int width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation))); int height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation))); long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE)); @@ -284,26 +260,19 @@ class MediaRepository { } private static class FolderResult { - private final String cameraBucketId; private final Uri thumbnail; private final long thumbnailTimestamp; private final Map folderData; - private FolderResult(@Nullable String cameraBucketId, - @Nullable Uri thumbnail, + private FolderResult(@Nullable Uri thumbnail, long thumbnailTimestamp, @NonNull Map folderData) { - this.cameraBucketId = cameraBucketId; this.thumbnail = thumbnail; this.thumbnailTimestamp = thumbnailTimestamp; this.folderData = folderData; } - @Nullable String getCameraBucketId() { - return cameraBucketId; - } - @Nullable Uri getThumbnail() { return thumbnail; } diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java index 41324d4e61..ff1792ec1a 100644 --- a/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java +++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java @@ -1,6 +1,8 @@ package org.thoughtcrime.securesms.mediasend; import android.Manifest; + +import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProviders; import android.content.Context; import android.content.Intent; @@ -133,7 +135,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple countButtonText = findViewById(R.id.mediasend_count_button_text); cameraButton = findViewById(R.id.mediasend_camera_button); - viewModel = ViewModelProviders.of(this, new MediaSendViewModel.Factory(getApplication(), new MediaRepository())).get(MediaSendViewModel.class); + viewModel = new ViewModelProvider(this, new MediaSendViewModel.Factory(getApplication(), new MediaRepository())).get(MediaSendViewModel.class); recipient = Recipient.from(this, Address.fromSerialized(getIntent().getStringExtra(KEY_ADDRESS)), true); transport = getIntent().getParcelableExtra(KEY_TRANSPORT); @@ -375,7 +377,6 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple private void navigateToCamera() { Permissions.with(this) .request(Manifest.permission.CAMERA) - .ifNecessary() .withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_baseline_photo_camera_48) .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video)) .onAllGranted(() -> { diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java index 53e4ca54cc..16c0d44ed0 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -371,37 +371,25 @@ 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 String body, @NonNull TransportOption transport) { Permissions.with(activity) - .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) - .ifNecessary() + .request(Manifest.permission.READ_EXTERNAL_STORAGE) .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(() -> 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(); + selectMediaType(activity, "audio/*", null, requestCode); } public static void selectContactInfo(Activity activity, int requestCode) { Permissions.with(activity) .request(Manifest.permission.WRITE_CONTACTS) - .ifNecessary() .withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information)) .onAllGranted(() -> { Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI); @@ -414,7 +402,6 @@ public class AttachmentManager { /* Loki - Enable again once we have location sharing Permissions.with(activity) .request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) - .ifNecessary() .withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location)) .onAllGranted(() -> { try { @@ -444,7 +431,6 @@ public class AttachmentManager { 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 { @@ -469,6 +455,7 @@ public class AttachmentManager { } private static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) { + //TODO Constrain media file size to match the Loki protocol limit. final Intent intent = new Intent(); intent.setType(type); diff --git a/src/org/thoughtcrime/securesms/permissions/Permissions.java b/src/org/thoughtcrime/securesms/permissions/Permissions.java index f647da6db4..41cce6f8ef 100644 --- a/src/org/thoughtcrime/securesms/permissions/Permissions.java +++ b/src/org/thoughtcrime/securesms/permissions/Permissions.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.permissions; - import android.app.Activity; import android.app.AlertDialog; import android.content.Context; @@ -63,9 +62,8 @@ public class Permissions { private @DrawableRes int[] rationalDialogHeader; private String rationaleDialogMessage; - private boolean ifNecesary; - - private boolean condition = true; + private int minSdkVersion = 0; + private int maxSdkVersion = Integer.MAX_VALUE; PermissionsBuilder(PermissionObject permissionObject) { this.permissionObject = permissionObject; @@ -76,17 +74,6 @@ public class Permissions { return this; } - public PermissionsBuilder ifNecessary() { - this.ifNecesary = true; - return this; - } - - public PermissionsBuilder ifNecessary(boolean condition) { - this.ifNecesary = true; - this.condition = condition; - return this; - } - public PermissionsBuilder withRationaleDialog(@NonNull String message, @NonNull @DrawableRes int... headers) { this.rationalDialogHeader = headers; this.rationaleDialogMessage = message; @@ -133,11 +120,29 @@ public class Permissions { return this; } + /** + * Min Android SDK version to request the permissions for (inclusive). + */ + public PermissionsBuilder minSdkVersion(int minSdkVersion) { + this.minSdkVersion = minSdkVersion; + return this; + } + + /** + * Max Android SDK version to request the permissions for (inclusive). + */ + public PermissionsBuilder maxSdkVersion(int maxSdkVersion) { + this.maxSdkVersion = maxSdkVersion; + return this; + } + public void execute() { PermissionsRequest request = new PermissionsRequest(allGrantedListener, anyDeniedListener, anyPermanentlyDeniedListener, anyResultListener, someGrantedListener, someDeniedListener, somePermanentlyDeniedListener); - if (ifNecesary && (permissionObject.hasAll(requestedPermissions) || !condition)) { + boolean targetSdk = Build.VERSION.SDK_INT >= minSdkVersion && Build.VERSION.SDK_INT <= maxSdkVersion; + + if (!targetSdk || permissionObject.hasAll(requestedPermissions)) { executePreGrantedPermissionsRequest(request); } else if (rationaleDialogMessage != null && rationalDialogHeader != null) { executePermissionsRequestWithRationale(request); diff --git a/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java index 877da0506d..97c14b4279 100644 --- a/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java @@ -139,7 +139,6 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment { public boolean onPreferenceClick(Preference preference) { Permissions.with(ChatsPreferenceFragment.this) .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) - .ifNecessary() .onAllGranted(() -> { if (!((SwitchPreferenceCompat)preference).isChecked()) { BackupDialog.showEnableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference); @@ -160,7 +159,6 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment { public boolean onPreferenceClick(Preference preference) { Permissions.with(ChatsPreferenceFragment.this) .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) - .ifNecessary() .onAllGranted(() -> { Log.i(TAG, "Queing backup..."); ApplicationContext.getInstance(getContext()) diff --git a/src/org/thoughtcrime/securesms/registration/WelcomeActivity.java b/src/org/thoughtcrime/securesms/registration/WelcomeActivity.java index e9f8f2e026..d8d7809606 100644 --- a/src/org/thoughtcrime/securesms/registration/WelcomeActivity.java +++ b/src/org/thoughtcrime/securesms/registration/WelcomeActivity.java @@ -55,7 +55,6 @@ public class WelcomeActivity extends BaseActionBarActivity { private void onContinueClicked() { Permissions.with(this) .request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE) - .ifNecessary() .withRationaleDialog(getString(R.string.activity_landing_permission_dialog_message), R.drawable.ic_baseline_folder_48) .onAnyResult(() -> { Intent nextIntent = getIntent().getParcelableExtra("next_intent"); diff --git a/src/org/thoughtcrime/securesms/util/BackupUtil.java b/src/org/thoughtcrime/securesms/util/BackupUtil.java index d60660da4f..5df025a3bc 100644 --- a/src/org/thoughtcrime/securesms/util/BackupUtil.java +++ b/src/org/thoughtcrime/securesms/util/BackupUtil.java @@ -16,6 +16,7 @@ import java.util.Arrays; import java.util.Calendar; import java.util.Locale; +//TODO AC: Needs to be refactored to use Storage Access Framework or Media Store API. public class BackupUtil { private static final String TAG = BackupUtil.class.getSimpleName(); diff --git a/src/org/thoughtcrime/securesms/util/CommunicationActions.java b/src/org/thoughtcrime/securesms/util/CommunicationActions.java index 2c06f714b5..db66b955cf 100644 --- a/src/org/thoughtcrime/securesms/util/CommunicationActions.java +++ b/src/org/thoughtcrime/securesms/util/CommunicationActions.java @@ -35,8 +35,7 @@ public class CommunicationActions { Permissions.with(activity) .request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA) - .ifNecessary() - .withRationaleDialog(activity.getString(R.string.ConversationActivity_to_call_s_signal_needs_access_to_your_microphone_and_camera, recipient.toShortString()), + .withRationaleDialog(activity.getString(R.string.ConversationActivity_to_call_s_signal_needs_access_to_your_microphone_and_camera), R.drawable.ic_mic_white_48dp, R.drawable.ic_videocam_white_48dp) .withPermanentDenialDialog(activity.getString(R.string.ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s, recipient.toShortString())) diff --git a/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java deleted file mode 100644 index c4daadf36e..0000000000 --- a/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java +++ /dev/null @@ -1,234 +0,0 @@ -package org.thoughtcrime.securesms.util; - -import android.content.Context; -import android.content.DialogInterface.OnClickListener; -import android.media.MediaScannerConnection; -import android.net.Uri; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import android.text.TextUtils; -import android.webkit.MimeTypeMap; -import android.widget.Toast; - -import network.loki.messenger.R; -import org.thoughtcrime.securesms.database.NoExternalStorageException; -import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; -import org.whispersystems.libsignal.util.Pair; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.ref.WeakReference; -import java.text.SimpleDateFormat; - -public class SaveAttachmentTask extends ProgressDialogAsyncTask> { - private static final String TAG = SaveAttachmentTask.class.getSimpleName(); - - static final int SUCCESS = 0; - private static final int FAILURE = 1; - private static final int WRITE_ACCESS_FAILURE = 2; - - private final WeakReference contextReference; - - private final int attachmentCount; - - public SaveAttachmentTask(Context context) { - this(context, 1); - } - - public SaveAttachmentTask(Context context, int count) { - super(context, - context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_attachments, count, count), - context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_attachments_to_sd_card, count, count)); - this.contextReference = new WeakReference<>(context); - this.attachmentCount = count; - } - - @Override - protected Pair doInBackground(SaveAttachmentTask.Attachment... attachments) { - if (attachments == null || attachments.length == 0) { - throw new AssertionError("must pass in at least one attachment"); - } - - try { - Context context = contextReference.get(); - String directory = null; - - if (context == null) { - return new Pair<>(FAILURE, null); - } - - for (Attachment attachment : attachments) { - if (attachment != null) { - directory = saveAttachment(context, attachment); - if (directory == null) return new Pair<>(FAILURE, null); - } - } - - if (attachments.length > 1) return new Pair<>(SUCCESS, null); - else return new Pair<>(SUCCESS, directory); - } catch (NoExternalStorageException|IOException ioe) { - Log.w(TAG, ioe); - return new Pair<>(FAILURE, null); - } - } - - private @Nullable String saveAttachment(Context context, Attachment attachment) - throws NoExternalStorageException, IOException - { - String contentType = MediaUtil.getCorrectedMimeType(attachment.contentType); - String fileName = attachment.fileName; - - if (fileName == null) fileName = generateOutputFileName(contentType, attachment.date); - fileName = sanitizeOutputFileName(fileName); - - File outputDirectory = createOutputDirectoryFromContentType(contentType); - File mediaFile = createOutputFile(outputDirectory, fileName); - InputStream inputStream = PartAuthority.getAttachmentStream(context, attachment.uri); - - if (inputStream == null) { - return null; - } - - OutputStream outputStream = new FileOutputStream(mediaFile); - Util.copy(inputStream, outputStream); - - MediaScannerConnection.scanFile(context, new String[]{mediaFile.getAbsolutePath()}, - new String[]{contentType}, null); - - return outputDirectory.getName(); - } - - private File createOutputDirectoryFromContentType(@NonNull String contentType) - throws NoExternalStorageException - { - File outputDirectory; - - if (contentType.startsWith("video/")) { - outputDirectory = ExternalStorageUtil.getVideoDir(getContext()); - } else if (contentType.startsWith("audio/")) { - outputDirectory = ExternalStorageUtil.getAudioDir(getContext()); - } else if (contentType.startsWith("image/")) { - outputDirectory = ExternalStorageUtil.getImageDir(getContext()); - } else { - outputDirectory = ExternalStorageUtil.getDownloadDir(getContext()); - } - - if (!outputDirectory.mkdirs()) Log.w(TAG, "mkdirs() returned false, attempting to continue"); - return outputDirectory; - } - - private String generateOutputFileName(@NonNull String contentType, long timestamp) { - MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); - String extension = mimeTypeMap.getExtensionFromMimeType(contentType); - SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd-HHmmss"); - String base = "signal-" + dateFormatter.format(timestamp); - - if (extension == null) extension = "attach"; - - return base + "." + extension; - } - - private String sanitizeOutputFileName(@NonNull String fileName) { - return new File(fileName).getName(); - } - - private File createOutputFile(@NonNull File outputDirectory, @NonNull String fileName) - throws IOException - { - String[] fileParts = getFileNameParts(fileName); - String base = fileParts[0]; - String extension = fileParts[1]; - - 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 outputFile; - } - - private String[] getFileNameParts(String fileName) { - String[] result = new String[2]; - String[] tokens = fileName.split("\\.(?=[^\\.]+$)"); - - result[0] = tokens[0]; - - if (tokens.length > 1) result[1] = tokens[1]; - else result[1] = ""; - - return result; - } - - @Override - protected void onPostExecute(final Pair result) { - super.onPostExecute(result); - final Context context = contextReference.get(); - if (context == null) return; - - switch (result.first()) { - case FAILURE: - Toast.makeText(context, - context.getResources().getQuantityText(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, - attachmentCount), - Toast.LENGTH_LONG).show(); - break; - case SUCCESS: - String message = !TextUtils.isEmpty(result.second()) ? context.getResources().getString(R.string.SaveAttachmentTask_saved_to, result.second()) - : context.getResources().getString(R.string.SaveAttachmentTask_saved); - Toast.makeText(context, message, Toast.LENGTH_LONG).show(); - break; - case WRITE_ACCESS_FAILURE: - Toast.makeText(context, R.string.ConversationFragment_unable_to_write_to_sd_card_exclamation, - Toast.LENGTH_LONG).show(); - break; - } - } - - public static class Attachment { - public Uri uri; - public String fileName; - public String contentType; - public long date; - - public Attachment(@NonNull Uri uri, @NonNull String contentType, - long date, @Nullable String fileName) - { - if (uri == null || contentType == null || date < 0) { - throw new AssertionError("uri, content type, and date must all be specified"); - } - this.uri = uri; - this.fileName = fileName; - this.contentType = contentType; - this.date = date; - } - } - - public static void showWarningDialog(Context context, OnClickListener onAcceptListener) { - showWarningDialog(context, onAcceptListener, 1); - } - - public static void showWarningDialog(Context context, OnClickListener onAcceptListener, int count) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.ConversationFragment_save_to_sd_card); - builder.setIconAttribute(R.attr.dialog_alert_icon); - builder.setCancelable(true); - builder.setMessage(context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_media_to_storage_warning, - count, count)); - builder.setPositiveButton(R.string.yes, onAcceptListener); - builder.setNegativeButton(R.string.no, null); - builder.show(); - } -} - diff --git a/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt new file mode 100644 index 0000000000..6e4938dd8c --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt @@ -0,0 +1,196 @@ +package org.thoughtcrime.securesms.util + +import android.content.ContentValues +import android.content.Context +import android.content.DialogInterface.OnClickListener +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import android.text.TextUtils +import android.webkit.MimeTypeMap +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import network.loki.messenger.R +import org.thoughtcrime.securesms.logging.Log +import org.thoughtcrime.securesms.mms.PartAuthority +import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask +import java.io.File +import java.io.IOException +import java.lang.ref.WeakReference +import java.text.SimpleDateFormat + +/** + * Saves attachment files to an external storage using [MediaStore] API. + */ +class SaveAttachmentTask : ProgressDialogAsyncTask> { + + companion object { + @JvmStatic + private val TAG = SaveAttachmentTask::class.simpleName + + private const val RESULT_SUCCESS = 0 + private const val RESULT_FAILURE = 1 + + @JvmStatic + @JvmOverloads + fun showWarningDialog(context: Context, onAcceptListener: OnClickListener, count: Int = 1) { + val builder = AlertDialog.Builder(context) + builder.setTitle(R.string.ConversationFragment_save_to_sd_card) + builder.setIconAttribute(R.attr.dialog_alert_icon) + builder.setCancelable(true) + builder.setMessage(context.resources.getQuantityString( + R.plurals.ConversationFragment_saving_n_media_to_storage_warning, + count, + count)) + builder.setPositiveButton(R.string.yes, onAcceptListener) + builder.setNegativeButton(R.string.no, null) + builder.show() + } + } + + private val contextReference: WeakReference + private val attachmentCount: Int + + @JvmOverloads + constructor(context: Context, count: Int = 1): super(context, + context.resources.getQuantityString(R.plurals.ConversationFragment_saving_n_attachments, count, count), + context.resources.getQuantityString(R.plurals.ConversationFragment_saving_n_attachments_to_sd_card, count, count)) { + this.contextReference = WeakReference(context) + this.attachmentCount = count + } + + override fun doInBackground(vararg attachments: Attachment?): Pair { + if (attachments.isEmpty()) { + throw IllegalArgumentException("Must pass in at least one attachment") + } + + try { + val context = contextReference.get() + var directory: String? = null + + if (context == null) { + return Pair(RESULT_FAILURE, null) + } + + for (attachment in attachments) { + if (attachment != null) { + directory = saveAttachment(context, attachment) + if (directory == null) return Pair(RESULT_FAILURE, null) + } + } + + return if (attachments.size > 1) + Pair(RESULT_SUCCESS, null) + else + Pair(RESULT_SUCCESS, directory) + } catch (e: IOException) { + Log.w(TAG, e) + return Pair(RESULT_FAILURE, null) + } + } + + @Throws(IOException::class) + private fun saveAttachment(context: Context, attachment: Attachment): String? { + val resolver = context.contentResolver + + val contentType = MediaUtil.getCorrectedMimeType(attachment.contentType)!! + val fileName = attachment.fileName + ?: sanitizeOutputFileName(generateOutputFileName(contentType, attachment.date)) + + val mediaRecord = ContentValues() + val mediaVolume = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + MediaStore.VOLUME_EXTERNAL + } else { + MediaStore.VOLUME_EXTERNAL_PRIMARY + } + val collectionUri: Uri + + when { + contentType.startsWith("video/") -> { + collectionUri = MediaStore.Video.Media.getContentUri(mediaVolume) + mediaRecord.put(MediaStore.Video.Media.DISPLAY_NAME, fileName) + mediaRecord.put(MediaStore.Video.Media.MIME_TYPE, contentType) + // Add the date meta data to ensure the image is added at the front of the gallery + mediaRecord.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis()) + mediaRecord.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis()) + + } + contentType.startsWith("audio/") -> { + collectionUri = MediaStore.Audio.Media.getContentUri(mediaVolume) + mediaRecord.put(MediaStore.Audio.Media.DISPLAY_NAME, fileName) + mediaRecord.put(MediaStore.Audio.Media.MIME_TYPE, contentType) + mediaRecord.put(MediaStore.Audio.Media.DATE_ADDED, System.currentTimeMillis()) + mediaRecord.put(MediaStore.Audio.Media.DATE_TAKEN, System.currentTimeMillis()) + + } + contentType.startsWith("image/") -> { + collectionUri = MediaStore.Images.Media.getContentUri(mediaVolume) + mediaRecord.put(MediaStore.Images.Media.TITLE, fileName) + mediaRecord.put(MediaStore.Images.Media.DISPLAY_NAME, fileName) + mediaRecord.put(MediaStore.Images.Media.MIME_TYPE, contentType) + mediaRecord.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis()) + mediaRecord.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) + + } + else -> { + mediaRecord.put(MediaStore.Files.FileColumns.DISPLAY_NAME, fileName) + collectionUri = MediaStore.Files.getContentUri(mediaVolume) + } + } + + val mediaFileUri = resolver.insert(collectionUri, mediaRecord) + if (mediaFileUri == null) return null + + val inputStream = PartAuthority.getAttachmentStream(context, attachment.uri) + if (inputStream == null) return null + + inputStream.use { + resolver.openOutputStream(mediaFileUri).use { + Util.copy(inputStream, it) + } + } + + return mediaFileUri.toString() + } + + private fun generateOutputFileName(contentType: String, timestamp: Long): String { + val mimeTypeMap = MimeTypeMap.getSingleton() + val extension = mimeTypeMap.getExtensionFromMimeType(contentType) ?: "attach" + val dateFormatter = SimpleDateFormat("yyyy-MM-dd-HHmmss") + val base = "signal-${dateFormatter.format(timestamp)}" + + return "${base}.${extension}"; + } + + private fun sanitizeOutputFileName(fileName: String): String { + return File(fileName).name + } + + override fun onPostExecute(result: Pair) { + super.onPostExecute(result) + val context = contextReference.get() + if (context == null) return + + when (result.first) { + RESULT_FAILURE -> { + val message = context.resources.getQuantityText( + R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, + attachmentCount) + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + } + + RESULT_SUCCESS -> { + val message = if (!TextUtils.isEmpty(result.second)) { + context.resources.getString(R.string.SaveAttachmentTask_saved_to, result.second) + } else { + context.resources.getString(R.string.SaveAttachmentTask_saved) + } + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + } + + else -> throw IllegalStateException("Unexpected result value: " + result.first) + } + } + + data class Attachment(val uri: Uri, val contentType: String, val date: Long, val fileName: String?) +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/util/task/ProgressDialogAsyncTask.java b/src/org/thoughtcrime/securesms/util/task/ProgressDialogAsyncTask.java index e862d5d4f5..4c343bcf00 100644 --- a/src/org/thoughtcrime/securesms/util/task/ProgressDialogAsyncTask.java +++ b/src/org/thoughtcrime/securesms/util/task/ProgressDialogAsyncTask.java @@ -4,6 +4,8 @@ import android.app.ProgressDialog; import android.content.Context; import android.os.AsyncTask; +import androidx.annotation.NonNull; + import java.lang.ref.WeakReference; public abstract class ProgressDialogAsyncTask extends AsyncTask { @@ -13,14 +15,14 @@ public abstract class ProgressDialogAsyncTask extends private final String title; private final String message; - public ProgressDialogAsyncTask(Context context, String title, String message) { + public ProgressDialogAsyncTask(@NonNull Context context, @NonNull String title, @NonNull String message) { super(); this.contextReference = new WeakReference<>(context); this.title = title; this.message = message; } - public ProgressDialogAsyncTask(Context context, int title, int message) { + public ProgressDialogAsyncTask(@NonNull Context context, int title, int message) { this(context, context.getString(title), context.getString(message)); } @@ -35,7 +37,7 @@ public abstract class ProgressDialogAsyncTask extends if (progress != null) progress.dismiss(); } - protected Context getContext() { + protected @NonNull Context getContext() { return contextReference.get(); } }