diff --git a/src/org/thoughtcrime/securesms/MediaOverviewActivity.java b/src/org/thoughtcrime/securesms/MediaOverviewActivity.java index 54b80a60c1..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,46 +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) - .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 36117ea86a..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,21 +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) - .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/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/permissions/Permissions.java b/src/org/thoughtcrime/securesms/permissions/Permissions.java index 91eec1e011..41cce6f8ef 100644 --- a/src/org/thoughtcrime/securesms/permissions/Permissions.java +++ b/src/org/thoughtcrime/securesms/permissions/Permissions.java @@ -62,6 +62,9 @@ public class Permissions { private @DrawableRes int[] rationalDialogHeader; private String rationaleDialogMessage; + private int minSdkVersion = 0; + private int maxSdkVersion = Integer.MAX_VALUE; + PermissionsBuilder(PermissionObject permissionObject) { this.permissionObject = permissionObject; } @@ -117,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 (permissionObject.hasAll(requestedPermissions)) { + 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/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(); } }