android: reviewed storage access

This commit is contained in:
Thibault Deckers 2020-05-29 11:39:05 +09:00
parent 5b3eed7449
commit 3a657c12f0
7 changed files with 122 additions and 138 deletions

View file

@ -5,6 +5,7 @@ import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.media.MediaMetadataRetriever; import android.media.MediaMetadataRetriever;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.text.format.Formatter; import android.text.format.Formatter;
@ -351,6 +352,9 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
} else if (mimeType.startsWith(MimeTypes.VIDEO)) { } else if (mimeType.startsWith(MimeTypes.VIDEO)) {
contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id); contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentUri = MediaStore.setRequireOriginal(contentUri);
}
Cursor cursor = context.getContentResolver().query(contentUri, null, null, null, null); Cursor cursor = context.getContentResolver().query(contentUri, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {

View file

@ -59,7 +59,7 @@ public class StorageHandler implements MethodChannel.MethodCallHandler {
List<Map<String, Object>> volumes = new ArrayList<>(); List<Map<String, Object>> volumes = new ArrayList<>();
StorageManager sm = activity.getSystemService(StorageManager.class); StorageManager sm = activity.getSystemService(StorageManager.class);
if (sm != null) { if (sm != null) {
for (String path : Env.getStorageVolumes(activity)) { for (String path : Env.getStorageVolumeRoots(activity)) {
try { try {
File file = new File(path); File file = new File(path);
StorageVolume volume = sm.getStorageVolume(file); StorageVolume volume = sm.getStorageVolume(file);

View file

@ -13,7 +13,6 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.util.Log; import android.util.Log;
@ -21,6 +20,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.exifinterface.media.ExifInterface; import androidx.exifinterface.media.ExifInterface;
import com.commonsware.cwac.document.DocumentFileCompat;
import com.drew.imaging.ImageMetadataReader; import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException; import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Metadata; import com.drew.metadata.Metadata;
@ -29,7 +29,6 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import java.io.File; import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
@ -44,6 +43,15 @@ import deckers.thibault.aves.utils.PermissionManager;
import deckers.thibault.aves.utils.StorageUtils; import deckers.thibault.aves.utils.StorageUtils;
import deckers.thibault.aves.utils.Utils; import deckers.thibault.aves.utils.Utils;
// *** about file access to write/rename/delete
// * primary volume
// until 28/Pie, use `File`
// on 29/Q, use `File` after setting `requestLegacyExternalStorage` flag in the manifest
// from 30/R, use `DocumentFile` (not `File`) after requesting permission to the volume root???
// * non primary volumes
// on 19/KitKat, use `DocumentFile` (not `File`) after getting permission for each file
// from 21/Lollipop, use `DocumentFile` (not `File`) after getting permission to the volume root
public abstract class ImageProvider { public abstract class ImageProvider {
private static final String LOG_TAG = Utils.createLogTag(ImageProvider.class); private static final String LOG_TAG = Utils.createLogTag(ImageProvider.class);
@ -59,9 +67,9 @@ public abstract class ImageProvider {
return Futures.immediateFailedFuture(new UnsupportedOperationException()); return Futures.immediateFailedFuture(new UnsupportedOperationException());
} }
public void rename(final Activity activity, final String oldPath, final Uri oldUri, final String mimeType, final String newFilename, final ImageOpCallback callback) { public void rename(final Activity activity, final String oldPath, final Uri oldMediaUri, final String mimeType, final String newFilename, final ImageOpCallback callback) {
if (oldPath == null) { if (oldPath == null) {
Log.w(LOG_TAG, "entry does not have a path, uri=" + oldUri); Log.w(LOG_TAG, "entry does not have a path, uri=" + oldMediaUri);
callback.onFailure(); callback.onFailure();
return; return;
} }
@ -75,26 +83,26 @@ public abstract class ImageProvider {
return; return;
} }
// Before KitKat, we do whatever we want on the SD card.
// From KitKat, we need access permission from the Document Provider, at the file level.
// From Lollipop, we can request the permission at the SD card root level.
boolean renamed;
if (Env.isOnSdCard(activity, oldPath)) { if (Env.isOnSdCard(activity, oldPath)) {
// rename with DocumentFile // rename with DocumentFile
Uri sdCardTreeUri = PermissionManager.getSdCardTreeUri(activity); Uri sdCardTreeUri = PermissionManager.getSdCardTreeUri(activity);
if (sdCardTreeUri == null) { if (sdCardTreeUri == null) {
Runnable runnable = () -> rename(activity, oldPath, oldUri, mimeType, newFilename, callback); Runnable runnable = () -> rename(activity, oldPath, oldMediaUri, mimeType, newFilename, callback);
new Handler(Looper.getMainLooper()).post(() -> PermissionManager.showSdCardAccessDialog(activity, runnable)); new Handler(Looper.getMainLooper()).post(() -> PermissionManager.showSdCardAccessDialog(activity, runnable));
return; return;
} }
renamed = StorageUtils.renameOnSdCard(activity, sdCardTreeUri, Env.getStorageVolumes(activity), oldPath, newFilename);
} else {
// rename with File
renamed = oldFile.renameTo(newFile);
} }
if (!renamed) { DocumentFileCompat df = StorageUtils.getDocumentFile(activity, oldPath, oldMediaUri);
Log.w(LOG_TAG, "failed to rename entry at path=" + oldPath); try {
boolean renamed = df != null && df.renameTo(newFilename);
if (!renamed) {
Log.w(LOG_TAG, "failed to rename entry at path=" + oldPath);
callback.onFailure();
return;
}
} catch (FileNotFoundException e) {
Log.w(LOG_TAG, "failed to rename entry at path=" + oldPath, e);
callback.onFailure(); callback.onFailure();
return; return;
} }
@ -204,7 +212,10 @@ public abstract class ImageProvider {
exif.saveAttributes(); exif.saveAttributes();
// if the image is on the SD card, copy the edited temporary file to the original DocumentFile // if the image is on the SD card, copy the edited temporary file to the original DocumentFile
rotated = !onSdCard || StorageUtils.writeToDocumentFile(activity, editablePath, uri); if (onSdCard) {
DocumentFileCompat.fromFile(new File(editablePath)).copyTo(DocumentFileCompat.fromSingleUri(activity, uri));
}
rotated = true;
} catch (IOException e) { } catch (IOException e) {
Log.w(LOG_TAG, "failed to edit EXIF to rotate image at path=" + path, e); Log.w(LOG_TAG, "failed to edit EXIF to rotate image at path=" + path, e);
} }
@ -227,7 +238,8 @@ public abstract class ImageProvider {
values.clear(); values.clear();
values.put(MediaStore.MediaColumns.IS_PENDING, 0); values.put(MediaStore.MediaColumns.IS_PENDING, 0);
} }
values.put(MediaStore.MediaColumns.ORIENTATION, orientationDegrees); // uses MediaStore.Images.Media instead of MediaStore.MediaColumns for APIs < Q
values.put(MediaStore.Images.Media.ORIENTATION, orientationDegrees);
int updatedRowCount = contentResolver.update(uri, values, null, null); int updatedRowCount = contentResolver.update(uri, values, null, null);
if (updatedRowCount > 0) { if (updatedRowCount > 0) {
MediaScannerConnection.scanFile(activity, new String[]{path}, new String[]{mimeType}, (p, u) -> callback.onSuccess(newFields)); MediaScannerConnection.scanFile(activity, new String[]{path}, new String[]{mimeType}, (p, u) -> callback.onSuccess(newFields));
@ -239,15 +251,20 @@ public abstract class ImageProvider {
private void rotatePng(final Activity activity, final String path, final Uri uri, boolean clockwise, final ImageOpCallback callback) { private void rotatePng(final Activity activity, final String path, final Uri uri, boolean clockwise, final ImageOpCallback callback) {
final String mimeType = MimeTypes.PNG; final String mimeType = MimeTypes.PNG;
if (path == null) { String editablePath = path;
callback.onFailure(); boolean onSdCard = Env.isOnSdCard(activity, path);
return; if (onSdCard) {
if (PermissionManager.getSdCardTreeUri(activity) == null) {
Runnable runnable = () -> rotate(activity, path, uri, mimeType, clockwise, callback);
new Handler(Looper.getMainLooper()).post(() -> PermissionManager.showSdCardAccessDialog(activity, runnable));
return;
}
// copy original file to a temporary file for editing
editablePath = StorageUtils.copyFileToTemp(path);
} }
boolean onSdCard = Env.isOnSdCard(activity, path); if (editablePath == null) {
if (onSdCard && PermissionManager.getSdCardTreeUri(activity) == null) { callback.onFailure();
Runnable runnable = () -> rotate(activity, path, uri, mimeType, clockwise, callback);
new Handler(Looper.getMainLooper()).post(() -> PermissionManager.showSdCardAccessDialog(activity, runnable));
return; return;
} }
@ -264,29 +281,16 @@ public abstract class ImageProvider {
Bitmap rotatedImage = Bitmap.createBitmap(originalImage, 0, 0, originalWidth, originalHeight, matrix, true); Bitmap rotatedImage = Bitmap.createBitmap(originalImage, 0, 0, originalWidth, originalHeight, matrix, true);
boolean rotated = false; boolean rotated = false;
if (onSdCard) { try (FileOutputStream fos = new FileOutputStream(editablePath)) {
FileDescriptor fd = null; rotatedImage.compress(Bitmap.CompressFormat.PNG, 100, fos);
try {
ParcelFileDescriptor pfd = activity.getContentResolver().openFileDescriptor(uri, "rw"); // if the image is on the SD card, copy the edited temporary file to the original DocumentFile
if (pfd != null) fd = pfd.getFileDescriptor(); if (onSdCard) {
} catch (FileNotFoundException e) { DocumentFileCompat.fromFile(new File(editablePath)).copyTo(DocumentFileCompat.fromSingleUri(activity, uri));
Log.e(LOG_TAG, "failed to get file descriptor for document at uri=" + path, e);
}
if (fd != null) {
try (FileOutputStream fos = new FileOutputStream(fd)) {
rotatedImage.compress(Bitmap.CompressFormat.PNG, 100, fos);
rotated = true;
} catch (IOException e) {
Log.e(LOG_TAG, "failed to save rotated image to document at uri=" + path, e);
}
}
} else {
try (FileOutputStream fos = new FileOutputStream(path)) {
rotatedImage.compress(Bitmap.CompressFormat.PNG, 100, fos);
rotated = true;
} catch (IOException e) {
Log.e(LOG_TAG, "failed to save rotated image to path=" + path, e);
} }
rotated = true;
} catch (IOException e) {
Log.e(LOG_TAG, "failed to save rotated image to path=" + path, e);
} }
if (!rotated) { if (!rotated) {
callback.onFailure(); callback.onFailure();

View file

@ -22,6 +22,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -29,7 +30,6 @@ import java.util.stream.Stream;
import deckers.thibault.aves.model.ImageEntry; import deckers.thibault.aves.model.ImageEntry;
import deckers.thibault.aves.utils.Env; import deckers.thibault.aves.utils.Env;
import deckers.thibault.aves.utils.MimeTypes; import deckers.thibault.aves.utils.MimeTypes;
import deckers.thibault.aves.utils.PathComponents;
import deckers.thibault.aves.utils.PermissionManager; import deckers.thibault.aves.utils.PermissionManager;
import deckers.thibault.aves.utils.StorageUtils; import deckers.thibault.aves.utils.StorageUtils;
import deckers.thibault.aves.utils.Utils; import deckers.thibault.aves.utils.Utils;
@ -176,13 +176,8 @@ public class MediaStoreImageProvider extends ImageProvider {
return !MimeTypes.SVG.equals(mimeType); return !MimeTypes.SVG.equals(mimeType);
} }
// check write access permission to SD card
// Before KitKat, we do whatever we want on the SD card.
// From KitKat, we need access permission from the Document Provider, at the file level.
// From Lollipop, we can request the permission at the SD card root level.
@Override @Override
public ListenableFuture<Object> delete(final Activity activity, final String path, final Uri uri) { public ListenableFuture<Object> delete(final Activity activity, final String path, final Uri mediaUri) {
SettableFuture<Object> future = SettableFuture.create(); SettableFuture<Object> future = SettableFuture.create();
if (Env.isOnSdCard(activity, path)) { if (Env.isOnSdCard(activity, path)) {
@ -190,7 +185,7 @@ public class MediaStoreImageProvider extends ImageProvider {
if (sdCardTreeUri == null) { if (sdCardTreeUri == null) {
Runnable runnable = () -> { Runnable runnable = () -> {
try { try {
future.set(delete(activity, path, uri).get()); future.set(delete(activity, path, mediaUri).get());
} catch (Exception e) { } catch (Exception e) {
future.setException(e); future.setException(e);
} }
@ -201,15 +196,20 @@ public class MediaStoreImageProvider extends ImageProvider {
// if the file is on SD card, calling the content resolver delete() removes the entry from the Media Store // if the file is on SD card, calling the content resolver delete() removes the entry from the Media Store
// but it doesn't delete the file, even if the app has the permission // but it doesn't delete the file, even if the app has the permission
StorageUtils.deleteFromSdCard(activity, sdCardTreeUri, Env.getStorageVolumes(activity), path); try {
Log.d(LOG_TAG, "deleted from SD card at path=" + uri); DocumentFileCompat df = StorageUtils.getDocumentFile(activity, path, mediaUri);
future.set(null); if (df != null && df.delete()) {
future.set(null);
future.setException(new Exception("failed to delete file with df=" + df));
}
} catch (FileNotFoundException e) {
future.setException(e);
}
return future; return future;
} }
try { try {
if (activity.getContentResolver().delete(uri, null, null) > 0) { if (activity.getContentResolver().delete(mediaUri, null, null) > 0) {
Log.d(LOG_TAG, "deleted from content resolver uri=" + uri);
future.set(null); future.set(null);
} else { } else {
future.setException(new Exception("failed to delete row from content provider")); future.setException(new Exception("failed to delete row from content provider"));
@ -247,8 +247,7 @@ public class MediaStoreImageProvider extends ImageProvider {
// DocumentFile.getParentFile() is null without picking a tree first // DocumentFile.getParentFile() is null without picking a tree first
// DocumentsContract.copyDocument() and moveDocument() need parent doc uri // DocumentsContract.copyDocument() and moveDocument() need parent doc uri
PathComponents sourcePathComponents = new PathComponents(sourcePath, Env.getStorageVolumes(activity)); String destinationPath = destinationDir + File.separator + new File(sourcePath).getName();
String destinationPath = destinationDir + File.separator + sourcePathComponents.getFilename();
ContentValues contentValues = new ContentValues(); ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DATA, destinationPath); contentValues.put(MediaStore.MediaColumns.DATA, destinationPath);

View file

@ -6,7 +6,7 @@ import android.content.SharedPreferences;
import android.os.Environment; import android.os.Environment;
public class Env { public class Env {
private static String[] mStorageVolumes; private static String[] mStorageVolumeRoots;
private static String mExternalStorage; private static String mExternalStorage;
// SD card path as a content URI from the Documents Provider // SD card path as a content URI from the Documents Provider
// e.g. content://com.android.externalstorage.documents/tree/12A9-8B42%3A // e.g. content://com.android.externalstorage.documents/tree/12A9-8B42%3A
@ -29,11 +29,11 @@ public class Env {
return mSdCardDocumentUri; return mSdCardDocumentUri;
} }
public static String[] getStorageVolumes(final Activity activity) { public static String[] getStorageVolumeRoots(final Activity activity) {
if (mStorageVolumes == null) { if (mStorageVolumeRoots == null) {
mStorageVolumes = StorageUtils.getStorageVolumes(activity); mStorageVolumeRoots = StorageUtils.getStorageVolumeRoots(activity);
} }
return mStorageVolumes; return mStorageVolumeRoots;
} }
private static String getExternalStorage() { private static String getExternalStorage() {
@ -44,6 +44,6 @@ public class Env {
} }
public static boolean isOnSdCard(final Activity activity, String path) { public static boolean isOnSdCard(final Activity activity, String path) {
return path != null && !getExternalStorage().equals(new PathComponents(path, getStorageVolumes(activity)).getStorage()); return path != null && !getExternalStorage().equals(new PathSegments(path, getStorageVolumeRoots(activity)).getStorage());
} }
} }

View file

@ -4,22 +4,22 @@ import androidx.annotation.NonNull;
import java.io.File; import java.io.File;
public class PathComponents { public class PathSegments {
private String storage; private String storage;
private String folder; private String relativePath;
private String filename; private String filename;
public PathComponents(@NonNull String path, @NonNull String[] storageVolumes) { public PathSegments(@NonNull String path, @NonNull String[] storageVolumePaths) {
for (int i = 0; i < storageVolumes.length && storage == null; i++) { for (int i = 0; i < storageVolumePaths.length && storage == null; i++) {
if (path.startsWith(storageVolumes[i])) { if (path.startsWith(storageVolumePaths[i])) {
storage = storageVolumes[i]; storage = storageVolumePaths[i];
} }
} }
int lastSeparatorIndex = path.lastIndexOf(File.separator) + 1; int lastSeparatorIndex = path.lastIndexOf(File.separator) + 1;
if (lastSeparatorIndex > storage.length()) { if (lastSeparatorIndex > storage.length()) {
filename = path.substring(lastSeparatorIndex); filename = path.substring(lastSeparatorIndex);
folder = path.substring(storage.length(), lastSeparatorIndex); relativePath = path.substring(storage.length(), lastSeparatorIndex);
} }
} }
@ -27,8 +27,8 @@ public class PathComponents {
return storage; return storage;
} }
public String getFolder() { public String getRelativePath() {
return folder; return relativePath;
} }
public String getFilename() { public String getFilename() {

View file

@ -1,20 +1,22 @@
package deckers.thibault.aves.utils; package deckers.thibault.aves.utils;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.media.MediaMetadataRetriever; import android.media.MediaMetadataRetriever;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import androidx.documentfile.provider.DocumentFile; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.commonsware.cwac.document.DocumentFileCompat;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
@ -85,7 +87,7 @@ public class StorageUtils {
* @return paths to all available SD-Cards in the system (include emulated) * @return paths to all available SD-Cards in the system (include emulated)
*/ */
@SuppressLint("ObsoleteSdkInt") @SuppressLint("ObsoleteSdkInt")
public static String[] getStorageVolumes(Context context) { public static String[] getStorageVolumeRoots(Context context) {
// Final set of paths // Final set of paths
final Set<String> rv = new HashSet<>(); final Set<String> rv = new HashSet<>();
@ -182,36 +184,47 @@ public class StorageUtils {
}; };
} }
private static Optional<DocumentFile> getSdCardDocumentFile(Context context, Uri sdCardTreeUri, String[] storageVolumes, String path) { private static Optional<DocumentFileCompat> getSdCardDocumentFile(Context context, Uri rootTreeUri, String[] storageVolumeRoots, String path) {
if (sdCardTreeUri == null || storageVolumes == null || path == null) { if (rootTreeUri == null || storageVolumeRoots == null || path == null) {
return Optional.empty(); return Optional.empty();
} }
PathComponents pathComponents = new PathComponents(path, storageVolumes); PathSegments pathSegments = new PathSegments(path, storageVolumeRoots);
ArrayList<String> pathSegments = Lists.newArrayList(Splitter.on(File.separatorChar) ArrayList<String> pathSteps = Lists.newArrayList(Splitter.on(File.separatorChar)
.trimResults().omitEmptyStrings().split(pathComponents.getFolder())); .trimResults().omitEmptyStrings().split(pathSegments.getRelativePath()));
pathSegments.add(pathComponents.getFilename()); pathSteps.add(pathSegments.getFilename());
Iterator<String> pathIterator = pathSegments.iterator(); Iterator<String> pathIterator = pathSteps.iterator();
DocumentFileCompat documentFile = DocumentFileCompat.fromTreeUri(context, rootTreeUri);
if (documentFile == null) {
return Optional.empty();
}
// follow the entry path down the document tree // follow the entry path down the document tree
boolean found = true; while (pathIterator.hasNext()) {
DocumentFile documentFile = DocumentFile.fromTreeUri(context, sdCardTreeUri); documentFile = documentFile.findFile(pathIterator.next());
while (pathIterator.hasNext() && found) { if (documentFile == null) {
String segment = pathIterator.next(); return Optional.empty();
found = false;
if (documentFile != null) {
DocumentFile[] children = documentFile.listFiles();
for (int i = children.length - 1; i >= 0 && !found; i--) {
DocumentFile child = children[i];
if (segment.equals(child.getName())) {
found = true;
documentFile = child;
}
}
} }
} }
return Optional.of(documentFile);
}
return found && documentFile != null ? Optional.of(documentFile) : Optional.empty();
@Nullable
public static DocumentFileCompat getDocumentFile(Activity activity, @NonNull String path, @NonNull Uri mediaUri) {
if (Env.isOnSdCard(activity, path)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Uri docUri = MediaStore.getDocumentUri(activity, mediaUri);
return DocumentFileCompat.fromSingleUri(activity, docUri);
} else {
Uri sdCardTreeUri = PermissionManager.getSdCardTreeUri(activity);
String[] storageVolumeRoots = Env.getStorageVolumeRoots(activity);
Optional<DocumentFileCompat> docFile = StorageUtils.getSdCardDocumentFile(activity, sdCardTreeUri, storageVolumeRoots, path);
return docFile.orElse(null);
}
} else {
return DocumentFileCompat.fromFile(new File(path));
}
} }
public static String copyFileToTemp(String path) { public static String copyFileToTemp(String path) {
@ -226,40 +239,4 @@ public class StorageUtils {
} }
return null; return null;
} }
public static boolean writeToDocumentFile(Context context, String from, Uri documentUri) {
try {
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(documentUri, "rw");
if (pfd == null) {
Log.w(LOG_TAG, "failed to get file descriptor for documentUri=" + documentUri);
return false;
}
Utils.copyFile(new File(from), pfd.getFileDescriptor());
return true;
} catch (IOException e) {
Log.w(LOG_TAG, "failed to write to DocumentFile at documentUri=" + documentUri);
}
return false;
}
/**
* Delete the specified file on SD card
* Note that it does not update related content providers such as the Media Store.
*/
public static boolean deleteFromSdCard(Context context, Uri sdCardTreeUri, String[] storageVolumes, String path) {
Optional<DocumentFile> documentFile = getSdCardDocumentFile(context, sdCardTreeUri, storageVolumes, path);
boolean success = documentFile.isPresent() && documentFile.get().delete();
Log.d(LOG_TAG, "deleteFromSdCard success=" + success + " for sdCardTreeUri=" + sdCardTreeUri + ", path=" + path);
return success;
}
/**
* Rename the specified file on SD card
* Note that it does not update related content providers such as the Media Store.
*/
public static boolean renameOnSdCard(Context context, Uri sdCardTreeUri, String[] storageVolumes, String path, String newFilename) {
Log.d(LOG_TAG, "renameOnSdCard with path=" + path + ", newFilename=" + newFilename);
Optional<DocumentFile> documentFile = getSdCardDocumentFile(context, sdCardTreeUri, storageVolumes, path);
return documentFile.isPresent() && documentFile.get().renameTo(newFilename);
}
} }