Attachment save task uses MediaStore API now.

This commit is contained in:
Anton Chekulaev 2020-09-10 16:32:13 +10:00
parent 4e909ff3b9
commit 7319ac4682
7 changed files with 299 additions and 304 deletions

View File

@ -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<MediaDatabase.MediaRecord> 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<Void, Void, List<SaveAttachmentTask.Attachment>>(context,
R.string.MediaOverviewActivity_collecting_attachments,
R.string.please_wait) {
@Override
protected List<SaveAttachmentTask.Attachment> doInBackground(Void... params) {
List<SaveAttachmentTask.Attachment> attachments = new LinkedList<>();
.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<Void, Void, List<SaveAttachmentTask.Attachment>>(
context,
R.string.MediaOverviewActivity_collecting_attachments,
R.string.please_wait) {
@Override
protected List<SaveAttachmentTask.Attachment> doInBackground(Void... params) {
List<SaveAttachmentTask.Attachment> attachments = new LinkedList<>();
for (MediaDatabase.MediaRecord mediaRecord : mediaRecords) {
if (mediaRecord.getAttachment().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<SaveAttachmentTask.Attachment> 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<SaveAttachmentTask.Attachment> 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());
}

View File

@ -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")

View File

@ -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<SaveAttachmentTask.Attachment> attachments = Stream.of(message.getSlideDeck().getSlides())
.filter(s -> s.getUri() != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()))
.map(s -> new SaveAttachmentTask.Attachment(s.getUri(), s.getContentType(), message.getDateReceived(), s.getFileName().orNull()))
.toList();
if (!Util.isEmpty(attachments)) {
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity());
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0]));
return;
}
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<SaveAttachmentTask.Attachment> attachments =
Stream.of(message.getSlideDeck().getSlides())
.filter(s -> s.getUri() != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()))
.map(s -> new SaveAttachmentTask.Attachment(s.getUri(), s.getContentType(), message.getDateReceived(), s.getFileName().orNull()))
.toList();
if (!Util.isEmpty(attachments)) {
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity());
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0]));
return;
}
Log.w(TAG, "No slide with attachable media found, failing nicely.");
Toast.makeText(getActivity(),
getResources().getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1),
Toast.LENGTH_LONG).show();
}
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();
});
}

View File

@ -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);

View File

@ -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<SaveAttachmentTask.Attachment, Void, Pair<Integer, String>> {
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<Context> 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<Integer, String> 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<Integer, String> 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();
}
}

View File

@ -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<SaveAttachmentTask.Attachment, Void, Pair<Int, String?>> {
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<Context>
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<Int, String?> {
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<Int, String?>) {
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?)
}

View File

@ -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<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
@ -13,14 +15,14 @@ public abstract class ProgressDialogAsyncTask<Params, Progress, Result> 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<Params, Progress, Result> extends
if (progress != null) progress.dismiss();
}
protected Context getContext() {
protected @NonNull Context getContext() {
return contextReference.get();
}
}