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();
}
}