diff --git a/packages/core/file-system/file-system-access.android.ts b/packages/core/file-system/file-system-access.android.ts index f8bab8c25..d0d6d9bd1 100644 --- a/packages/core/file-system/file-system-access.android.ts +++ b/packages/core/file-system/file-system-access.android.ts @@ -607,6 +607,7 @@ export class FileSystemAccess implements IFileSystemAccess { } export class FileSystemAccess29 extends FileSystemAccess { + __skip = true; getLastModified(path: string): Date { if (isContentUri(path)) { return new Date(getOrSetHelper(path).getLastModified() * 1000); diff --git a/packages/core/file-system/index.ts b/packages/core/file-system/index.ts index 7944e5500..6a694fddc 100644 --- a/packages/core/file-system/index.ts +++ b/packages/core/file-system/index.ts @@ -126,6 +126,26 @@ export class FileSystemEntity { return; } + const localError = function (error) { + if (onError) { + onError(error); + } + + return null; + }; + + const fileAccess = getFileAccess(); + // call rename for FileSystemAccess29 + if ((fileAccess).__skip) { + fileAccess.rename(this.path, newName, localError); + const fileInfo = getFileAccess().getFile(this.path, null); + if (fileInfo) { + this._name = fileInfo.name; + this._extension = fileInfo.extension; + } + return; + } + const parentFolder = this.parent; if (!parentFolder) { if (onError) { @@ -135,18 +155,9 @@ export class FileSystemEntity { return; } - const fileAccess = getFileAccess(); const path = parentFolder.path; const newPath = fileAccess.joinPath(path, newName); - const localError = function (error) { - if (onError) { - onError(error); - } - - return null; - }; - fileAccess.rename(this.path, newPath, localError); this._path = newPath; this._name = newName; diff --git a/packages/core/platforms/android/widgets-release.aar b/packages/core/platforms/android/widgets-release.aar index 81534e9bc..6bb9d0ea2 100644 Binary files a/packages/core/platforms/android/widgets-release.aar and b/packages/core/platforms/android/widgets-release.aar differ diff --git a/packages/ui-mobile-base/android/widgets/build.gradle b/packages/ui-mobile-base/android/widgets/build.gradle index 8e0384488..a17206f3d 100644 --- a/packages/ui-mobile-base/android/widgets/build.gradle +++ b/packages/ui-mobile-base/android/widgets/build.gradle @@ -83,6 +83,12 @@ dependencies { outLogger.withStyle(Style.SuccessHeader).println "\t + using android X library androidx.appcompat:appcompat:$androidXAppCompatVersion" } + def androidXDocumentFileVersion = "1.0.1" + if (project.hasProperty("androidXDocumentFile")) { + androidXDocumentFileVersion = androidXDocumentFile + outLogger.withStyle(Style.SuccessHeader).println "\t + using android X library androidx.documentfile:documentfile:$androidXDocumentFileVersion" + } + implementation fileTree(include: ['*.jar'], dir: 'libs') @@ -93,6 +99,7 @@ dependencies { implementation "androidx.transition:transition:$androidXTransitionVersion" implementation "androidx.exifinterface:exifinterface:$androidXExifInterfaceVersion" implementation "androidx.appcompat:appcompat:$androidXAppCompatVersion" + implementation "androidx.documentfile:documentfile:$androidXDocumentFileVersion" } task cleanBuildDir (type: Delete) { diff --git a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/FileHelper.java b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/FileHelper.java index 93ebda0e8..1aba3c12b 100644 --- a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/FileHelper.java +++ b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/FileHelper.java @@ -5,15 +5,20 @@ import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Build; +import android.os.Environment; import android.os.Handler; import android.os.Looper; +import android.provider.DocumentsContract; import android.provider.MediaStore; import android.webkit.MimeTypeMap; import androidx.annotation.Nullable; +import androidx.documentfile.provider.DocumentFile; import java.io.BufferedReader; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.InputStream; import java.io.InputStreamReader; @@ -42,6 +47,51 @@ public class FileHelper { this.uri = uri; } + private static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri + .getAuthority()); + } + + private static @Nullable + Cursor getCursor(Context context, Uri uri) { + Cursor cursor = null; + try { + if (Build.VERSION.SDK_INT >= 19) { + if (DocumentsContract.isDocumentUri(context, uri)) { + String docId = DocumentsContract.getDocumentId(uri); + String[] split = docId.split(":"); + String type = split[0]; + + Uri contentUri; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } else { + contentUri = MediaStore.Files.getContentUri("external"); + } + + String selection = "_id=?"; + String[] selectionArgs = {split[1]}; + + cursor = context.getContentResolver().query( + contentUri, null, selection, selectionArgs, null, null + ); + } + } + if (cursor == null) { + cursor = context.getContentResolver().query(uri, null, null, null, null); + } + } catch (Exception e) { + cursor = null; + } + + return cursor; + + } + public static boolean exists(Context context, String string) { try { return exists(context, Uri.parse(string)); @@ -51,9 +101,17 @@ public class FileHelper { } public static boolean exists(Context context, Uri uri) { - Cursor cursor = context.getContentResolver() - .query(uri, null, null, null, null); - + if (Build.VERSION.SDK_INT >= 19 && isExternalStorageDocument(uri)) { + File file = getFile(context, uri); + if (file != null) { + return file.exists(); + } + return false; + } + Cursor cursor = getCursor(context, uri); + if (cursor == null) { + return false; + } boolean exists = cursor.moveToFirst(); cursor.close(); return exists; @@ -62,17 +120,77 @@ public class FileHelper { public static @Nullable FileHelper fromString(Context context, String string) { try { - return fromUri(context, Uri.parse(string)); - } catch (Exception ignored) { + return fromUri(context, Uri.parse(Uri.decode(string)), false); + } catch (Exception e) { return null; } + } + @SuppressWarnings("deprecation") + private static @Nullable + File getFile(Context context, Uri uri) { + if (Build.VERSION.SDK_INT >= 19) { + String docId = DocumentsContract.getDocumentId(uri); + String[] split = docId.split(":"); + String type = split[0]; + String path = split[1]; + + if ("primary".equals(type)) { + String[] parts = Uri.decode(uri.toString()).split(":" + path + "/"); + String file = Environment.getExternalStorageDirectory() + "/" + path + "/" + parts[1]; + return new File(file); + } else { + File[] cacheDirs = context.getExternalCacheDirs(); + String storageDir = null; + for (File cacheDir : cacheDirs) { + final String cachePath = cacheDir.getPath(); + int index = cachePath.indexOf(type); + if (index >= 0) { + storageDir = cachePath.substring(0, index + type.length()); + } + } + + if (storageDir != null) { + return new File(storageDir + "/" + path); + } + } + } + return null; } public static @Nullable FileHelper fromUri(Context context, Uri uri) { - Cursor cursor = context.getContentResolver() - .query(uri, null, null, null, null); + return fromUri(context, uri, true); + } + + private static @Nullable + FileHelper fromUri(Context context, Uri contentUri, boolean parseUri) { + Uri uri; + + if (parseUri) { + uri = Uri.parse(Uri.decode(contentUri.toString())); + } else { + uri = contentUri; + } + + if (Build.VERSION.SDK_INT >= 19 && isExternalStorageDocument(uri)) { + File file = getFile(context, uri); + if (file == null) { + return null; + } + FileHelper helper = new FileHelper(uri); + helper.size = file.length(); + helper.name = file.getName(); + helper.mime = context.getContentResolver().getType(uri); + helper.lastModified = file.lastModified() / 1000; + return helper; + } + + Cursor cursor = getCursor(context, uri); + + if (cursor == null) { + return null; + } int sizeIndex = cursor.getColumnIndex( MediaStore.MediaColumns.SIZE @@ -104,20 +222,41 @@ public class FileHelper { updateInternal(context, true); } + + private void updateValue(Context context, Uri uri, ContentValues values) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + context.getContentResolver().update(uri, values, null); + } else { + context.getContentResolver().update(uri, values, null, null); + } + } + private void updateInternal(Context context, boolean force) { if (force) { // trigger db update ContentValues values = new ContentValues(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - context.getContentResolver().update(uri, values, null); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (isExternalStorageDocument(uri)) { + return; + } + if (DocumentsContract.isDocumentUri(context, uri)) { + DocumentFile file = DocumentFile.fromSingleUri(context, uri); + if (file != null) { + updateValue(context, file.getUri(), values); + } + } else { + updateValue(context, uri, values); + } } else { - context.getContentResolver().update(uri, values, null, null); + updateValue(context, uri, values); } } - Cursor cursor = context.getContentResolver() - .query(uri, null, null, null, null); + Cursor cursor = getCursor(context, uri); + if (cursor == null) { + return; + } int sizeIndex = cursor.getColumnIndex( MediaStore.MediaColumns.SIZE @@ -170,8 +309,35 @@ public class FileHelper { return lastModified; } + private InputStream getInputStream(Context context, Uri uri) throws Exception { + if (Build.VERSION.SDK_INT >= 19) { + if (isExternalStorageDocument(uri)) { + File file = getFile(context, uri); + return new FileInputStream(file); + } + if (DocumentsContract.isDocumentUri(context, uri)) { + return context.getContentResolver().openInputStream(DocumentFile.fromSingleUri(context, uri).getUri()); + } + } + return context.getContentResolver().openInputStream(uri); + } + + + private OutputStream getOutputStream(Context context, Uri uri) throws Exception { + if (Build.VERSION.SDK_INT >= 19) { + if (isExternalStorageDocument(uri)) { + File file = getFile(context, uri); + return new FileOutputStream(file); + } + if (DocumentsContract.isDocumentUri(context, uri)) { + return context.getContentResolver().openOutputStream(DocumentFile.fromSingleUri(context, uri).getUri()); + } + } + return context.getContentResolver().openOutputStream(uri); + } + private byte[] readSyncInternal(Context context) throws Exception { - InputStream is = context.getContentResolver().openInputStream(uri); + InputStream is = getInputStream(context, uri); byte[] array = new byte[(int) size]; is.read(array); is.close(); @@ -207,7 +373,7 @@ public class FileHelper { characterSet = "UTF-8"; } - InputStream is = context.getContentResolver().openInputStream(uri); + InputStream is = getInputStream(context, uri); InputStreamReader isr = new InputStreamReader(is, characterSet); BufferedReader reader = new BufferedReader(isr); char[] buf = new char[is.available()]; @@ -239,7 +405,7 @@ public class FileHelper { } private void writeSyncInternal(Context context, byte[] content) throws Exception { - OutputStream os = context.getContentResolver().openOutputStream(uri); + OutputStream os = getOutputStream(context, uri); os.write(content, 0, content.length); os.flush(); os.close(); @@ -268,7 +434,7 @@ public class FileHelper { } private void writeTextSyncInternal(Context context, String content, @Nullable String encoding) throws Exception { - OutputStream os = context.getContentResolver().openOutputStream(uri); + OutputStream os = getOutputStream(context, uri); String characterSet = encoding; if (characterSet == null) { characterSet = "UTF-8"; @@ -313,7 +479,7 @@ public class FileHelper { } private void copyToFileInternal(Context context, File file) throws Exception { - InputStream is = context.getContentResolver().openInputStream(uri); + InputStream is = getInputStream(context, uri); FileOutputStream os = new FileOutputStream(file); copyToFileInternal(is, os); } @@ -344,6 +510,41 @@ public class FileHelper { public boolean delete(Context context) { try { + if (Build.VERSION.SDK_INT >= 19) { + if (isExternalStorageDocument(uri)) { + File file = getFile(context, uri); + if (file != null) { + return file.delete(); + } + return false; + } + + if (DocumentsContract.isDocumentUri(context, uri)) { + String docId = DocumentsContract.getDocumentId(uri); + String[] split = docId.split(":"); + String type = split[0]; + + Uri contentUri; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } else { + contentUri = MediaStore.Files.getContentUri("external"); + } + + String selection = "_id=?"; + String[] selectionArgs = {split[1]}; + + return context.getContentResolver().delete( + contentUri, selection, selectionArgs + ) > 0; + + } + + } return context.getContentResolver().delete(uri, null, null) > 0; } catch (SecurityException e) { return false; @@ -354,6 +555,44 @@ public class FileHelper { ContentValues values = new ContentValues(); values.put(MediaStore.MediaColumns.DISPLAY_NAME, newName); + if (Build.VERSION.SDK_INT >= 19) { + if (isExternalStorageDocument(uri)) { + File file = getFile(context, uri); + if (file != null) { + file.renameTo(new File(file.getParentFile(), newName)); + return; + } + return; + } + + if (DocumentsContract.isDocumentUri(context, uri)) { + String docId = DocumentsContract.getDocumentId(uri); + String[] split = docId.split(":"); + String type = split[0]; + + Uri contentUri; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } else { + contentUri = MediaStore.Files.getContentUri("external"); + } + + String selection = "_id=?"; + String[] selectionArgs = {split[1]}; + + context.getContentResolver().update( + contentUri, values, selection, selectionArgs + ); + + return; + } + + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { context.getContentResolver().update(uri, values, null); } else { @@ -373,7 +612,6 @@ public class FileHelper { } } - public void rename(Context context, String newName, Callback callback) { executor.execute(() -> { try {