mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-24 10:35:19 +00:00
Attachment save task uses MediaStore API now.
This commit is contained in:
parent
4e909ff3b9
commit
7319ac4682
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
196
src/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt
Normal file
196
src/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt
Normal 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?)
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user