mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-26 12:44:38 +00:00
Use AttachmentSaver to save media overview files to device storage.
This commit is contained in:
committed by
Cody Henthorne
parent
18328079c8
commit
0ef627b864
@@ -21,6 +21,7 @@ import org.signal.core.util.orNull
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ProgressCardDialogFragment
|
||||
import org.thoughtcrime.securesms.components.ProgressCardDialogFragmentArgs
|
||||
import org.thoughtcrime.securesms.database.MediaTable
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
@@ -56,6 +57,20 @@ class AttachmentSaver(private val host: Host) {
|
||||
|
||||
fun saveAttachmentsRx(attachments: Set<SaveAttachment>): Completable = rxCompletable { saveAttachments(attachments) }
|
||||
|
||||
suspend fun saveAttachments(records: Collection<MediaTable.MediaRecord>) {
|
||||
val attachments = records.mapNotNull { record ->
|
||||
val uri = record.attachment?.uri
|
||||
val contentType = record.contentType
|
||||
if (uri != null && contentType != null) {
|
||||
SaveAttachment(uri, contentType, record.date, record.attachment.fileName)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.toSet()
|
||||
saveAttachments(attachments)
|
||||
}
|
||||
|
||||
fun saveAttachmentsRx(records: Collection<MediaTable.MediaRecord>): Completable = rxCompletable { saveAttachments(records) }
|
||||
suspend fun saveAttachments(attachments: Set<SaveAttachment>) {
|
||||
if (checkIsSaveWarningAccepted(attachmentCount = attachments.size) == SaveToStorageWarningResult.ACCEPTED) {
|
||||
if (checkCanWriteToMediaStore() == RequestPermissionResult.GRANTED) {
|
||||
|
||||
@@ -1,56 +1,37 @@
|
||||
package org.thoughtcrime.securesms.mediaoverview;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentSaver;
|
||||
import org.thoughtcrime.securesms.database.MediaTable;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSyncJob;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import io.reactivex.rxjava3.core.Completable;
|
||||
|
||||
final class MediaActions {
|
||||
|
||||
private MediaActions() {
|
||||
}
|
||||
|
||||
static void handleSaveMedia(@NonNull Fragment fragment,
|
||||
@NonNull Collection<MediaTable.MediaRecord> mediaRecords,
|
||||
@Nullable Runnable postExecute)
|
||||
static Completable handleSaveMedia(@NonNull Fragment fragment,
|
||||
@NonNull Collection<MediaTable.MediaRecord> mediaRecords)
|
||||
{
|
||||
Context context = fragment.requireContext();
|
||||
|
||||
if (StorageUtil.canWriteToMediaStore()) {
|
||||
performSaveToDisk(context, mediaRecords, postExecute);
|
||||
return;
|
||||
}
|
||||
|
||||
SaveAttachmentTask.showWarningDialogIfNecessary(context, mediaRecords.size(), () -> Permissions.with(fragment)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(fragment.getString(R.string.AttachmentSaver__signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
||||
.onAnyDenied(() -> Toast.makeText(context, R.string.AttachmentSaver__unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||
.onAllGranted(() -> performSaveToDisk(context, mediaRecords, postExecute))
|
||||
.execute());
|
||||
return new AttachmentSaver(fragment).saveAttachmentsRx(mediaRecords);
|
||||
}
|
||||
|
||||
static void handleDeleteMedia(@NonNull Context context,
|
||||
@@ -96,37 +77,4 @@ final class MediaActions {
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private static void performSaveToDisk(@NonNull Context context, @NonNull Collection<MediaTable.MediaRecord> mediaRecords, @Nullable Runnable postExecute) {
|
||||
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 (MediaTable.MediaRecord mediaRecord : mediaRecords) {
|
||||
if (mediaRecord.getAttachment().getUri() != null) {
|
||||
attachments.add(new SaveAttachmentTask.Attachment(mediaRecord.getAttachment().getUri(),
|
||||
mediaRecord.getContentType(),
|
||||
mediaRecord.getDate(),
|
||||
mediaRecord.getAttachment().fileName));
|
||||
}
|
||||
}
|
||||
|
||||
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[0]));
|
||||
|
||||
if (postExecute != null) postExecute.run();
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,14 +46,16 @@ import org.thoughtcrime.securesms.database.loaders.MediaLoader;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewV2Activity;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.BottomOffsetDecoration;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
|
||||
public final class MediaOverviewPageFragment extends Fragment
|
||||
implements MediaGalleryAllAdapter.ItemClickListener,
|
||||
MediaGalleryAllAdapter.AudioItemListener,
|
||||
@@ -359,9 +361,12 @@ public final class MediaOverviewPageFragment extends Fragment
|
||||
|
||||
bottomActionBar.setItems(Arrays.asList(
|
||||
new ActionItem(R.drawable.symbol_save_android_24, getResources().getQuantityString(R.plurals.MediaOverviewActivity_save_plural, selectionCount), () -> {
|
||||
MediaActions.handleSaveMedia(MediaOverviewPageFragment.this,
|
||||
getListAdapter().getSelectedMedia(),
|
||||
this::exitMultiSelect);
|
||||
lifecycleDisposable.add(
|
||||
MediaActions
|
||||
.handleSaveMedia(MediaOverviewPageFragment.this, getListAdapter().getSelectedMedia())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::exitMultiSelect)
|
||||
);
|
||||
}),
|
||||
new ActionItem(R.drawable.symbol_check_circle_24, getString(R.string.MediaOverviewActivity_select_all), this::handleSelectAllMedia),
|
||||
new ActionItem(R.drawable.symbol_trash_24, getResources().getQuantityString(R.plurals.MediaOverviewActivity_delete_plural, selectionCount), this::handleDeleteSelectedMedia)
|
||||
@@ -400,6 +405,12 @@ public final class MediaOverviewPageFragment extends Fragment
|
||||
voiceNoteMediaController.getVoiceNotePlaybackState().removeObserver(observer);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
private class ActionModeCallback implements ActionMode.Callback {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,465 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.MapUtil;
|
||||
import org.signal.core.util.StreamUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.util.Pair;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
|
||||
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;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link SaveAttachmentUtil} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTask.Attachment, Void, Pair<Integer, String>> {
|
||||
private static final String TAG = Log.tag(SaveAttachmentTask.class);
|
||||
|
||||
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;
|
||||
|
||||
private final Map<Uri, Set<String>> batchOperationNameCache = new HashMap<>();
|
||||
|
||||
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 (!StorageUtil.canWriteToMediaStore()) {
|
||||
return new Pair<>(WRITE_ACCESS_FAILURE, 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 (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
return new Pair<>(FAILURE, null);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable String saveAttachment(Context context, Attachment attachment) throws IOException
|
||||
{
|
||||
String contentType = Objects.requireNonNull(MediaUtil.getCorrectedMimeType(attachment.contentType));
|
||||
String fileName = attachment.fileName;
|
||||
|
||||
if (fileName == null) {
|
||||
fileName = generateOutputFileName(contentType, attachment.date);
|
||||
}
|
||||
|
||||
fileName = sanitizeOutputFileName(fileName);
|
||||
|
||||
CreateMediaUriResult result = createMediaUri(getMediaStoreContentUriForType(contentType), contentType, fileName);
|
||||
ContentValues updateValues = new ContentValues();
|
||||
|
||||
final Uri mediaUri = result.mediaUri;
|
||||
|
||||
if (mediaUri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try (InputStream inputStream = PartAuthority.getAttachmentStream(context, attachment.uri)) {
|
||||
|
||||
if (inputStream == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Objects.equals(result.outputUri.getScheme(), ContentResolver.SCHEME_FILE)) {
|
||||
try (OutputStream outputStream = new FileOutputStream(mediaUri.getPath())) {
|
||||
StreamUtil.copy(inputStream, outputStream);
|
||||
MediaScannerConnection.scanFile(context, new String[] { mediaUri.getPath() }, new String[] { contentType }, null);
|
||||
}
|
||||
} else {
|
||||
try (OutputStream outputStream = context.getContentResolver().openOutputStream(mediaUri, "w")) {
|
||||
long total = StreamUtil.copy(inputStream, outputStream);
|
||||
if (total > 0) {
|
||||
updateValues.put(MediaStore.MediaColumns.SIZE, total);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT > 28) {
|
||||
updateValues.put(MediaStore.MediaColumns.IS_PENDING, 0);
|
||||
}
|
||||
|
||||
if (updateValues.size() > 0) {
|
||||
getContext().getContentResolver().update(mediaUri, updateValues, null, null);
|
||||
}
|
||||
|
||||
return result.outputUri.getLastPathSegment();
|
||||
}
|
||||
|
||||
private @NonNull Uri getMediaStoreContentUriForType(@NonNull String contentType) {
|
||||
if (contentType.startsWith("video/")) {
|
||||
return StorageUtil.getVideoUri();
|
||||
} else if (contentType.startsWith("audio/")) {
|
||||
return StorageUtil.getAudioUri();
|
||||
} else if (contentType.startsWith("image/")) {
|
||||
return StorageUtil.getImageUri();
|
||||
} else {
|
||||
return StorageUtil.getDownloadUri();
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable File ensureExternalPath(@Nullable File path) {
|
||||
if (path != null && path.exists()) {
|
||||
return path;
|
||||
}
|
||||
|
||||
if (path == null) {
|
||||
File documents = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
|
||||
if (documents.exists() || documents.mkdirs()) {
|
||||
return documents;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (path.mkdirs()) {
|
||||
return path;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a path to a shared media (or documents) directory for the type of the file.
|
||||
* <p>
|
||||
* Note that this method attempts to create a directory if the path returned from
|
||||
* Environment object does not exist yet. The attempt may fail in which case it attempts
|
||||
* to return the default "Document" path. It finally returns null if it also fails.
|
||||
* Otherwise it returns the absolute path to the directory.
|
||||
*
|
||||
* @param contentType a MIME type of a file
|
||||
* @return an absolute path to a directory or null
|
||||
*/
|
||||
private @Nullable String getExternalPathForType(String contentType) {
|
||||
File storage = null;
|
||||
if (contentType.startsWith("video/")) {
|
||||
storage = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
|
||||
} else if (contentType.startsWith("audio/")) {
|
||||
storage = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC);
|
||||
} else if (contentType.startsWith("image/")) {
|
||||
storage = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
|
||||
}
|
||||
|
||||
storage = ensureExternalPath(storage);
|
||||
if (storage == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return storage.getAbsolutePath();
|
||||
}
|
||||
|
||||
private String generateOutputFileName(@NonNull String contentType, long timestamp) {
|
||||
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
|
||||
String extension = mimeTypeMap.getExtensionFromMimeType(contentType);
|
||||
SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS");
|
||||
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 @NonNull CreateMediaUriResult createMediaUri(@NonNull Uri outputUri, @NonNull String contentType, @NonNull String fileName)
|
||||
throws IOException
|
||||
{
|
||||
String[] fileParts = getFileNameParts(fileName);
|
||||
String base = fileParts[0];
|
||||
String extension = fileParts[1];
|
||||
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
|
||||
if (MediaUtil.isOctetStream(mimeType) && MediaUtil.isImageVideoOrAudioType(contentType)) {
|
||||
Log.d(TAG, "MimeTypeMap returned octet stream for media, changing to provided content type [" + contentType + "] instead.");
|
||||
mimeType = contentType;
|
||||
}
|
||||
|
||||
if (MediaUtil.isOctetStream(mimeType)) {
|
||||
if (outputUri.equals(StorageUtil.getAudioUri())) {
|
||||
mimeType = "audio/*";
|
||||
} else if (outputUri.equals(StorageUtil.getVideoUri())) {
|
||||
mimeType = "video/*";
|
||||
} else if (outputUri.equals(StorageUtil.getImageUri())) {
|
||||
mimeType = "image/*";
|
||||
}
|
||||
}
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
|
||||
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
|
||||
contentValues.put(MediaStore.MediaColumns.DATE_ADDED, TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
|
||||
contentValues.put(MediaStore.MediaColumns.DATE_MODIFIED, TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
|
||||
|
||||
if (Build.VERSION.SDK_INT > 28) {
|
||||
int i = 0;
|
||||
String displayName = fileName;
|
||||
|
||||
while (pathInCache(outputUri, displayName) || displayNameTaken(outputUri, displayName)) {
|
||||
displayName = base + "-" + (++i) + "." + extension;
|
||||
}
|
||||
|
||||
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName);
|
||||
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 1);
|
||||
putInCache(outputUri, displayName);
|
||||
} else if (Objects.equals(outputUri.getScheme(), ContentResolver.SCHEME_FILE)) {
|
||||
File outputDirectory = new File(outputUri.getPath());
|
||||
File outputFile = new File(outputDirectory, base + "." + extension);
|
||||
|
||||
int i = 0;
|
||||
while (pathInCache(outputUri, outputFile.getPath()) || outputFile.exists()) {
|
||||
outputFile = new File(outputDirectory, base + "-" + (++i) + "." + extension);
|
||||
}
|
||||
|
||||
if (outputFile.isHidden()) {
|
||||
throw new IOException("Specified name would not be visible");
|
||||
}
|
||||
|
||||
putInCache(outputUri, outputFile.getPath());
|
||||
return new CreateMediaUriResult(outputUri, Uri.fromFile(outputFile));
|
||||
} else {
|
||||
String dir = getExternalPathForType(contentType);
|
||||
if (dir == null) {
|
||||
throw new IOException(String.format(Locale.US, "Path for type: %s was not available", contentType));
|
||||
}
|
||||
|
||||
String outputFileName = fileName;
|
||||
String dataPath = String.format("%s/%s", dir, outputFileName);
|
||||
int i = 0;
|
||||
while (pathInCache(outputUri, dataPath) || pathTaken(outputUri, dataPath)) {
|
||||
Log.d(TAG, "The content exists. Rename and check again.");
|
||||
outputFileName = base + "-" + (++i) + "." + extension;
|
||||
dataPath = String.format("%s/%s", dir, outputFileName);
|
||||
}
|
||||
putInCache(outputUri, outputFileName);
|
||||
contentValues.put(MediaStore.MediaColumns.DATA, dataPath);
|
||||
}
|
||||
|
||||
try {
|
||||
return new CreateMediaUriResult(outputUri, getContext().getContentResolver().insert(outputUri, contentValues));
|
||||
} catch (RuntimeException e) {
|
||||
if (e instanceof IllegalArgumentException || e.getCause() instanceof IllegalArgumentException) {
|
||||
Log.w(TAG, "Unable to create uri in " + outputUri + " with mimeType [" + mimeType + "]");
|
||||
return new CreateMediaUriResult(StorageUtil.getDownloadUri(), getContext().getContentResolver().insert(StorageUtil.getDownloadUri(), contentValues));
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void putInCache(@NonNull Uri outputUri, @NonNull String dataPath) {
|
||||
Set<String> pathSet = MapUtil.getOrDefault(batchOperationNameCache, outputUri, new HashSet<>());
|
||||
if (!pathSet.add(dataPath)) {
|
||||
throw new IllegalStateException("Path already used in data set.");
|
||||
}
|
||||
|
||||
batchOperationNameCache.put(outputUri, pathSet);
|
||||
}
|
||||
|
||||
private boolean pathInCache(@NonNull Uri outputUri, @NonNull String dataPath) {
|
||||
Set<String> pathSet = batchOperationNameCache.get(outputUri);
|
||||
if (pathSet == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pathSet.contains(dataPath);
|
||||
}
|
||||
|
||||
private boolean pathTaken(@NonNull Uri outputUri, @NonNull String dataPath) throws IOException {
|
||||
try (Cursor cursor = getContext().getContentResolver().query(outputUri,
|
||||
new String[] { MediaStore.MediaColumns.DATA },
|
||||
MediaStore.MediaColumns.DATA + " = ?",
|
||||
new String[] { dataPath },
|
||||
null))
|
||||
{
|
||||
if (cursor == null) {
|
||||
throw new IOException("Something is wrong with the filename to save");
|
||||
}
|
||||
return cursor.moveToFirst();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean displayNameTaken(@NonNull Uri outputUri, @NonNull String displayName) throws IOException {
|
||||
try (Cursor cursor = getContext().getContentResolver().query(outputUri,
|
||||
new String[] { MediaStore.MediaColumns.DISPLAY_NAME },
|
||||
MediaStore.MediaColumns.DISPLAY_NAME + " = ?",
|
||||
new String[] { displayName },
|
||||
null))
|
||||
{
|
||||
if (cursor == null) {
|
||||
throw new IOException("Something is wrong with the displayName to save");
|
||||
}
|
||||
|
||||
return cursor.moveToFirst();
|
||||
}
|
||||
}
|
||||
|
||||
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.SaveAttachment_error_while_saving_attachments_to_sd_card, attachmentCount),
|
||||
Toast.LENGTH_LONG).show();
|
||||
break;
|
||||
case SUCCESS:
|
||||
Toast.makeText(context,
|
||||
context.getResources().getQuantityText(R.plurals.SaveAttachment_saved_success, attachmentCount),
|
||||
Toast.LENGTH_LONG).show();
|
||||
break;
|
||||
case WRITE_ACCESS_FAILURE:
|
||||
Toast.makeText(context, R.string.SaveAttachment_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,
|
||||
@Nullable 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;
|
||||
}
|
||||
}
|
||||
|
||||
private static class CreateMediaUriResult {
|
||||
final Uri outputUri;
|
||||
final Uri mediaUri;
|
||||
|
||||
private CreateMediaUriResult(Uri outputUri, Uri mediaUri) {
|
||||
this.outputUri = outputUri;
|
||||
this.mediaUri = mediaUri;
|
||||
}
|
||||
}
|
||||
|
||||
public static void showWarningDialogIfNecessary(Context context, int count, Runnable onSave) {
|
||||
if (SignalStore.uiHints().hasDismissedSaveStorageWarning()) {
|
||||
onSave.run();
|
||||
} else {
|
||||
new MaterialAlertDialogBuilder(context)
|
||||
.setView(R.layout.dialog_save_attachment)
|
||||
.setTitle(R.string.AttachmentSaver__save_to_phone)
|
||||
.setCancelable(true)
|
||||
.setMessage(context.getResources().getQuantityString(R.plurals.AttachmentSaver__this_media_will_be_saved, count, count))
|
||||
.setPositiveButton(R.string.save, ((dialog, i) -> {
|
||||
CheckBox checkbox = ((AlertDialog) dialog).findViewById(R.id.checkbox);
|
||||
if (checkbox.isChecked()) {
|
||||
SignalStore.uiHints().markDismissedSaveStorageWarning();
|
||||
}
|
||||
onSave.run();
|
||||
}))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +40,6 @@ import java.util.concurrent.TimeUnit
|
||||
*/
|
||||
private typealias BatchOperationNameCache = HashMap<Uri, HashSet<String>>
|
||||
|
||||
/**
|
||||
* This is a rewrite of [SaveAttachmentTask] that does not handle displaying
|
||||
* a progress dialog and is not backed by an async task.
|
||||
*/
|
||||
object SaveAttachmentUtil {
|
||||
private val TAG = Log.tag(SaveAttachmentUtil::class.java)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user