From af73d133a4979ea73d044b5917df3970e852a919 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Fri, 29 Mar 2024 12:51:19 -0700 Subject: [PATCH 01/21] Add fix --- .../file_selector/file_selector/pubspec.yaml | 3 +- .../FileSelectorApiImpl.java | 8 ++- .../file_selector_android/FileUtils.java | 54 +++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java diff --git a/packages/file_selector/file_selector/pubspec.yaml b/packages/file_selector/file_selector/pubspec.yaml index d53b7b3efed..882d248e67e 100644 --- a/packages/file_selector/file_selector/pubspec.yaml +++ b/packages/file_selector/file_selector/pubspec.yaml @@ -26,7 +26,8 @@ flutter: default_package: file_selector_windows dependencies: - file_selector_android: ^0.5.0 + file_selector_android: + path: ../file_selector_android/ file_selector_ios: ^0.5.0 file_selector_linux: ^0.9.2 file_selector_macos: ^0.9.3 diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java index 32502ed2693..ee3bc4bf7b1 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java @@ -204,7 +204,13 @@ public void getDirectoryPath( public void onResult(int resultCode, @Nullable Intent data) { if (resultCode == Activity.RESULT_OK && data != null) { final Uri uri = data.getData(); - result.success(uri.toString()); + Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri)); + try { + String path = FileUtils.getPathFromUri(activityPluginBinding.getActivity(), docUri); + result.success(path); + } catch (UnsupportedOperationException exception) { + result.error(exception) + } } else { result.success(null); } diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java new file mode 100644 index 00000000000..61eb48b0cb2 --- /dev/null +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -0,0 +1,54 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package dev.flutter.packages.file_selector_android; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.text.TextUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; + +public class FileUtils { + + /** URI authority that represents access to external storage providers. */ + public static String EXTERNAL_DOCUMENT_AUTHORITY = "com.android.externalstorage.documents"; + + /** + * Retrieves path of directory represented by the specified {@code Uri}. + * + *

Will return the path for on-device directories, but does not handle external storage volumes. + */ + public static String getPathFromUri(Activity activity, Uri uri) { + String uriAuthority = uri.getAuthority(); + + if (uriAuthority.equals(EXTERNAL_DOCUMENT_AUTHORITY)) { + String uriDocumentId = DocumentsContract.getDocumentId(uri); + String documentStorageVolume = uriDocumentId.split(":")[0]; + + // Non-primary storage volumes come from SD cards, USB drives, etc. and are + // not handled here. + if (!documentStorageVolume.equals("primary")) { + throw new UnsupportedOperationException("Retrieving the path of a document from storage volume " + documentStorageVolume + "is unsupported by this plugin."); + } + String innermostDirectoryName = uriDocumentId.split(":")[1]; + String externalStorageDirectory = Environment.getExternalStorageDirectory().getPath(); + + return externalStorageDirectory + "/" + innermostDirectoryName; + } else { + throw new UnsupportedOperationException("Retrieving the path from URIs with authority " + uriAuthority.toString() + "are unsupported by this plugin."); + } + } +} From d976090b56ca04111466396dcb99099749f8767e Mon Sep 17 00:00:00 2001 From: camsim99 Date: Fri, 29 Mar 2024 12:57:26 -0700 Subject: [PATCH 02/21] Add todo --- .../packages/file_selector_android/FileSelectorApiImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java index ee3bc4bf7b1..7a079c377d5 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java @@ -341,7 +341,7 @@ GeneratedFileSelectorApi.FileResponse toFileResponse(@NonNull Uri uri) { return new GeneratedFileSelectorApi.FileResponse.Builder() .setName(name) .setBytes(bytes) - .setPath(uri.toString()) + .setPath(uri.toString()) // TODO(camsim99): Test getDirectoryPath fix for getting actual path here. .setMimeType(contentResolver.getType(uri)) .setSize(size.longValue()) .build(); From 9a39660436d39f069c655b1033b82f8420ba4e68 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Mon, 1 Apr 2024 11:29:34 -0700 Subject: [PATCH 03/21] Debugging --- .../FileSelectorApiImpl.java | 21 ++++++++++++--- .../file_selector_android/FileUtils.java | 26 +++++++++++++++++-- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java index 7a079c377d5..9d02ed23c00 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java @@ -204,12 +204,12 @@ public void getDirectoryPath( public void onResult(int resultCode, @Nullable Intent data) { if (resultCode == Activity.RESULT_OK && data != null) { final Uri uri = data.getData(); - Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri)); + final Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri)); try { - String path = FileUtils.getPathFromUri(activityPluginBinding.getActivity(), docUri); + final String path = FileUtils.getPathFromUri(activityPluginBinding.getActivity(), docUri); result.success(path); } catch (UnsupportedOperationException exception) { - result.error(exception) + result.error(exception); } } else { result.success(null); @@ -338,10 +338,23 @@ GeneratedFileSelectorApi.FileResponse toFileResponse(@NonNull Uri uri) { return null; } + // try { + // System.out.println("CAMILLE: " + uri.getPath()); + // System.out.println("CAMILLE: " + DocumentsContract.getDocumentId(uri)); + // Uri newUri = DocumentsContract.buildDocumentUri(uri.getAuthority(), DocumentsContract.getDocumentId(uri)); + // System.out.println("CAMILLE: " + newUri.getPath()); + // final String docUriPath2 = FileUtils.getPathFromUri(activityPluginBinding.getActivity(), newUri); + // System.out.println("CAMILLE: " + docUriPath2); + // // System.out.println("CAMILLE: " + DocumentsContract.findDocumentPath(contentResolver, newUri)); + // // System.out.println("CAMILLE: " + DocumentsContract.findDocumentPath(contentResolver, uri)); + // } catch (Exception e) {System.out.println(" CAMILLE ERROR: " + e.toString());} + // final Uri docUri = DocumentsContract.buildDocumentUri(uri, DocumentsContract.getTreeDocumentId(uri)); + final String docUrIPath = FileUtils.getPathFromUri(activityPluginBinding.getActivity(), uri); + return new GeneratedFileSelectorApi.FileResponse.Builder() .setName(name) .setBytes(bytes) - .setPath(uri.toString()) // TODO(camsim99): Test getDirectoryPath fix for getting actual path here. + .setPath(docUrIPath) .setMimeType(contentResolver.getType(uri)) .setSize(size.longValue()) .build(); diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 61eb48b0cb2..1d3d5830979 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -26,9 +26,16 @@ public class FileUtils { /** URI authority that represents access to external storage providers. */ public static String EXTERNAL_DOCUMENT_AUTHORITY = "com.android.externalstorage.documents"; + /** URI authority that represents a media document. */ + public static String MEDIA_DOCUMENT_AUTHORITY = "com.android.providers.media.documents"; + /** * Retrieves path of directory represented by the specified {@code Uri}. * + * Intended to handle any cases needed to return paths from URIS retrieved from open documents/directories + * by starting one of {@code Intent.ACTION_OPEN_FILE}, {@code Intent.ACTION_OPEN_FILES}, or + * {@code Intent.ACTION_OPEN_DOCUMENT_TREE}. + * *

Will return the path for on-device directories, but does not handle external storage volumes. */ public static String getPathFromUri(Activity activity, Uri uri) { @@ -41,14 +48,29 @@ public static String getPathFromUri(Activity activity, Uri uri) { // Non-primary storage volumes come from SD cards, USB drives, etc. and are // not handled here. if (!documentStorageVolume.equals("primary")) { - throw new UnsupportedOperationException("Retrieving the path of a document from storage volume " + documentStorageVolume + "is unsupported by this plugin."); + throw new UnsupportedOperationException("Retrieving the path of a document from storage volume " + documentStorageVolume + " is unsupported by this plugin."); } String innermostDirectoryName = uriDocumentId.split(":")[1]; String externalStorageDirectory = Environment.getExternalStorageDirectory().getPath(); return externalStorageDirectory + "/" + innermostDirectoryName; + } else if (uriAuthority.equals(MEDIA_DOCUMENT_AUTHORITY)) { + String uriDocumentId = DocumentsContract.getDocumentId(uri); + String documentStorageVolume = uriDocumentId.split(":")[0]; + ContentResolver contentResolver = activity.getContentResolver(); + + // This makes an assumption that the URI has the content scheme, which we can safely + // assume since this method only supports finding paths of URIs retrieved from + // the Intents ACTION_OPEN_FILE(S) and ACTION_OPEN_DOCUMENT_TREE. + Cursor cursor = contentResolver.query(uri, null, null, null, null, null); + + if (cursor != null && cursor.moveToFirst()) { + return cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH)); // TODO(camsim99): probably making assumption here about file type + } else { + throw new IllegalStateException("Was unable to retrieve path from URI " + uri.toString() + " by using Cursor."); + } } else { - throw new UnsupportedOperationException("Retrieving the path from URIs with authority " + uriAuthority.toString() + "are unsupported by this plugin."); + throw new UnsupportedOperationException("Retrieving the path from URIs with authority " + uriAuthority.toString() + " are unsupported by this plugin."); } } } From 79f36fd91d7cf38cba9f9a6134956b2ba17987aa Mon Sep 17 00:00:00 2001 From: camsim99 Date: Fri, 5 Apr 2024 11:06:42 -0700 Subject: [PATCH 04/21] Debugging --- .../file_selector_android/FileSelectorApiImpl.java | 7 ++++--- .../example/android/app/src/main/AndroidManifest.xml | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java index 9d02ed23c00..c2e2aa83b74 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java @@ -338,7 +338,7 @@ GeneratedFileSelectorApi.FileResponse toFileResponse(@NonNull Uri uri) { return null; } - // try { + try { // System.out.println("CAMILLE: " + uri.getPath()); // System.out.println("CAMILLE: " + DocumentsContract.getDocumentId(uri)); // Uri newUri = DocumentsContract.buildDocumentUri(uri.getAuthority(), DocumentsContract.getDocumentId(uri)); @@ -346,8 +346,9 @@ GeneratedFileSelectorApi.FileResponse toFileResponse(@NonNull Uri uri) { // final String docUriPath2 = FileUtils.getPathFromUri(activityPluginBinding.getActivity(), newUri); // System.out.println("CAMILLE: " + docUriPath2); // // System.out.println("CAMILLE: " + DocumentsContract.findDocumentPath(contentResolver, newUri)); - // // System.out.println("CAMILLE: " + DocumentsContract.findDocumentPath(contentResolver, uri)); - // } catch (Exception e) {System.out.println(" CAMILLE ERROR: " + e.toString());} + // final Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri)); + System.out.println("CAMILLE: " + DocumentsContract.findDocumentPath(contentResolver, uri)); + } catch (Exception e) {System.out.println(" CAMILLE ERROR: " + e.toString());} // final Uri docUri = DocumentsContract.buildDocumentUri(uri, DocumentsContract.getTreeDocumentId(uri)); final String docUrIPath = FileUtils.getPathFromUri(activityPluginBinding.getActivity(), uri); diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/AndroidManifest.xml b/packages/file_selector/file_selector_android/example/android/app/src/main/AndroidManifest.xml index ce8443c7bd6..59965197e11 100644 --- a/packages/file_selector/file_selector_android/example/android/app/src/main/AndroidManifest.xml +++ b/packages/file_selector/file_selector_android/example/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,7 @@ + + + Date: Tue, 9 Apr 2024 15:24:32 -0700 Subject: [PATCH 05/21] debugging --- .../packages/file_selector_android/FileSelectorApiImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java index c2e2aa83b74..24eb29cafd8 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java @@ -205,6 +205,7 @@ public void onResult(int resultCode, @Nullable Intent data) { if (resultCode == Activity.RESULT_OK && data != null) { final Uri uri = data.getData(); final Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri)); + System.out.println(docUri); try { final String path = FileUtils.getPathFromUri(activityPluginBinding.getActivity(), docUri); result.success(path); From a9ced0d79051ea9daa5430787d9cbc1192a1b76e Mon Sep 17 00:00:00 2001 From: camsim99 Date: Mon, 15 Apr 2024 10:05:35 -0700 Subject: [PATCH 06/21] Add copy workaround --- .../FileSelectorApiImpl.java | 8 +- .../file_selector_android/FileUtils.java | 119 ++++++++++++++++++ 2 files changed, 123 insertions(+), 4 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java index 24eb29cafd8..a77473485fd 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java @@ -339,7 +339,7 @@ GeneratedFileSelectorApi.FileResponse toFileResponse(@NonNull Uri uri) { return null; } - try { + // try { // System.out.println("CAMILLE: " + uri.getPath()); // System.out.println("CAMILLE: " + DocumentsContract.getDocumentId(uri)); // Uri newUri = DocumentsContract.buildDocumentUri(uri.getAuthority(), DocumentsContract.getDocumentId(uri)); @@ -348,10 +348,10 @@ GeneratedFileSelectorApi.FileResponse toFileResponse(@NonNull Uri uri) { // System.out.println("CAMILLE: " + docUriPath2); // // System.out.println("CAMILLE: " + DocumentsContract.findDocumentPath(contentResolver, newUri)); // final Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri)); - System.out.println("CAMILLE: " + DocumentsContract.findDocumentPath(contentResolver, uri)); - } catch (Exception e) {System.out.println(" CAMILLE ERROR: " + e.toString());} + // System.out.println("CAMILLE: " + DocumentsContract.findDocumentPath(contentResolver, uri)); + // } catch (Exception e) {System.out.println(" CAMILLE ERROR: " + e.toString());} // final Uri docUri = DocumentsContract.buildDocumentUri(uri, DocumentsContract.getTreeDocumentId(uri)); - final String docUrIPath = FileUtils.getPathFromUri(activityPluginBinding.getActivity(), uri); + final String docUrIPath = FileUtils.getPathFromUri2(activityPluginBinding.getActivity(), uri); return new GeneratedFileSelectorApi.FileResponse.Builder() .setName(name) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 1d3d5830979..fd6d00def9b 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -17,9 +17,16 @@ import android.provider.MediaStore; import android.text.TextUtils; +import android.provider.MediaStore; +import android.webkit.MimeTypeMap; +import io.flutter.Log; import java.io.File; import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.util.UUID; + public class FileUtils { @@ -29,6 +36,118 @@ public class FileUtils { /** URI authority that represents a media document. */ public static String MEDIA_DOCUMENT_AUTHORITY = "com.android.providers.media.documents"; + /** + * Copies the file from the given content URI to a temporary directory, retaining the original + * file name if possible. + * + *

Each file is placed in its own directory to avoid conflicts according to the following + * scheme: {cacheDir}/{randomUuid}/{fileName} + * + *

File extension is changed to match MIME type of the file, if known. Otherwise, the extension + * is left unchanged. + * + *

If the original file name is unknown, a predefined "image_picker" filename is used and the + * file extension is deduced from the mime type (with fallback to ".jpg" in case of failure). + */ + public static String getPathFromUri2(final Context context, final Uri uri) { + try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { + String uuid = UUID.randomUUID().toString(); + File targetDirectory = new File(context.getCacheDir(), uuid); + targetDirectory.mkdir(); + // TODO(SynSzakala) according to the docs, `deleteOnExit` does not work reliably on Android; we should preferably + // just clear the picked files after the app startup. + targetDirectory.deleteOnExit(); + String fileName = getImageName(context, uri); + String extension = getImageExtension(context, uri); + + if (fileName == null) { + if (extension == null) { + throw new IllegalArgumentException("CAMILLE: NO EXTENSION FOUND"); + } else { + fileName = "file_selector" + extension; + } + } else if (extension != null) { + fileName = getBaseName(fileName) + extension; + } else { + throw new IllegalArgumentException("CAMILLE: NO EXTENSiON FOUND 2"); + } + + File file = new File(targetDirectory, fileName); + try (OutputStream outputStream = new FileOutputStream(file)) { + copy(inputStream, outputStream); + return file.getPath(); + } + } catch (IOException e) { + // If closing the output stream fails, we cannot be sure that the + // target file was written in full. Flushing the stream merely moves + // the bytes into the OS, not necessarily to the file. + return null; + } catch (SecurityException e) { + // Calling `ContentResolver#openInputStream()` has been reported to throw a + // `SecurityException` on some devices in certain circumstances. Instead of crashing, we + // return `null`. + // + // See https://github.com/flutter/flutter/issues/100025 for more details. + return null; + } + } + + /** @return extension of image with dot, or null if it's empty. */ + private static String getImageExtension(Context context, Uri uriImage) { + String extension; + + try { + if (uriImage.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { + final MimeTypeMap mime = MimeTypeMap.getSingleton(); + extension = mime.getExtensionFromMimeType(context.getContentResolver().getType(uriImage)); + } else { + extension = + MimeTypeMap.getFileExtensionFromUrl( + Uri.fromFile(new File(uriImage.getPath())).toString()); + } + } catch (Exception e) { + return null; + } + + if (extension == null || extension.isEmpty()) { + return null; + } + + return "." + extension; + } + + /** @return name of the image provided by ContentResolver; this may be null. */ + private static String getImageName(Context context, Uri uriImage) { + try (Cursor cursor = queryImageName(context, uriImage)) { + if (cursor == null || !cursor.moveToFirst() || cursor.getColumnCount() < 1) return null; + return cursor.getString(0); + } + } + + private static Cursor queryImageName(Context context, Uri uriImage) { + return context + .getContentResolver() + .query(uriImage, new String[] {MediaStore.MediaColumns.DISPLAY_NAME}, null, null, null); + } + + private static void copy(InputStream in, OutputStream out) throws IOException { + final byte[] buffer = new byte[4 * 1024]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + } + out.flush(); + } + + private static String getBaseName(String fileName) { + int lastDotIndex = fileName.lastIndexOf('.'); + if (lastDotIndex < 0) { + return fileName; + } + // Basename is everything before the last '.'. + return fileName.substring(0, lastDotIndex); + } + /** * Retrieves path of directory represented by the specified {@code Uri}. * From 9cb098cd8e1ccc315d0d806f0efdc3c1c55f98b3 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Mon, 15 Apr 2024 12:16:03 -0700 Subject: [PATCH 07/21] Cleanup --- .../FileSelectorApiImpl.java | 17 +----- .../file_selector_android/FileUtils.java | 56 ++++++++++++------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java index a77473485fd..99e2c97fb99 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java @@ -205,7 +205,6 @@ public void onResult(int resultCode, @Nullable Intent data) { if (resultCode == Activity.RESULT_OK && data != null) { final Uri uri = data.getData(); final Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri)); - System.out.println(docUri); try { final String path = FileUtils.getPathFromUri(activityPluginBinding.getActivity(), docUri); result.success(path); @@ -339,24 +338,12 @@ GeneratedFileSelectorApi.FileResponse toFileResponse(@NonNull Uri uri) { return null; } - // try { - // System.out.println("CAMILLE: " + uri.getPath()); - // System.out.println("CAMILLE: " + DocumentsContract.getDocumentId(uri)); - // Uri newUri = DocumentsContract.buildDocumentUri(uri.getAuthority(), DocumentsContract.getDocumentId(uri)); - // System.out.println("CAMILLE: " + newUri.getPath()); - // final String docUriPath2 = FileUtils.getPathFromUri(activityPluginBinding.getActivity(), newUri); - // System.out.println("CAMILLE: " + docUriPath2); - // // System.out.println("CAMILLE: " + DocumentsContract.findDocumentPath(contentResolver, newUri)); - // final Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri)); - // System.out.println("CAMILLE: " + DocumentsContract.findDocumentPath(contentResolver, uri)); - // } catch (Exception e) {System.out.println(" CAMILLE ERROR: " + e.toString());} - // final Uri docUri = DocumentsContract.buildDocumentUri(uri, DocumentsContract.getTreeDocumentId(uri)); - final String docUrIPath = FileUtils.getPathFromUri2(activityPluginBinding.getActivity(), uri); + final String docUriPath = FileUtils.getPathFromCopyOfFileFromUri(activityPluginBinding.getActivity(), uri); return new GeneratedFileSelectorApi.FileResponse.Builder() .setName(name) .setBytes(bytes) - .setPath(docUrIPath) + .setPath(docUriPath) .setMimeType(contentResolver.getType(uri)) .setSize(size.longValue()) .build(); diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index fd6d00def9b..394343615d3 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -2,6 +2,25 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +/* + * Copyright (C) 2007-2008 OpenIntents.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file was modified by the Flutter authors from the following original file: + * https://raw.githubusercontent.com/iPaulPro/aFileChooser/master/aFileChooser/src/com/ipaulpro/afilechooser/utils/FileUtils.java + */ + package dev.flutter.packages.file_selector_android; import android.annotation.TargetApi; @@ -27,7 +46,6 @@ import java.io.OutputStream; import java.util.UUID; - public class FileUtils { /** URI authority that represents access to external storage providers. */ @@ -36,7 +54,7 @@ public class FileUtils { /** URI authority that represents a media document. */ public static String MEDIA_DOCUMENT_AUTHORITY = "com.android.providers.media.documents"; - /** + /** * Copies the file from the given content URI to a temporary directory, retaining the original * file name if possible. * @@ -46,19 +64,19 @@ public class FileUtils { *

File extension is changed to match MIME type of the file, if known. Otherwise, the extension * is left unchanged. * - *

If the original file name is unknown, a predefined "image_picker" filename is used and the - * file extension is deduced from the mime type (with fallback to ".jpg" in case of failure). + *

If the original file name is unknown, a predefined "file_selector" filename is used and the + * file extension is deduced from the mime type. */ - public static String getPathFromUri2(final Context context, final Uri uri) { + public static String getPathFromCopyOfFileFromUri(final Context context, final Uri uri) { try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { String uuid = UUID.randomUUID().toString(); File targetDirectory = new File(context.getCacheDir(), uuid); targetDirectory.mkdir(); - // TODO(SynSzakala) according to the docs, `deleteOnExit` does not work reliably on Android; we should preferably - // just clear the picked files after the app startup. + // TODO(camsim99): according to the docs, `deleteOnExit` does not work reliably on Android; we should preferably + // just clear the picked files after the app startup. targetDirectory.deleteOnExit(); - String fileName = getImageName(context, uri); - String extension = getImageExtension(context, uri); + String fileName = getFileName(context, uri); + String extension = getFileExtension(context, uri); if (fileName == null) { if (extension == null) { @@ -92,18 +110,18 @@ public static String getPathFromUri2(final Context context, final Uri uri) { } } - /** @return extension of image with dot, or null if it's empty. */ - private static String getImageExtension(Context context, Uri uriImage) { + /** Returns the extension of file with dot, or null if it's empty. */ + private static String getFileExtension(Context context, Uri uriFile) { String extension; try { - if (uriImage.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { + if (uriFile.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { final MimeTypeMap mime = MimeTypeMap.getSingleton(); - extension = mime.getExtensionFromMimeType(context.getContentResolver().getType(uriImage)); + extension = mime.getExtensionFromMimeType(context.getContentResolver().getType(uriFile)); } else { extension = MimeTypeMap.getFileExtensionFromUrl( - Uri.fromFile(new File(uriImage.getPath())).toString()); + Uri.fromFile(new File(uriFile.getPath())).toString()); } } catch (Exception e) { return null; @@ -116,18 +134,18 @@ private static String getImageExtension(Context context, Uri uriImage) { return "." + extension; } - /** @return name of the image provided by ContentResolver; this may be null. */ - private static String getImageName(Context context, Uri uriImage) { - try (Cursor cursor = queryImageName(context, uriImage)) { + /** Returns the name of the file provided by ContentResolver; this may be null. */ + private static String getFileName(Context context, Uri uriFile) { + try (Cursor cursor = queryFileName(context, uriFile)) { if (cursor == null || !cursor.moveToFirst() || cursor.getColumnCount() < 1) return null; return cursor.getString(0); } } - private static Cursor queryImageName(Context context, Uri uriImage) { + private static Cursor queryFileName(Context context, Uri uriFile) { return context .getContentResolver() - .query(uriImage, new String[] {MediaStore.MediaColumns.DISPLAY_NAME}, null, null, null); + .query(uriFile, new String[] {MediaStore.MediaColumns.DISPLAY_NAME}, null, null, null); } private static void copy(InputStream in, OutputStream out) throws IOException { From 30295800e9298b1a1ff9b5d8589164b63dca417a Mon Sep 17 00:00:00 2001 From: camsim99 Date: Mon, 15 Apr 2024 12:27:33 -0700 Subject: [PATCH 08/21] Add todos, test file --- .../file_selector_android/FileUtils.java | 224 +++++++++--------- .../android/app/src/main/AndroidManifest.xml | 1 + 2 files changed, 113 insertions(+), 112 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 394343615d3..d9575f993e9 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -54,118 +54,6 @@ public class FileUtils { /** URI authority that represents a media document. */ public static String MEDIA_DOCUMENT_AUTHORITY = "com.android.providers.media.documents"; - /** - * Copies the file from the given content URI to a temporary directory, retaining the original - * file name if possible. - * - *

Each file is placed in its own directory to avoid conflicts according to the following - * scheme: {cacheDir}/{randomUuid}/{fileName} - * - *

File extension is changed to match MIME type of the file, if known. Otherwise, the extension - * is left unchanged. - * - *

If the original file name is unknown, a predefined "file_selector" filename is used and the - * file extension is deduced from the mime type. - */ - public static String getPathFromCopyOfFileFromUri(final Context context, final Uri uri) { - try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { - String uuid = UUID.randomUUID().toString(); - File targetDirectory = new File(context.getCacheDir(), uuid); - targetDirectory.mkdir(); - // TODO(camsim99): according to the docs, `deleteOnExit` does not work reliably on Android; we should preferably - // just clear the picked files after the app startup. - targetDirectory.deleteOnExit(); - String fileName = getFileName(context, uri); - String extension = getFileExtension(context, uri); - - if (fileName == null) { - if (extension == null) { - throw new IllegalArgumentException("CAMILLE: NO EXTENSION FOUND"); - } else { - fileName = "file_selector" + extension; - } - } else if (extension != null) { - fileName = getBaseName(fileName) + extension; - } else { - throw new IllegalArgumentException("CAMILLE: NO EXTENSiON FOUND 2"); - } - - File file = new File(targetDirectory, fileName); - try (OutputStream outputStream = new FileOutputStream(file)) { - copy(inputStream, outputStream); - return file.getPath(); - } - } catch (IOException e) { - // If closing the output stream fails, we cannot be sure that the - // target file was written in full. Flushing the stream merely moves - // the bytes into the OS, not necessarily to the file. - return null; - } catch (SecurityException e) { - // Calling `ContentResolver#openInputStream()` has been reported to throw a - // `SecurityException` on some devices in certain circumstances. Instead of crashing, we - // return `null`. - // - // See https://github.com/flutter/flutter/issues/100025 for more details. - return null; - } - } - - /** Returns the extension of file with dot, or null if it's empty. */ - private static String getFileExtension(Context context, Uri uriFile) { - String extension; - - try { - if (uriFile.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { - final MimeTypeMap mime = MimeTypeMap.getSingleton(); - extension = mime.getExtensionFromMimeType(context.getContentResolver().getType(uriFile)); - } else { - extension = - MimeTypeMap.getFileExtensionFromUrl( - Uri.fromFile(new File(uriFile.getPath())).toString()); - } - } catch (Exception e) { - return null; - } - - if (extension == null || extension.isEmpty()) { - return null; - } - - return "." + extension; - } - - /** Returns the name of the file provided by ContentResolver; this may be null. */ - private static String getFileName(Context context, Uri uriFile) { - try (Cursor cursor = queryFileName(context, uriFile)) { - if (cursor == null || !cursor.moveToFirst() || cursor.getColumnCount() < 1) return null; - return cursor.getString(0); - } - } - - private static Cursor queryFileName(Context context, Uri uriFile) { - return context - .getContentResolver() - .query(uriFile, new String[] {MediaStore.MediaColumns.DISPLAY_NAME}, null, null, null); - } - - private static void copy(InputStream in, OutputStream out) throws IOException { - final byte[] buffer = new byte[4 * 1024]; - int bytesRead; - while ((bytesRead = in.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); - } - out.flush(); - } - - private static String getBaseName(String fileName) { - int lastDotIndex = fileName.lastIndexOf('.'); - if (lastDotIndex < 0) { - return fileName; - } - // Basename is everything before the last '.'. - return fileName.substring(0, lastDotIndex); - } - /** * Retrieves path of directory represented by the specified {@code Uri}. * @@ -210,4 +98,116 @@ public static String getPathFromUri(Activity activity, Uri uri) { throw new UnsupportedOperationException("Retrieving the path from URIs with authority " + uriAuthority.toString() + " are unsupported by this plugin."); } } + + /** + * Copies the file from the given content URI to a temporary directory, retaining the original + * file name if possible. + * + *

Each file is placed in its own directory to avoid conflicts according to the following + * scheme: {cacheDir}/{randomUuid}/{fileName} + * + *

File extension is changed to match MIME type of the file, if known. Otherwise, the extension + * is left unchanged. + * + *

If the original file name is unknown, a predefined "file_selector" filename is used and the + * file extension is deduced from the mime type. + */ + public static String getPathFromCopyOfFileFromUri(final Context context, final Uri uri) { + try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { + String uuid = UUID.randomUUID().toString(); + File targetDirectory = new File(context.getCacheDir(), uuid); + targetDirectory.mkdir(); + // TODO(camsim99): according to the docs, `deleteOnExit` does not work reliably on Android; we should preferably + // just clear the picked files after the app startup. + targetDirectory.deleteOnExit(); + String fileName = getFileName(context, uri); + String extension = getFileExtension(context, uri); + + if (fileName == null) { + if (extension == null) { + throw new IllegalArgumentException("CAMILLE: NO EXTENSION FOUND"); + } else { + fileName = "file_selector" + extension; + } + } else if (extension != null) { + fileName = getBaseName(fileName) + extension; + } else { + throw new IllegalArgumentException("CAMILLE: NO EXTENSiON FOUND 2"); + } + + File file = new File(targetDirectory, fileName); + try (OutputStream outputStream = new FileOutputStream(file)) { + copy(inputStream, outputStream); + return file.getPath(); + } + } catch (IOException e) { + // If closing the output stream fails, we cannot be sure that the + // target file was written in full. Flushing the stream merely moves + // the bytes into the OS, not necessarily to the file. + return null; + } catch (SecurityException e) { + // Calling `ContentResolver#openInputStream()` has been reported to throw a + // `SecurityException` on some devices in certain circumstances. Instead of crashing, we + // return `null`. + // + // See https://github.com/flutter/flutter/issues/100025 for more details. + return null; + } + } + + /** Returns the extension of file with dot, or null if it's empty. */ + private static String getFileExtension(Context context, Uri uriFile) { + String extension; + + try { + if (uriFile.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { + final MimeTypeMap mime = MimeTypeMap.getSingleton(); + extension = mime.getExtensionFromMimeType(context.getContentResolver().getType(uriFile)); + } else { + extension = + MimeTypeMap.getFileExtensionFromUrl( + Uri.fromFile(new File(uriFile.getPath())).toString()); + } + } catch (Exception e) { + return null; + } + + if (extension == null || extension.isEmpty()) { + return null; + } + + return "." + extension; + } + + /** Returns the name of the file provided by ContentResolver; this may be null. */ + private static String getFileName(Context context, Uri uriFile) { + try (Cursor cursor = queryFileName(context, uriFile)) { + if (cursor == null || !cursor.moveToFirst() || cursor.getColumnCount() < 1) return null; + return cursor.getString(0); + } + } + + private static Cursor queryFileName(Context context, Uri uriFile) { + return context + .getContentResolver() + .query(uriFile, new String[] {MediaStore.MediaColumns.DISPLAY_NAME}, null, null, null); + } + + private static void copy(InputStream in, OutputStream out) throws IOException { + final byte[] buffer = new byte[4 * 1024]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + } + out.flush(); + } + + private static String getBaseName(String fileName) { + int lastDotIndex = fileName.lastIndexOf('.'); + if (lastDotIndex < 0) { + return fileName; + } + // Basename is everything before the last '.'. + return fileName.substring(0, lastDotIndex); + } } diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/AndroidManifest.xml b/packages/file_selector/file_selector_android/example/android/app/src/main/AndroidManifest.xml index 59965197e11..2487f9d375b 100644 --- a/packages/file_selector/file_selector_android/example/android/app/src/main/AndroidManifest.xml +++ b/packages/file_selector/file_selector_android/example/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ + From 58e37d2d5baac335862122ddc3ee8f5a6064a1a2 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Tue, 16 Apr 2024 10:25:20 -0700 Subject: [PATCH 09/21] Remove permissions --- .../example/android/app/src/main/AndroidManifest.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/AndroidManifest.xml b/packages/file_selector/file_selector_android/example/android/app/src/main/AndroidManifest.xml index 2487f9d375b..ce8443c7bd6 100644 --- a/packages/file_selector/file_selector_android/example/android/app/src/main/AndroidManifest.xml +++ b/packages/file_selector/file_selector_android/example/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,4 @@ - - - - Date: Tue, 16 Apr 2024 13:48:48 -0700 Subject: [PATCH 10/21] Adding tests --- .../FileSelectorApiImpl.java | 6 +- .../file_selector_android/FileUtils.java | 18 +- .../FileSelectorAndroidPluginTest.java | 2 +- .../file_selector_android/FileUtilsTest.java | 236 ++++++++++++++++++ .../FileSelectorAndroidTest.java | 4 +- 5 files changed, 252 insertions(+), 14 deletions(-) create mode 100644 packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java index 99e2c97fb99..aa513b16206 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java @@ -204,6 +204,7 @@ public void getDirectoryPath( public void onResult(int resultCode, @Nullable Intent data) { if (resultCode == Activity.RESULT_OK && data != null) { final Uri uri = data.getData(); + System.out.println(uri); final Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri)); try { final String path = FileUtils.getPathFromUri(activityPluginBinding.getActivity(), docUri); @@ -338,12 +339,13 @@ GeneratedFileSelectorApi.FileResponse toFileResponse(@NonNull Uri uri) { return null; } - final String docUriPath = FileUtils.getPathFromCopyOfFileFromUri(activityPluginBinding.getActivity(), uri); + System.out.println(uri); + final String uriPath = FileUtils.getPathFromCopyOfFileFromUri(activityPluginBinding.getActivity(), uri); return new GeneratedFileSelectorApi.FileResponse.Builder() .setName(name) .setBytes(bytes) - .setPath(docUriPath) + .setPath(uriPath) .setMimeType(contentResolver.getType(uri)) .setSize(size.longValue()) .build(); diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index d9575f993e9..8045a1bd26c 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -24,7 +24,6 @@ package dev.flutter.packages.file_selector_android; import android.annotation.TargetApi; -import android.app.Activity; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; @@ -35,7 +34,6 @@ import android.provider.DocumentsContract; import android.provider.MediaStore; import android.text.TextUtils; - import android.provider.MediaStore; import android.webkit.MimeTypeMap; import io.flutter.Log; @@ -63,7 +61,7 @@ public class FileUtils { * *

Will return the path for on-device directories, but does not handle external storage volumes. */ - public static String getPathFromUri(Activity activity, Uri uri) { + public static String getPathFromUri(Context context, Uri uri) { String uriAuthority = uri.getAuthority(); if (uriAuthority.equals(EXTERNAL_DOCUMENT_AUTHORITY)) { @@ -82,7 +80,7 @@ public static String getPathFromUri(Activity activity, Uri uri) { } else if (uriAuthority.equals(MEDIA_DOCUMENT_AUTHORITY)) { String uriDocumentId = DocumentsContract.getDocumentId(uri); String documentStorageVolume = uriDocumentId.split(":")[0]; - ContentResolver contentResolver = activity.getContentResolver(); + ContentResolver contentResolver = context.getContentResolver(); // This makes an assumption that the URI has the content scheme, which we can safely // assume since this method only supports finding paths of URIs retrieved from @@ -90,12 +88,13 @@ public static String getPathFromUri(Activity activity, Uri uri) { Cursor cursor = contentResolver.query(uri, null, null, null, null, null); if (cursor != null && cursor.moveToFirst()) { - return cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH)); // TODO(camsim99): probably making assumption here about file type + return cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH)); } else { - throw new IllegalStateException("Was unable to retrieve path from URI " + uri.toString() + " by using Cursor."); + // Unable to retrieve path of file using cursor. + return null; } } else { - throw new UnsupportedOperationException("Retrieving the path from URIs with authority " + uriAuthority.toString() + " are unsupported by this plugin."); + throw new UnsupportedOperationException("Retrieving the path from URIs with authority " + uriAuthority.toString() + " is unsupported by this plugin."); } } @@ -125,14 +124,15 @@ public static String getPathFromCopyOfFileFromUri(final Context context, final U if (fileName == null) { if (extension == null) { - throw new IllegalArgumentException("CAMILLE: NO EXTENSION FOUND"); + throw new IllegalArgumentException("No name nor extension found for file."); } else { fileName = "file_selector" + extension; } } else if (extension != null) { fileName = getBaseName(fileName) + extension; } else { - throw new IllegalArgumentException("CAMILLE: NO EXTENSiON FOUND 2"); + // TODO: check this logic + throw new IllegalArgumentException("Unable to determine name or extension for file."); } File file = new File(targetDirectory, fileName); diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java index 63dad80954a..303e3981256 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java @@ -190,7 +190,7 @@ public void openFilesReturnsSuccessfully() throws FileNotFoundException { @Test public void getDirectoryPathReturnsSuccessfully() { final Uri mockUri = mock(Uri.class); - when(mockUri.toString()).thenReturn("some/path/"); + when(mockUri.toString()).thenReturn("some/path/"); //TODO: change this when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT_TREE)).thenReturn(mockIntent); when(mockActivityBinding.getActivity()).thenReturn(mockActivity); diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java new file mode 100644 index 00000000000..f88f17f22d6 --- /dev/null +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java @@ -0,0 +1,236 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.file_selector_android; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; + +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.provider.MediaStore; +import android.webkit.MimeTypeMap; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowContentResolver; +import org.robolectric.shadows.ShadowEnvironment; +import org.robolectric.shadows.ShadowMimeTypeMap; + +@RunWith(RobolectricTestRunner.class) +public class FileUtilTest { + + private Context context; + final private String externalStorageDirectoryPath = ShadowEnvironment.getExternalStorageDirectory(); + ShadowContentResolver shadowContentResolver; + + @Before + public void before() { + context = ApplicationProvider.getApplicationContext(); + shadowContentResolver = shadowOf(context.getContentResolver()); + ShadowMimeTypeMap mimeTypeMap = shadowOf(MimeTypeMap.getSingleton()); + mimeTypeMap.addExtensionMimeTypMapping("txt", "document/txt"); + mimeTypeMap.addExtensionMimeTypMapping("jpg", "image/jpeg"); + mimeTypeMap.addExtensionMimeTypMapping("png", "image/png"); + mimeTypeMap.addExtensionMimeTypMapping("webp", "image/webp"); + } + + @Test + public void getPathFromUri_returnsExpectedPathForExternalDocumentUri() { + Uri uri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3ADocuments%2Ftest"); + String path = FileUtils.getPathFromUri(context, uri); + String expectedPath = externalStorageDirectoryPath + "document.txt"; + assertEquals(path, expectedPath); + } + + @Test + public void getPathFromUri_returnsExpectedPathForMediaDocumentUri() { + Uri uri = Uri.parse("content://com.android.providers.media.documents/document/image:1"); + String path = FileUtils.getPathFromUri(context, uri); + String expectedPath = ""; // TODO + assertEquals(path, expectedPath); + } + + @Test + public void getPathFromUri_throwExceptionForExternalDocumentUriWithNonPrimaryStorageVolume() { + Uri uri = Uri.parse("content://com.android.externalstorage.documents/tree/emulated%3ADocuments%2Ftest"); + assertThrows(UnsupportedOperationException.class, () -> FileUtils.getPathFromUri(context, uri)); + } + + @Test + public void getPathFromUri_throwExceptionForUriWithUnhandledAuthority() { + Uri uri = Uri.parse("content://com.unsupported.authority/tree/primary%3ADocuments%2Ftest"); + assertThrows(UnsupportedOperationException.class, () -> FileUtils.getPathFromUri(context, uri)); + } + + @Test + public void getPathFromCopyOfFileFromUri_returnsPathWithContent() throws IOException { + Uri uri = Uri.parse("content://dummy/dummy.png"); + shadowContentResolver.registerInputStream( + uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); + String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); + File file = new File(path); + int size = (int) file.length(); + byte[] bytes = new byte[size]; + + BufferedInputStream buf = new BufferedInputStream(new FileInputStream(file)); + buf.read(bytes, 0, bytes.length); + buf.close(); + + assertTrue(bytes.length > 0); + String fileStream = new String(bytes, UTF_8); + assertEquals("fileStream", fileStream); + } + + @Test + public void getPathFromCopyOfFileFromUri_returnsNullPathWhenSecurityExceptionThrown() throws IOException { + Uri uri = Uri.parse("content://dummy/dummy.png"); + + ContentResolver mockContentResolver = mock(ContentResolver.class); + when(mockContentResolver.openInputStream(any(Uri.class))).thenThrow(SecurityException.class); + + Context mockContext = mock(Context.class); + when(mockContext.getContentResolver()).thenReturn(mockContentResolver); + + String path = fileUtils.getPathFromCopyOfFileFromUri(mockContext, uri); + + assertNull(path); + } + + @Test + public void getFileExtension_returnsExpectedTextFileExtension() throws IOException { + Uri uri = Uri.parse("content://document/dummy.txt"); + shadowContentResolver.registerInputStream( + uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); + String path = fileUtils.getPathFromCopyOfFileFromUri(context, uri); + assertTrue(path.endsWith(".txt")); + } + + @Test + public void getFileExtension_returnsExpectedImageExtension() throws IOException { + Uri uri = Uri.parse("content://dummy/dummy.png"); + shadowContentResolver.registerInputStream( + uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); + String path = fileUtils.getPathFromCopyOfFileFromUri(context, uri); + assertTrue(path.endsWith(".jpg")); + } + + @Test + public void getFileName_returnsExpectedName() throws IOException { + Uri uri = MockContentProvider.PNG_URI; + Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); + shadowContentResolver.registerInputStream( + uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); + String path = fileUtils.getPathFromCopyOfFileFromUri(context, uri); + assertTrue(path.endsWith("a.b.png")); + } + + @Test + public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithNoExtensionInBaseName() throws IOException { + Uri uri = MockContentProvider.NO_EXTENSION_URI; + Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); + shadowContentResolver.registerInputStream( + uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); + String path = fileUtils.getPathFromCopyOfFileFromUri(context, uri); + assertTrue(path.endsWith("abc.png")); + } + + @Test + public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithMismatchedTypeToFile() throws IOException { + Uri uri = MockContentProvider.WEBP_URI; + Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); + shadowContentResolver.registerInputStream( + uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); + String path = fileUtils.getPathFromCopyOfFileFromUri(context, uri); + assertTrue(path.endsWith("c.d.webp")); + } + + @Test + public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithUnknownType() throws IOException { + Uri uri = MockContentProvider.UNKNOWN_URI; + Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); + shadowContentResolver.registerInputStream( + uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); + String path = fileUtils.getPathFromCopyOfFileFromUri(context, uri); + assertTrue(path.endsWith("e.f.g")); + } + + private static class MockContentProvider extends ContentProvider { + public static final Uri TXT_URI = Uri.parse("content://document/dummy.txt"); + public static final Uri PNG_URI = Uri.parse("content://dummy/a.b.png"); + public static final Uri WEBP_URI = Uri.parse("content://dummy/c.d.png"); + public static final Uri UNKNOWN_URI = Uri.parse("content://dummy/e.f.g"); + public static final Uri NO_EXTENSION_URI = Uri.parse("content://dummy/abc"); + + @Override + public boolean onCreate() { + return true; + } + + @Nullable + @Override + public Cursor query( + @NonNull Uri uri, + @Nullable String[] projection, + @Nullable String selection, + @Nullable String[] selectionArgs, + @Nullable String sortOrder) { + MatrixCursor cursor = new MatrixCursor(new String[] {MediaStore.MediaColumns.DISPLAY_NAME}); + cursor.addRow(new Object[] {uri.getLastPathSegment()}); + return cursor; + } + + @Nullable + @Override + public String getType(@NonNull Uri uri) { + if (uri.equals(TXT_URI)) return "document/txt"; + if (uri.equals(PNG_URI)) return "image/png"; + if (uri.equals(WEBP_URI)) return "image/webp"; + if (uri.equals(NO_EXTENSION_URI)) return "image/png"; + return null; + } + + @Nullable + @Override + public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { + return null; + } + + @Override + public int delete( + @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { + return 0; + } + + @Override + public int update( + @NonNull Uri uri, + @Nullable ContentValues values, + @Nullable String selection, + @Nullable String[] selectionArgs) { + return 0; + } + } +} diff --git a/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java b/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java index b464d10c249..60e8b28e980 100644 --- a/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java +++ b/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java @@ -60,7 +60,7 @@ public void openImageFile() { onFlutterWidget(withText("Press to open an image file(png, jpg)")).perform(click()); intended(hasAction(Intent.ACTION_OPEN_DOCUMENT)); onFlutterWidget(withValueKey("result_image_name")) - .check(matches(withText("content://file_selector_android_test/dummy.png"))); + .check(matches(withText("content://file_selector_android_test/dummy.png"))); // TODO: change this } @Test @@ -68,7 +68,7 @@ public void openImageFiles() { clearAnySystemDialog(); final ClipData.Item clipDataItem = - new ClipData.Item(Uri.parse("content://file_selector_android_test/dummy.png")); + new ClipData.Item(Uri.parse("content://file_selector_android_test/dummy.png")); // TODO: change this final ClipData clipData = new ClipData("", new String[0], clipDataItem); clipData.addItem(clipDataItem); From adfc0e77a3058dc7d62ed9a800e9a941518734c7 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Wed, 17 Apr 2024 08:44:20 -0700 Subject: [PATCH 11/21] Adding tests --- .../android/build.gradle | 1 + .../file_selector_android/FileUtils.java | 56 ++- .../FileSelectorAndroidPluginTest.java | 468 +++++++++--------- .../file_selector_android/FileUtilsTest.java | 261 +++++++--- .../FileSelectorAndroidTest.java | 150 +++--- .../plugins/imagepicker/FileUtilTest.java | 1 + 6 files changed, 526 insertions(+), 411 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/build.gradle b/packages/file_selector/file_selector_android/android/build.gradle index 98a0a63deca..2132f6b1a15 100644 --- a/packages/file_selector/file_selector_android/android/build.gradle +++ b/packages/file_selector/file_selector_android/android/build.gradle @@ -42,6 +42,7 @@ android { testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-inline:5.1.0' testImplementation 'androidx.test:core:1.3.0' + testImplementation "org.robolectric:robolectric:4.10.3" // org.jetbrains.kotlin:kotlin-bom artifact purpose is to align kotlin stdlib and related code versions. // See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7 diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 8045a1bd26c..a8efe35c8c5 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -62,38 +62,53 @@ public class FileUtils { *

Will return the path for on-device directories, but does not handle external storage volumes. */ public static String getPathFromUri(Context context, Uri uri) { + System.out.println("CAMILLE AA"); + String uriAuthority = uri.getAuthority(); + System.out.println("CAMILLE AB"); if (uriAuthority.equals(EXTERNAL_DOCUMENT_AUTHORITY)) { + System.out.println("CAMILLE AC"); + String uriDocumentId = DocumentsContract.getDocumentId(uri); + System.out.println(uriDocumentId); + System.out.println("CAMILLE AD"); + String documentStorageVolume = uriDocumentId.split(":")[0]; + System.out.println("CAMILLE A"); // Non-primary storage volumes come from SD cards, USB drives, etc. and are // not handled here. if (!documentStorageVolume.equals("primary")) { + System.out.println("CAMILLE B"); + throw new UnsupportedOperationException("Retrieving the path of a document from storage volume " + documentStorageVolume + " is unsupported by this plugin."); } String innermostDirectoryName = uriDocumentId.split(":")[1]; String externalStorageDirectory = Environment.getExternalStorageDirectory().getPath(); + System.out.println("CAMILLE C"); return externalStorageDirectory + "/" + innermostDirectoryName; - } else if (uriAuthority.equals(MEDIA_DOCUMENT_AUTHORITY)) { - String uriDocumentId = DocumentsContract.getDocumentId(uri); - String documentStorageVolume = uriDocumentId.split(":")[0]; - ContentResolver contentResolver = context.getContentResolver(); - - // This makes an assumption that the URI has the content scheme, which we can safely - // assume since this method only supports finding paths of URIs retrieved from - // the Intents ACTION_OPEN_FILE(S) and ACTION_OPEN_DOCUMENT_TREE. - Cursor cursor = contentResolver.query(uri, null, null, null, null, null); - - if (cursor != null && cursor.moveToFirst()) { - return cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH)); - } else { - // Unable to retrieve path of file using cursor. - return null; - } - } else { + } + // } else if (uriAuthority.equals(MEDIA_DOCUMENT_AUTHORITY)) { + // System.out.println("HERE!!!!!11"); + // String uriDocumentId = DocumentsContract.getDocumentId(uri); + // String documentStorageVolume = uriDocumentId.split(":")[0]; + // ContentResolver contentResolver = context.getContentResolver(); + + // // This makes an assumption that the URI has the content scheme, which we can safely + // // assume since this method only supports finding paths of URIs retrieved from + // // the Intents ACTION_OPEN_FILE(S) and ACTION_OPEN_DOCUMENT_TREE. + // Cursor cursor = contentResolver.query(uri, null, null, null, null, null); + + // if (cursor != null && cursor.moveToFirst()) { + // return cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH)); + // } else { + // // Unable to retrieve path of file using cursor. + // return null; + // } + // } + else { throw new UnsupportedOperationException("Retrieving the path from URIs with authority " + uriAuthority.toString() + " is unsupported by this plugin."); } } @@ -130,9 +145,6 @@ public static String getPathFromCopyOfFileFromUri(final Context context, final U } } else if (extension != null) { fileName = getBaseName(fileName) + extension; - } else { - // TODO: check this logic - throw new IllegalArgumentException("Unable to determine name or extension for file."); } File file = new File(targetDirectory, fileName); @@ -161,9 +173,13 @@ private static String getFileExtension(Context context, Uri uriFile) { try { if (uriFile.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { + System.out.println("CAMILLE 1"); final MimeTypeMap mime = MimeTypeMap.getSingleton(); + System.out.println(uriFile); + System.out.println(context.getContentResolver().getType(uriFile)); extension = mime.getExtensionFromMimeType(context.getContentResolver().getType(uriFile)); } else { + System.out.println("CAMILLE 2"); extension = MimeTypeMap.getFileExtensionFromUrl( Uri.fromFile(new File(uriFile.getPath())).toString()); diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java index 303e3981256..030f025a687 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java @@ -1,234 +1,234 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package dev.flutter.packages.file_selector_android; - -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.app.Activity; -import android.content.ClipData; -import android.content.ContentResolver; -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.provider.OpenableColumns; -import androidx.annotation.NonNull; -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.PluginRegistry; -import java.io.DataInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.util.Collections; -import java.util.List; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; - -public class FileSelectorAndroidPluginTest { - @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); - - @Mock public Intent mockIntent; - - @Mock public Activity mockActivity; - - @Mock FileSelectorApiImpl.NativeObjectFactory mockObjectFactory; - - @Mock public ActivityPluginBinding mockActivityBinding; - - private void mockContentResolver( - @NonNull ContentResolver mockResolver, - @NonNull Uri uri, - @NonNull String displayName, - int size, - @NonNull String mimeType) - throws FileNotFoundException { - final Cursor mockCursor = mock(Cursor.class); - when(mockCursor.moveToFirst()).thenReturn(true); - - when(mockCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)).thenReturn(0); - when(mockCursor.getString(0)).thenReturn(displayName); - - when(mockCursor.getColumnIndex(OpenableColumns.SIZE)).thenReturn(1); - when(mockCursor.isNull(1)).thenReturn(false); - when(mockCursor.getInt(1)).thenReturn(size); - - when(mockResolver.query(uri, null, null, null, null, null)).thenReturn(mockCursor); - when(mockResolver.getType(uri)).thenReturn(mimeType); - when(mockResolver.openInputStream(uri)).thenReturn(mock(InputStream.class)); - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - @Test - public void openFileReturnsSuccessfully() throws FileNotFoundException { - final ContentResolver mockContentResolver = mock(ContentResolver.class); - - final Uri mockUri = mock(Uri.class); - when(mockUri.toString()).thenReturn("some/path/"); - mockContentResolver(mockContentResolver, mockUri, "filename", 30, "text/plain"); - - when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); - when(mockObjectFactory.newDataInputStream(any())).thenReturn(mock(DataInputStream.class)); - when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); - when(mockActivityBinding.getActivity()).thenReturn(mockActivity); - final FileSelectorApiImpl fileSelectorApi = - new FileSelectorApiImpl( - mockActivityBinding, mockObjectFactory, (version) -> Build.VERSION.SDK_INT >= version); - - final GeneratedFileSelectorApi.Result mockResult = mock(GeneratedFileSelectorApi.Result.class); - fileSelectorApi.openFile( - null, - new GeneratedFileSelectorApi.FileTypes.Builder() - .setMimeTypes(Collections.emptyList()) - .setExtensions(Collections.emptyList()) - .build(), - mockResult); - verify(mockIntent).addCategory(Intent.CATEGORY_OPENABLE); - - verify(mockActivity).startActivityForResult(mockIntent, 221); - - final ArgumentCaptor listenerArgumentCaptor = - ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); - verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); - - final Intent resultMockIntent = mock(Intent.class); - when(resultMockIntent.getData()).thenReturn(mockUri); - listenerArgumentCaptor.getValue().onActivityResult(221, Activity.RESULT_OK, resultMockIntent); - - final ArgumentCaptor fileCaptor = - ArgumentCaptor.forClass(GeneratedFileSelectorApi.FileResponse.class); - verify(mockResult).success(fileCaptor.capture()); - - final GeneratedFileSelectorApi.FileResponse file = fileCaptor.getValue(); - assertEquals(file.getBytes().length, 30); - assertEquals(file.getMimeType(), "text/plain"); - assertEquals(file.getName(), "filename"); - assertEquals(file.getSize(), (Long) 30L); - assertEquals(file.getPath(), "some/path/"); - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - @Test - public void openFilesReturnsSuccessfully() throws FileNotFoundException { - final ContentResolver mockContentResolver = mock(ContentResolver.class); - - final Uri mockUri = mock(Uri.class); - when(mockUri.toString()).thenReturn("some/path/"); - mockContentResolver(mockContentResolver, mockUri, "filename", 30, "text/plain"); - - final Uri mockUri2 = mock(Uri.class); - when(mockUri2.toString()).thenReturn("some/other/path/"); - mockContentResolver(mockContentResolver, mockUri2, "filename2", 40, "image/jpg"); - - when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); - when(mockObjectFactory.newDataInputStream(any())).thenReturn(mock(DataInputStream.class)); - when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); - when(mockActivityBinding.getActivity()).thenReturn(mockActivity); - final FileSelectorApiImpl fileSelectorApi = - new FileSelectorApiImpl( - mockActivityBinding, mockObjectFactory, (version) -> Build.VERSION.SDK_INT >= version); - - final GeneratedFileSelectorApi.Result mockResult = mock(GeneratedFileSelectorApi.Result.class); - fileSelectorApi.openFiles( - null, - new GeneratedFileSelectorApi.FileTypes.Builder() - .setMimeTypes(Collections.emptyList()) - .setExtensions(Collections.emptyList()) - .build(), - mockResult); - verify(mockIntent).addCategory(Intent.CATEGORY_OPENABLE); - verify(mockIntent).putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); - - verify(mockActivity).startActivityForResult(mockIntent, 222); - - final ArgumentCaptor listenerArgumentCaptor = - ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); - verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); - - final Intent resultMockIntent = mock(Intent.class); - final ClipData mockClipData = mock(ClipData.class); - when(mockClipData.getItemCount()).thenReturn(2); - - final ClipData.Item mockClipDataItem = mock(ClipData.Item.class); - when(mockClipDataItem.getUri()).thenReturn(mockUri); - when(mockClipData.getItemAt(0)).thenReturn(mockClipDataItem); - - final ClipData.Item mockClipDataItem2 = mock(ClipData.Item.class); - when(mockClipDataItem2.getUri()).thenReturn(mockUri2); - when(mockClipData.getItemAt(1)).thenReturn(mockClipDataItem2); - - when(resultMockIntent.getClipData()).thenReturn(mockClipData); - - listenerArgumentCaptor.getValue().onActivityResult(222, Activity.RESULT_OK, resultMockIntent); - - final ArgumentCaptor fileListCaptor = ArgumentCaptor.forClass(List.class); - verify(mockResult).success(fileListCaptor.capture()); - - final List fileList = fileListCaptor.getValue(); - assertEquals(fileList.get(0).getBytes().length, 30); - assertEquals(fileList.get(0).getMimeType(), "text/plain"); - assertEquals(fileList.get(0).getName(), "filename"); - assertEquals(fileList.get(0).getSize(), (Long) 30L); - assertEquals(fileList.get(0).getPath(), "some/path/"); - - assertEquals(fileList.get(1).getBytes().length, 40); - assertEquals(fileList.get(1).getMimeType(), "image/jpg"); - assertEquals(fileList.get(1).getName(), "filename2"); - assertEquals(fileList.get(1).getSize(), (Long) 40L); - assertEquals(fileList.get(1).getPath(), "some/other/path/"); - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - @Test - public void getDirectoryPathReturnsSuccessfully() { - final Uri mockUri = mock(Uri.class); - when(mockUri.toString()).thenReturn("some/path/"); //TODO: change this - - when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT_TREE)).thenReturn(mockIntent); - when(mockActivityBinding.getActivity()).thenReturn(mockActivity); - final FileSelectorApiImpl fileSelectorApi = - new FileSelectorApiImpl( - mockActivityBinding, - mockObjectFactory, - (version) -> Build.VERSION_CODES.LOLLIPOP >= version); - - final GeneratedFileSelectorApi.Result mockResult = mock(GeneratedFileSelectorApi.Result.class); - fileSelectorApi.getDirectoryPath(null, mockResult); - - verify(mockActivity).startActivityForResult(mockIntent, 223); - - final ArgumentCaptor listenerArgumentCaptor = - ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); - verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); - - final Intent resultMockIntent = mock(Intent.class); - when(resultMockIntent.getData()).thenReturn(mockUri); - listenerArgumentCaptor.getValue().onActivityResult(223, Activity.RESULT_OK, resultMockIntent); - - verify(mockResult).success("some/path/"); - } - - @Test - public void getDirectoryPath_errorsForUnsupportedVersion() { - final FileSelectorApiImpl fileSelectorApi = - new FileSelectorApiImpl( - mockActivityBinding, - mockObjectFactory, - (version) -> Build.VERSION_CODES.KITKAT >= version); - - @SuppressWarnings("unchecked") - final GeneratedFileSelectorApi.Result mockResult = - mock(GeneratedFileSelectorApi.Result.class); - fileSelectorApi.getDirectoryPath(null, mockResult); - - verify(mockResult).error(any()); - } -} +// // Copyright 2013 The Flutter Authors. All rights reserved. +// // Use of this source code is governed by a BSD-style license that can be +// // found in the LICENSE file. + +// package dev.flutter.packages.file_selector_android; + +// import static org.junit.Assert.assertEquals; +// import static org.mockito.ArgumentMatchers.any; +// import static org.mockito.Mockito.mock; +// import static org.mockito.Mockito.verify; +// import static org.mockito.Mockito.when; + +// import android.app.Activity; +// import android.content.ClipData; +// import android.content.ContentResolver; +// import android.content.Intent; +// import android.database.Cursor; +// import android.net.Uri; +// import android.os.Build; +// import android.provider.OpenableColumns; +// import androidx.annotation.NonNull; +// import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +// import io.flutter.plugin.common.PluginRegistry; +// import java.io.DataInputStream; +// import java.io.FileNotFoundException; +// import java.io.InputStream; +// import java.util.Collections; +// import java.util.List; +// import org.junit.Rule; +// import org.junit.Test; +// import org.mockito.ArgumentCaptor; +// import org.mockito.Mock; +// import org.mockito.junit.MockitoJUnit; +// import org.mockito.junit.MockitoRule; + +// public class FileSelectorAndroidPluginTest { +// @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + +// @Mock public Intent mockIntent; + +// @Mock public Activity mockActivity; + +// @Mock FileSelectorApiImpl.NativeObjectFactory mockObjectFactory; + +// @Mock public ActivityPluginBinding mockActivityBinding; + +// private void mockContentResolver( +// @NonNull ContentResolver mockResolver, +// @NonNull Uri uri, +// @NonNull String displayName, +// int size, +// @NonNull String mimeType) +// throws FileNotFoundException { +// final Cursor mockCursor = mock(Cursor.class); +// when(mockCursor.moveToFirst()).thenReturn(true); + +// when(mockCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)).thenReturn(0); +// when(mockCursor.getString(0)).thenReturn(displayName); + +// when(mockCursor.getColumnIndex(OpenableColumns.SIZE)).thenReturn(1); +// when(mockCursor.isNull(1)).thenReturn(false); +// when(mockCursor.getInt(1)).thenReturn(size); + +// when(mockResolver.query(uri, null, null, null, null, null)).thenReturn(mockCursor); +// when(mockResolver.getType(uri)).thenReturn(mimeType); +// when(mockResolver.openInputStream(uri)).thenReturn(mock(InputStream.class)); +// } + +// @SuppressWarnings({"rawtypes", "unchecked"}) +// @Test +// public void openFileReturnsSuccessfully() throws FileNotFoundException { +// final ContentResolver mockContentResolver = mock(ContentResolver.class); + +// final Uri mockUri = mock(Uri.class); +// when(mockUri.toString()).thenReturn("some/path/"); +// mockContentResolver(mockContentResolver, mockUri, "filename", 30, "text/plain"); + +// when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); +// when(mockObjectFactory.newDataInputStream(any())).thenReturn(mock(DataInputStream.class)); +// when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); +// when(mockActivityBinding.getActivity()).thenReturn(mockActivity); +// final FileSelectorApiImpl fileSelectorApi = +// new FileSelectorApiImpl( +// mockActivityBinding, mockObjectFactory, (version) -> Build.VERSION.SDK_INT >= version); + +// final GeneratedFileSelectorApi.Result mockResult = mock(GeneratedFileSelectorApi.Result.class); +// fileSelectorApi.openFile( +// null, +// new GeneratedFileSelectorApi.FileTypes.Builder() +// .setMimeTypes(Collections.emptyList()) +// .setExtensions(Collections.emptyList()) +// .build(), +// mockResult); +// verify(mockIntent).addCategory(Intent.CATEGORY_OPENABLE); + +// verify(mockActivity).startActivityForResult(mockIntent, 221); + +// final ArgumentCaptor listenerArgumentCaptor = +// ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); +// verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); + +// final Intent resultMockIntent = mock(Intent.class); +// when(resultMockIntent.getData()).thenReturn(mockUri); +// listenerArgumentCaptor.getValue().onActivityResult(221, Activity.RESULT_OK, resultMockIntent); + +// final ArgumentCaptor fileCaptor = +// ArgumentCaptor.forClass(GeneratedFileSelectorApi.FileResponse.class); +// verify(mockResult).success(fileCaptor.capture()); + +// final GeneratedFileSelectorApi.FileResponse file = fileCaptor.getValue(); +// assertEquals(file.getBytes().length, 30); +// assertEquals(file.getMimeType(), "text/plain"); +// assertEquals(file.getName(), "filename"); +// assertEquals(file.getSize(), (Long) 30L); +// assertEquals(file.getPath(), "some/path/"); +// } + +// @SuppressWarnings({"rawtypes", "unchecked"}) +// @Test +// public void openFilesReturnsSuccessfully() throws FileNotFoundException { +// final ContentResolver mockContentResolver = mock(ContentResolver.class); + +// final Uri mockUri = mock(Uri.class); +// when(mockUri.toString()).thenReturn("some/path/"); +// mockContentResolver(mockContentResolver, mockUri, "filename", 30, "text/plain"); + +// final Uri mockUri2 = mock(Uri.class); +// when(mockUri2.toString()).thenReturn("some/other/path/"); +// mockContentResolver(mockContentResolver, mockUri2, "filename2", 40, "image/jpg"); + +// when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); +// when(mockObjectFactory.newDataInputStream(any())).thenReturn(mock(DataInputStream.class)); +// when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); +// when(mockActivityBinding.getActivity()).thenReturn(mockActivity); +// final FileSelectorApiImpl fileSelectorApi = +// new FileSelectorApiImpl( +// mockActivityBinding, mockObjectFactory, (version) -> Build.VERSION.SDK_INT >= version); + +// final GeneratedFileSelectorApi.Result mockResult = mock(GeneratedFileSelectorApi.Result.class); +// fileSelectorApi.openFiles( +// null, +// new GeneratedFileSelectorApi.FileTypes.Builder() +// .setMimeTypes(Collections.emptyList()) +// .setExtensions(Collections.emptyList()) +// .build(), +// mockResult); +// verify(mockIntent).addCategory(Intent.CATEGORY_OPENABLE); +// verify(mockIntent).putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + +// verify(mockActivity).startActivityForResult(mockIntent, 222); + +// final ArgumentCaptor listenerArgumentCaptor = +// ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); +// verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); + +// final Intent resultMockIntent = mock(Intent.class); +// final ClipData mockClipData = mock(ClipData.class); +// when(mockClipData.getItemCount()).thenReturn(2); + +// final ClipData.Item mockClipDataItem = mock(ClipData.Item.class); +// when(mockClipDataItem.getUri()).thenReturn(mockUri); +// when(mockClipData.getItemAt(0)).thenReturn(mockClipDataItem); + +// final ClipData.Item mockClipDataItem2 = mock(ClipData.Item.class); +// when(mockClipDataItem2.getUri()).thenReturn(mockUri2); +// when(mockClipData.getItemAt(1)).thenReturn(mockClipDataItem2); + +// when(resultMockIntent.getClipData()).thenReturn(mockClipData); + +// listenerArgumentCaptor.getValue().onActivityResult(222, Activity.RESULT_OK, resultMockIntent); + +// final ArgumentCaptor fileListCaptor = ArgumentCaptor.forClass(List.class); +// verify(mockResult).success(fileListCaptor.capture()); + +// final List fileList = fileListCaptor.getValue(); +// assertEquals(fileList.get(0).getBytes().length, 30); +// assertEquals(fileList.get(0).getMimeType(), "text/plain"); +// assertEquals(fileList.get(0).getName(), "filename"); +// assertEquals(fileList.get(0).getSize(), (Long) 30L); +// assertEquals(fileList.get(0).getPath(), "some/path/"); + +// assertEquals(fileList.get(1).getBytes().length, 40); +// assertEquals(fileList.get(1).getMimeType(), "image/jpg"); +// assertEquals(fileList.get(1).getName(), "filename2"); +// assertEquals(fileList.get(1).getSize(), (Long) 40L); +// assertEquals(fileList.get(1).getPath(), "some/other/path/"); +// } + +// @SuppressWarnings({"rawtypes", "unchecked"}) +// @Test +// public void getDirectoryPathReturnsSuccessfully() { +// final Uri mockUri = mock(Uri.class); +// when(mockUri.toString()).thenReturn("some/path/"); //TODO: change this + +// when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT_TREE)).thenReturn(mockIntent); +// when(mockActivityBinding.getActivity()).thenReturn(mockActivity); +// final FileSelectorApiImpl fileSelectorApi = +// new FileSelectorApiImpl( +// mockActivityBinding, +// mockObjectFactory, +// (version) -> Build.VERSION_CODES.LOLLIPOP >= version); + +// final GeneratedFileSelectorApi.Result mockResult = mock(GeneratedFileSelectorApi.Result.class); +// fileSelectorApi.getDirectoryPath(null, mockResult); + +// verify(mockActivity).startActivityForResult(mockIntent, 223); + +// final ArgumentCaptor listenerArgumentCaptor = +// ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); +// verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); + +// final Intent resultMockIntent = mock(Intent.class); +// when(resultMockIntent.getData()).thenReturn(mockUri); +// listenerArgumentCaptor.getValue().onActivityResult(223, Activity.RESULT_OK, resultMockIntent); + +// verify(mockResult).success("some/path/"); +// } + +// @Test +// public void getDirectoryPath_errorsForUnsupportedVersion() { +// final FileSelectorApiImpl fileSelectorApi = +// new FileSelectorApiImpl( +// mockActivityBinding, +// mockObjectFactory, +// (version) -> Build.VERSION_CODES.KITKAT >= version); + +// @SuppressWarnings("unchecked") +// final GeneratedFileSelectorApi.Result mockResult = +// mock(GeneratedFileSelectorApi.Result.class); +// fileSelectorApi.getDirectoryPath(null, mockResult); + +// verify(mockResult).error(any()); +// } +// } diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java index f88f17f22d6..8e778718c2c 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java @@ -2,14 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package io.flutter.plugins.file_selector_android; +package dev.flutter.packages.file_selector_android; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; @@ -20,6 +23,8 @@ import android.database.Cursor; import android.database.MatrixCursor; import android.net.Uri; +import android.os.Environment; +import android.provider.DocumentsContract; import android.provider.MediaStore; import android.webkit.MimeTypeMap; import androidx.annotation.NonNull; @@ -33,22 +38,28 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.MockedStatic; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; import org.robolectric.shadows.ShadowContentResolver; import org.robolectric.shadows.ShadowEnvironment; import org.robolectric.shadows.ShadowMimeTypeMap; +import org.mockito.stubbing.Answer; @RunWith(RobolectricTestRunner.class) -public class FileUtilTest { +public class FileUtilsTest { private Context context; - final private String externalStorageDirectoryPath = ShadowEnvironment.getExternalStorageDirectory(); + final private String externalStorageDirectoryPath = Environment.getExternalStorageDirectory().getPath(); ShadowContentResolver shadowContentResolver; + ContentResolver contentResolver; @Before public void before() { context = ApplicationProvider.getApplicationContext(); + contentResolver = spy(context.getContentResolver()); shadowContentResolver = shadowOf(context.getContentResolver()); ShadowMimeTypeMap mimeTypeMap = shadowOf(MimeTypeMap.getSingleton()); mimeTypeMap.addExtensionMimeTypMapping("txt", "document/txt"); @@ -59,24 +70,41 @@ public void before() { @Test public void getPathFromUri_returnsExpectedPathForExternalDocumentUri() { + // Uri that represents Documents/test directory on device: Uri uri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3ADocuments%2Ftest"); - String path = FileUtils.getPathFromUri(context, uri); - String expectedPath = externalStorageDirectoryPath + "document.txt"; - assertEquals(path, expectedPath); + try (MockedStatic mockedDocumentsContract = mockStatic(DocumentsContract.class)) { + mockedDocumentsContract + .when(() -> DocumentsContract.getDocumentId(uri)) + .thenAnswer( + (Answer) + invocation -> "primary:Documents/test"); + String path = FileUtils.getPathFromUri(context, uri); + String expectedPath = externalStorageDirectoryPath + "/Documents/test"; + assertEquals(path, expectedPath); + } } - @Test - public void getPathFromUri_returnsExpectedPathForMediaDocumentUri() { - Uri uri = Uri.parse("content://com.android.providers.media.documents/document/image:1"); - String path = FileUtils.getPathFromUri(context, uri); - String expectedPath = ""; // TODO - assertEquals(path, expectedPath); - } +// @Test +// public void getPathFromUri_returnsExpectedPathForMediaDocumentUri() { +// // Uri that represents tex +// Uri uri = Uri.parse("content://com.android.providers.media.documents/document/document%3A35"); +// String path = FileUtils.getPathFromUri(context, uri); +// String expectedPath = ""; // TODO +// assertEquals(path, expectedPath); +// } @Test public void getPathFromUri_throwExceptionForExternalDocumentUriWithNonPrimaryStorageVolume() { - Uri uri = Uri.parse("content://com.android.externalstorage.documents/tree/emulated%3ADocuments%2Ftest"); + // Uri that represents Documents/test directory from some external storage volume ("external" for this test): + Uri uri = Uri.parse("content://com.android.externalstorage.documents/tree/external%3ADocuments%2Ftest"); + try (MockedStatic mockedDocumentsContract = mockStatic(DocumentsContract.class)) { + mockedDocumentsContract + .when(() -> DocumentsContract.getDocumentId(uri)) + .thenAnswer( + (Answer) + invocation -> "external:Documents/test"); assertThrows(UnsupportedOperationException.class, () -> FileUtils.getPathFromUri(context, uri)); + } } @Test @@ -87,10 +115,15 @@ public void getPathFromUri_throwExceptionForUriWithUnhandledAuthority() { @Test public void getPathFromCopyOfFileFromUri_returnsPathWithContent() throws IOException { - Uri uri = Uri.parse("content://dummy/dummy.png"); - shadowContentResolver.registerInputStream( - uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); - String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); + Uri uri = Uri.parse("content://dummy/dummy.png"); + Context mc = mock(Context.class); + ContentResolver mcr = mock(ContentResolver.class); + when(mc.getContentResolver()).thenReturn(mcr); + when(mcr.getType(uri)).thenReturn("image/png"); + // when(contentResolver.getType(uri)).thenReturn("image/png"); // try using mock context + // shadowContentResolver.registerInputStream( + // uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); + String path = FileUtils.getPathFromCopyOfFileFromUri(mc, uri); File file = new File(path); int size = (int) file.length(); byte[] bytes = new byte[size]; @@ -100,82 +133,145 @@ public void getPathFromCopyOfFileFromUri_returnsPathWithContent() throws IOExcep buf.close(); assertTrue(bytes.length > 0); - String fileStream = new String(bytes, UTF_8); - assertEquals("fileStream", fileStream); + // String fileStream = new String(bytes, UTF_8); + // assertEquals("fileStream", fileStream); } - @Test - public void getPathFromCopyOfFileFromUri_returnsNullPathWhenSecurityExceptionThrown() throws IOException { - Uri uri = Uri.parse("content://dummy/dummy.png"); - ContentResolver mockContentResolver = mock(ContentResolver.class); - when(mockContentResolver.openInputStream(any(Uri.class))).thenThrow(SecurityException.class); +// @Test +// public void getPathFromCopyOfFileFromUri_returnsNullPathWhenSecurityExceptionThrown() throws IOException { +// Uri uri = Uri.parse("content://dummy/dummy.png"); - Context mockContext = mock(Context.class); - when(mockContext.getContentResolver()).thenReturn(mockContentResolver); +// ContentResolver mockContentResolver = mock(ContentResolver.class); +// when(mockContentResolver.openInputStream(any(Uri.class))).thenThrow(SecurityException.class); - String path = fileUtils.getPathFromCopyOfFileFromUri(mockContext, uri); +// Context mockContext = mock(Context.class); +// when(mockContext.getContentResolver()).thenReturn(mockContentResolver); - assertNull(path); - } +// String path = FileUtils.getPathFromCopyOfFileFromUri(mockContext, uri); - @Test - public void getFileExtension_returnsExpectedTextFileExtension() throws IOException { - Uri uri = Uri.parse("content://document/dummy.txt"); - shadowContentResolver.registerInputStream( - uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); - String path = fileUtils.getPathFromCopyOfFileFromUri(context, uri); - assertTrue(path.endsWith(".txt")); - } +// assertNull(path); +// } - @Test - public void getFileExtension_returnsExpectedImageExtension() throws IOException { - Uri uri = Uri.parse("content://dummy/dummy.png"); - shadowContentResolver.registerInputStream( - uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); - String path = fileUtils.getPathFromCopyOfFileFromUri(context, uri); - assertTrue(path.endsWith(".jpg")); - } +// @Test +// public void getFileExtension_returnsExpectedTextFileExtension() throws IOException { +// Uri uri = Uri.parse("content://document/dummy.txt"); +// shadowContentResolver.registerInputStream( +// uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); +// String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); +// assertTrue(path.endsWith(".txt")); +// } - @Test - public void getFileName_returnsExpectedName() throws IOException { - Uri uri = MockContentProvider.PNG_URI; - Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); - shadowContentResolver.registerInputStream( - uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); - String path = fileUtils.getPathFromCopyOfFileFromUri(context, uri); - assertTrue(path.endsWith("a.b.png")); - } +// @Test +// public void getFileExtension_returnsExpectedImageExtension() throws IOException { +// Uri uri = Uri.parse("content://dummy/dummy.png"); +// shadowContentResolver.registerInputStream( +// uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); +// String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); +// assertTrue(path.endsWith(".jpg")); +// } - @Test - public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithNoExtensionInBaseName() throws IOException { - Uri uri = MockContentProvider.NO_EXTENSION_URI; - Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); - shadowContentResolver.registerInputStream( - uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); - String path = fileUtils.getPathFromCopyOfFileFromUri(context, uri); - assertTrue(path.endsWith("abc.png")); - } +// @Test +// public void getFileName_returnsExpectedName() throws IOException { +// Uri uri = MockContentProvider.PNG_URI; +// Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); +// shadowContentResolver.registerInputStream( +// uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); +// String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); +// assertTrue(path.endsWith("a.b.png")); +// } - @Test - public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithMismatchedTypeToFile() throws IOException { - Uri uri = MockContentProvider.WEBP_URI; - Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); - shadowContentResolver.registerInputStream( - uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); - String path = fileUtils.getPathFromCopyOfFileFromUri(context, uri); - assertTrue(path.endsWith("c.d.webp")); - } +// @Test +// public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithNoExtensionInBaseName() throws IOException { +// Uri uri = MockContentProvider.NO_EXTENSION_URI; +// Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); +// shadowContentResolver.registerInputStream( +// uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); +// String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); +// assertTrue(path.endsWith("abc.png")); +// } - @Test - public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithUnknownType() throws IOException { - Uri uri = MockContentProvider.UNKNOWN_URI; - Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); - shadowContentResolver.registerInputStream( - uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); - String path = fileUtils.getPathFromCopyOfFileFromUri(context, uri); - assertTrue(path.endsWith("e.f.g")); - } +// @Test +// public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithMismatchedTypeToFile() throws IOException { +// Uri uri = MockContentProvider.WEBP_URI; +// Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); +// shadowContentResolver.registerInputStream( +// uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); +// String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); +// assertTrue(path.endsWith("c.d.webp")); +// } + +// @Test +// public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithUnknownType() throws IOException { +// Uri uri = MockContentProvider.UNKNOWN_URI; +// Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); +// shadowContentResolver.registerInputStream( +// uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); +// String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); +// assertTrue(path.endsWith("e.f.g")); +// } + + // @Implements(ShadowContentResolver.class) + // private static class TestShadowContentResolver extends ShadowContentResolver { + // public static final Uri PNG_URI_2 = Uri.parse("content://dummy/dummy.png"); + // public static final Uri TXT_URI = Uri.parse("content://document/dummy.txt"); + // public static final Uri PNG_URI = Uri.parse("content://dummy/a.b.png"); + // public static final Uri WEBP_URI = Uri.parse("content://dummy/c.d.png"); + // public static final Uri UNKNOWN_URI = Uri.parse("content://dummy/e.f.g"); + // public static final Uri NO_EXTENSION_URI = Uri.parse("content://dummy/abc"); + + // // @Override + // // public boolean onCreate() { + // // return true; + // // } + + // // @Nullable + // // @Override + // // public Cursor query( + // // @NonNull Uri uri, + // // @Nullable String[] projection, + // // @Nullable String selection, + // // @Nullable String[] selectionArgs, + // // @Nullable String sortOrder) { + // // MatrixCursor cursor = new MatrixCursor(new String[] {MediaStore.MediaColumns.DISPLAY_NAME}); + // // cursor.addRow(new Object[] {uri.getLastPathSegment()}); + // // return cursor; + // // } + + // @Nullable + // @Implementation + // public String getType(@NonNull Uri uri) { + // System.out.println("HERE!!!!!"); + // if (uri.equals(TXT_URI)) return "document/txt"; + // if (uri.equals(PNG_URI)) return "image/png"; + // if (uri.equals(PNG_URI_2)) return "image/png"; + // if (uri.equals(WEBP_URI)) return "image/webp"; + // if (uri.equals(NO_EXTENSION_URI)) return "image/png"; + // return null; + // } + + // // @Nullable + // // @Override + // // public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { + // // return null; + // // } + + // // @Override + // // public int delete( + // // @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { + // // return 0; + // // } + + // @Override + // public int update( + // @NonNull Uri uri, + // @Nullable ContentValues values, + // @Nullable String selection, + // @Nullable String[] selectionArgs) { + // return 0; + // } + // } + private static class MockContentProvider extends ContentProvider { public static final Uri TXT_URI = Uri.parse("content://document/dummy.txt"); @@ -205,6 +301,7 @@ public Cursor query( @Nullable @Override public String getType(@NonNull Uri uri) { + System.out.println("HERE"); if (uri.equals(TXT_URI)) return "document/txt"; if (uri.equals(PNG_URI)) return "image/png"; if (uri.equals(WEBP_URI)) return "image/webp"; diff --git a/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java b/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java index 60e8b28e980..e33795dfb74 100644 --- a/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java +++ b/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java @@ -1,90 +1,90 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// // Copyright 2013 The Flutter Authors. All rights reserved. +// // Use of this source code is governed by a BSD-style license that can be +// // found in the LICENSE file. -package dev.flutter.packages.file_selector_android_example; +// package dev.flutter.packages.file_selector_android_example; -import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget; -import static androidx.test.espresso.flutter.action.FlutterActions.click; -import static androidx.test.espresso.flutter.assertion.FlutterAssertions.matches; -import static androidx.test.espresso.flutter.matcher.FlutterMatchers.isExisting; -import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withText; -import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withValueKey; -import static androidx.test.espresso.intent.Intents.intended; -import static androidx.test.espresso.intent.Intents.intending; -import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction; -import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra; +// import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget; +// import static androidx.test.espresso.flutter.action.FlutterActions.click; +// import static androidx.test.espresso.flutter.assertion.FlutterAssertions.matches; +// import static androidx.test.espresso.flutter.matcher.FlutterMatchers.isExisting; +// import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withText; +// import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withValueKey; +// import static androidx.test.espresso.intent.Intents.intended; +// import static androidx.test.espresso.intent.Intents.intending; +// import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction; +// import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra; -import android.app.Activity; -import android.app.Instrumentation; -import android.content.ClipData; -import android.content.Intent; -import android.net.Uri; -import androidx.test.core.app.ActivityScenario; -import androidx.test.espresso.intent.rule.IntentsRule; -import androidx.test.ext.junit.rules.ActivityScenarioRule; -import org.junit.Rule; -import org.junit.Test; +// import android.app.Activity; +// import android.app.Instrumentation; +// import android.content.ClipData; +// import android.content.Intent; +// import android.net.Uri; +// import androidx.test.core.app.ActivityScenario; +// import androidx.test.espresso.intent.rule.IntentsRule; +// import androidx.test.ext.junit.rules.ActivityScenarioRule; +// import org.junit.Rule; +// import org.junit.Test; -public class FileSelectorAndroidTest { - @Rule - public ActivityScenarioRule myActivityTestRule = - new ActivityScenarioRule<>(DriverExtensionActivity.class); +// public class FileSelectorAndroidTest { +// @Rule +// public ActivityScenarioRule myActivityTestRule = +// new ActivityScenarioRule<>(DriverExtensionActivity.class); - @Rule public IntentsRule intentsRule = new IntentsRule(); +// @Rule public IntentsRule intentsRule = new IntentsRule(); - public void clearAnySystemDialog() { - myActivityTestRule - .getScenario() - .onActivity( - new ActivityScenario.ActivityAction() { - @Override - public void perform(DriverExtensionActivity activity) { - Intent closeDialog = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); - activity.sendBroadcast(closeDialog); - } - }); - } +// public void clearAnySystemDialog() { +// myActivityTestRule +// .getScenario() +// .onActivity( +// new ActivityScenario.ActivityAction() { +// @Override +// public void perform(DriverExtensionActivity activity) { +// Intent closeDialog = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); +// activity.sendBroadcast(closeDialog); +// } +// }); +// } - @Test - public void openImageFile() { - clearAnySystemDialog(); +// @Test +// public void openImageFile() { +// clearAnySystemDialog(); - final Instrumentation.ActivityResult result = - new Instrumentation.ActivityResult( - Activity.RESULT_OK, - new Intent().setData(Uri.parse("content://file_selector_android_test/dummy.png"))); +// final Instrumentation.ActivityResult result = +// new Instrumentation.ActivityResult( +// Activity.RESULT_OK, +// new Intent().setData(Uri.parse("content://file_selector_android_test/dummy.png"))); - intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(result); - onFlutterWidget(withText("Open an image")).perform(click()); - onFlutterWidget(withText("Press to open an image file(png, jpg)")).perform(click()); - intended(hasAction(Intent.ACTION_OPEN_DOCUMENT)); - onFlutterWidget(withValueKey("result_image_name")) - .check(matches(withText("content://file_selector_android_test/dummy.png"))); // TODO: change this - } +// intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(result); +// onFlutterWidget(withText("Open an image")).perform(click()); +// onFlutterWidget(withText("Press to open an image file(png, jpg)")).perform(click()); +// intended(hasAction(Intent.ACTION_OPEN_DOCUMENT)); +// onFlutterWidget(withValueKey("result_image_name")) +// .check(matches(withText("content://file_selector_android_test/dummy.png"))); // TODO: change this +// } - @Test - public void openImageFiles() { - clearAnySystemDialog(); +// @Test +// public void openImageFiles() { +// clearAnySystemDialog(); - final ClipData.Item clipDataItem = - new ClipData.Item(Uri.parse("content://file_selector_android_test/dummy.png")); // TODO: change this - final ClipData clipData = new ClipData("", new String[0], clipDataItem); - clipData.addItem(clipDataItem); +// final ClipData.Item clipDataItem = +// new ClipData.Item(Uri.parse("content://file_selector_android_test/dummy.png")); // TODO: change this +// final ClipData clipData = new ClipData("", new String[0], clipDataItem); +// clipData.addItem(clipDataItem); - final Intent resultIntent = new Intent(); - resultIntent.setClipData(clipData); +// final Intent resultIntent = new Intent(); +// resultIntent.setClipData(clipData); - final Instrumentation.ActivityResult result = - new Instrumentation.ActivityResult(Activity.RESULT_OK, resultIntent); - intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(result); - onFlutterWidget(withText("Open multiple images")).perform(click()); - onFlutterWidget(withText("Press to open multiple images (png, jpg)")).perform(click()); +// final Instrumentation.ActivityResult result = +// new Instrumentation.ActivityResult(Activity.RESULT_OK, resultIntent); +// intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(result); +// onFlutterWidget(withText("Open multiple images")).perform(click()); +// onFlutterWidget(withText("Press to open multiple images (png, jpg)")).perform(click()); - intended(hasAction(Intent.ACTION_OPEN_DOCUMENT)); - intended(hasExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)); +// intended(hasAction(Intent.ACTION_OPEN_DOCUMENT)); +// intended(hasExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)); - onFlutterWidget(withValueKey("result_image_name0")).check(matches(isExisting())); - onFlutterWidget(withValueKey("result_image_name1")).check(matches(isExisting())); - } -} +// onFlutterWidget(withValueKey("result_image_name0")).check(matches(isExisting())); +// onFlutterWidget(withValueKey("result_image_name1")).check(matches(isExisting())); +// } +// } diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java index 620bac74a17..6dc9023f9c2 100644 --- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java @@ -62,6 +62,7 @@ public void FileUtil_GetPathFromUri() throws IOException { shadowContentResolver.registerInputStream( uri, new ByteArrayInputStream("imageStream".getBytes(UTF_8))); String path = fileUtils.getPathFromUri(context, uri); + System.out.println(path); File file = new File(path); int size = (int) file.length(); byte[] bytes = new byte[size]; From 27bf9a1d466c71a7f91bd85c2444747cd9c658b3 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Wed, 17 Apr 2024 10:42:14 -0700 Subject: [PATCH 12/21] Finish tests --- .../FileSelectorApiImpl.java | 2 - .../file_selector_android/FileUtils.java | 41 +- .../FileSelectorAndroidPluginTest.java | 519 ++++++++++-------- .../file_selector_android/FileUtilsTest.java | 216 +++----- .../FileSelectorAndroidTest.java | 196 ++++--- .../example/pubspec.yaml | 2 +- 6 files changed, 461 insertions(+), 515 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java index aa513b16206..256f441ae23 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java @@ -204,7 +204,6 @@ public void getDirectoryPath( public void onResult(int resultCode, @Nullable Intent data) { if (resultCode == Activity.RESULT_OK && data != null) { final Uri uri = data.getData(); - System.out.println(uri); final Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri)); try { final String path = FileUtils.getPathFromUri(activityPluginBinding.getActivity(), docUri); @@ -339,7 +338,6 @@ GeneratedFileSelectorApi.FileResponse toFileResponse(@NonNull Uri uri) { return null; } - System.out.println(uri); final String uriPath = FileUtils.getPathFromCopyOfFileFromUri(activityPluginBinding.getActivity(), uri); return new GeneratedFileSelectorApi.FileResponse.Builder() diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index a8efe35c8c5..a3bd125aec0 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -49,9 +49,6 @@ public class FileUtils { /** URI authority that represents access to external storage providers. */ public static String EXTERNAL_DOCUMENT_AUTHORITY = "com.android.externalstorage.documents"; - /** URI authority that represents a media document. */ - public static String MEDIA_DOCUMENT_AUTHORITY = "com.android.providers.media.documents"; - /** * Retrieves path of directory represented by the specified {@code Uri}. * @@ -62,52 +59,22 @@ public class FileUtils { *

Will return the path for on-device directories, but does not handle external storage volumes. */ public static String getPathFromUri(Context context, Uri uri) { - System.out.println("CAMILLE AA"); - String uriAuthority = uri.getAuthority(); - System.out.println("CAMILLE AB"); if (uriAuthority.equals(EXTERNAL_DOCUMENT_AUTHORITY)) { - System.out.println("CAMILLE AC"); - String uriDocumentId = DocumentsContract.getDocumentId(uri); - System.out.println(uriDocumentId); - System.out.println("CAMILLE AD"); - String documentStorageVolume = uriDocumentId.split(":")[0]; - System.out.println("CAMILLE A"); // Non-primary storage volumes come from SD cards, USB drives, etc. and are // not handled here. if (!documentStorageVolume.equals("primary")) { - System.out.println("CAMILLE B"); - throw new UnsupportedOperationException("Retrieving the path of a document from storage volume " + documentStorageVolume + " is unsupported by this plugin."); } String innermostDirectoryName = uriDocumentId.split(":")[1]; String externalStorageDirectory = Environment.getExternalStorageDirectory().getPath(); - System.out.println("CAMILLE C"); return externalStorageDirectory + "/" + innermostDirectoryName; } - // } else if (uriAuthority.equals(MEDIA_DOCUMENT_AUTHORITY)) { - // System.out.println("HERE!!!!!11"); - // String uriDocumentId = DocumentsContract.getDocumentId(uri); - // String documentStorageVolume = uriDocumentId.split(":")[0]; - // ContentResolver contentResolver = context.getContentResolver(); - - // // This makes an assumption that the URI has the content scheme, which we can safely - // // assume since this method only supports finding paths of URIs retrieved from - // // the Intents ACTION_OPEN_FILE(S) and ACTION_OPEN_DOCUMENT_TREE. - // Cursor cursor = contentResolver.query(uri, null, null, null, null, null); - - // if (cursor != null && cursor.moveToFirst()) { - // return cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH)); - // } else { - // // Unable to retrieve path of file using cursor. - // return null; - // } - // } else { throw new UnsupportedOperationException("Retrieving the path from URIs with authority " + uriAuthority.toString() + " is unsupported by this plugin."); } @@ -131,8 +98,6 @@ public static String getPathFromCopyOfFileFromUri(final Context context, final U String uuid = UUID.randomUUID().toString(); File targetDirectory = new File(context.getCacheDir(), uuid); targetDirectory.mkdir(); - // TODO(camsim99): according to the docs, `deleteOnExit` does not work reliably on Android; we should preferably - // just clear the picked files after the app startup. targetDirectory.deleteOnExit(); String fileName = getFileName(context, uri); String extension = getFileExtension(context, uri); @@ -148,9 +113,11 @@ public static String getPathFromCopyOfFileFromUri(final Context context, final U } File file = new File(targetDirectory, fileName); + try (OutputStream outputStream = new FileOutputStream(file)) { copy(inputStream, outputStream); return file.getPath(); + } } catch (IOException e) { // If closing the output stream fails, we cannot be sure that the @@ -173,13 +140,9 @@ private static String getFileExtension(Context context, Uri uriFile) { try { if (uriFile.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { - System.out.println("CAMILLE 1"); final MimeTypeMap mime = MimeTypeMap.getSingleton(); - System.out.println(uriFile); - System.out.println(context.getContentResolver().getType(uriFile)); extension = mime.getExtensionFromMimeType(context.getContentResolver().getType(uriFile)); } else { - System.out.println("CAMILLE 2"); extension = MimeTypeMap.getFileExtensionFromUrl( Uri.fromFile(new File(uriFile.getPath())).toString()); diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java index 030f025a687..86a1486649d 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java @@ -1,234 +1,285 @@ -// // Copyright 2013 The Flutter Authors. All rights reserved. -// // Use of this source code is governed by a BSD-style license that can be -// // found in the LICENSE file. - -// package dev.flutter.packages.file_selector_android; - -// import static org.junit.Assert.assertEquals; -// import static org.mockito.ArgumentMatchers.any; -// import static org.mockito.Mockito.mock; -// import static org.mockito.Mockito.verify; -// import static org.mockito.Mockito.when; - -// import android.app.Activity; -// import android.content.ClipData; -// import android.content.ContentResolver; -// import android.content.Intent; -// import android.database.Cursor; -// import android.net.Uri; -// import android.os.Build; -// import android.provider.OpenableColumns; -// import androidx.annotation.NonNull; -// import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -// import io.flutter.plugin.common.PluginRegistry; -// import java.io.DataInputStream; -// import java.io.FileNotFoundException; -// import java.io.InputStream; -// import java.util.Collections; -// import java.util.List; -// import org.junit.Rule; -// import org.junit.Test; -// import org.mockito.ArgumentCaptor; -// import org.mockito.Mock; -// import org.mockito.junit.MockitoJUnit; -// import org.mockito.junit.MockitoRule; - -// public class FileSelectorAndroidPluginTest { -// @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); - -// @Mock public Intent mockIntent; - -// @Mock public Activity mockActivity; - -// @Mock FileSelectorApiImpl.NativeObjectFactory mockObjectFactory; - -// @Mock public ActivityPluginBinding mockActivityBinding; - -// private void mockContentResolver( -// @NonNull ContentResolver mockResolver, -// @NonNull Uri uri, -// @NonNull String displayName, -// int size, -// @NonNull String mimeType) -// throws FileNotFoundException { -// final Cursor mockCursor = mock(Cursor.class); -// when(mockCursor.moveToFirst()).thenReturn(true); - -// when(mockCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)).thenReturn(0); -// when(mockCursor.getString(0)).thenReturn(displayName); - -// when(mockCursor.getColumnIndex(OpenableColumns.SIZE)).thenReturn(1); -// when(mockCursor.isNull(1)).thenReturn(false); -// when(mockCursor.getInt(1)).thenReturn(size); - -// when(mockResolver.query(uri, null, null, null, null, null)).thenReturn(mockCursor); -// when(mockResolver.getType(uri)).thenReturn(mimeType); -// when(mockResolver.openInputStream(uri)).thenReturn(mock(InputStream.class)); -// } - -// @SuppressWarnings({"rawtypes", "unchecked"}) -// @Test -// public void openFileReturnsSuccessfully() throws FileNotFoundException { -// final ContentResolver mockContentResolver = mock(ContentResolver.class); - -// final Uri mockUri = mock(Uri.class); -// when(mockUri.toString()).thenReturn("some/path/"); -// mockContentResolver(mockContentResolver, mockUri, "filename", 30, "text/plain"); - -// when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); -// when(mockObjectFactory.newDataInputStream(any())).thenReturn(mock(DataInputStream.class)); -// when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); -// when(mockActivityBinding.getActivity()).thenReturn(mockActivity); -// final FileSelectorApiImpl fileSelectorApi = -// new FileSelectorApiImpl( -// mockActivityBinding, mockObjectFactory, (version) -> Build.VERSION.SDK_INT >= version); - -// final GeneratedFileSelectorApi.Result mockResult = mock(GeneratedFileSelectorApi.Result.class); -// fileSelectorApi.openFile( -// null, -// new GeneratedFileSelectorApi.FileTypes.Builder() -// .setMimeTypes(Collections.emptyList()) -// .setExtensions(Collections.emptyList()) -// .build(), -// mockResult); -// verify(mockIntent).addCategory(Intent.CATEGORY_OPENABLE); - -// verify(mockActivity).startActivityForResult(mockIntent, 221); - -// final ArgumentCaptor listenerArgumentCaptor = -// ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); -// verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); - -// final Intent resultMockIntent = mock(Intent.class); -// when(resultMockIntent.getData()).thenReturn(mockUri); -// listenerArgumentCaptor.getValue().onActivityResult(221, Activity.RESULT_OK, resultMockIntent); - -// final ArgumentCaptor fileCaptor = -// ArgumentCaptor.forClass(GeneratedFileSelectorApi.FileResponse.class); -// verify(mockResult).success(fileCaptor.capture()); - -// final GeneratedFileSelectorApi.FileResponse file = fileCaptor.getValue(); -// assertEquals(file.getBytes().length, 30); -// assertEquals(file.getMimeType(), "text/plain"); -// assertEquals(file.getName(), "filename"); -// assertEquals(file.getSize(), (Long) 30L); -// assertEquals(file.getPath(), "some/path/"); -// } - -// @SuppressWarnings({"rawtypes", "unchecked"}) -// @Test -// public void openFilesReturnsSuccessfully() throws FileNotFoundException { -// final ContentResolver mockContentResolver = mock(ContentResolver.class); - -// final Uri mockUri = mock(Uri.class); -// when(mockUri.toString()).thenReturn("some/path/"); -// mockContentResolver(mockContentResolver, mockUri, "filename", 30, "text/plain"); - -// final Uri mockUri2 = mock(Uri.class); -// when(mockUri2.toString()).thenReturn("some/other/path/"); -// mockContentResolver(mockContentResolver, mockUri2, "filename2", 40, "image/jpg"); - -// when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); -// when(mockObjectFactory.newDataInputStream(any())).thenReturn(mock(DataInputStream.class)); -// when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); -// when(mockActivityBinding.getActivity()).thenReturn(mockActivity); -// final FileSelectorApiImpl fileSelectorApi = -// new FileSelectorApiImpl( -// mockActivityBinding, mockObjectFactory, (version) -> Build.VERSION.SDK_INT >= version); - -// final GeneratedFileSelectorApi.Result mockResult = mock(GeneratedFileSelectorApi.Result.class); -// fileSelectorApi.openFiles( -// null, -// new GeneratedFileSelectorApi.FileTypes.Builder() -// .setMimeTypes(Collections.emptyList()) -// .setExtensions(Collections.emptyList()) -// .build(), -// mockResult); -// verify(mockIntent).addCategory(Intent.CATEGORY_OPENABLE); -// verify(mockIntent).putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); - -// verify(mockActivity).startActivityForResult(mockIntent, 222); - -// final ArgumentCaptor listenerArgumentCaptor = -// ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); -// verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); - -// final Intent resultMockIntent = mock(Intent.class); -// final ClipData mockClipData = mock(ClipData.class); -// when(mockClipData.getItemCount()).thenReturn(2); - -// final ClipData.Item mockClipDataItem = mock(ClipData.Item.class); -// when(mockClipDataItem.getUri()).thenReturn(mockUri); -// when(mockClipData.getItemAt(0)).thenReturn(mockClipDataItem); - -// final ClipData.Item mockClipDataItem2 = mock(ClipData.Item.class); -// when(mockClipDataItem2.getUri()).thenReturn(mockUri2); -// when(mockClipData.getItemAt(1)).thenReturn(mockClipDataItem2); - -// when(resultMockIntent.getClipData()).thenReturn(mockClipData); - -// listenerArgumentCaptor.getValue().onActivityResult(222, Activity.RESULT_OK, resultMockIntent); - -// final ArgumentCaptor fileListCaptor = ArgumentCaptor.forClass(List.class); -// verify(mockResult).success(fileListCaptor.capture()); - -// final List fileList = fileListCaptor.getValue(); -// assertEquals(fileList.get(0).getBytes().length, 30); -// assertEquals(fileList.get(0).getMimeType(), "text/plain"); -// assertEquals(fileList.get(0).getName(), "filename"); -// assertEquals(fileList.get(0).getSize(), (Long) 30L); -// assertEquals(fileList.get(0).getPath(), "some/path/"); - -// assertEquals(fileList.get(1).getBytes().length, 40); -// assertEquals(fileList.get(1).getMimeType(), "image/jpg"); -// assertEquals(fileList.get(1).getName(), "filename2"); -// assertEquals(fileList.get(1).getSize(), (Long) 40L); -// assertEquals(fileList.get(1).getPath(), "some/other/path/"); -// } - -// @SuppressWarnings({"rawtypes", "unchecked"}) -// @Test -// public void getDirectoryPathReturnsSuccessfully() { -// final Uri mockUri = mock(Uri.class); -// when(mockUri.toString()).thenReturn("some/path/"); //TODO: change this - -// when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT_TREE)).thenReturn(mockIntent); -// when(mockActivityBinding.getActivity()).thenReturn(mockActivity); -// final FileSelectorApiImpl fileSelectorApi = -// new FileSelectorApiImpl( -// mockActivityBinding, -// mockObjectFactory, -// (version) -> Build.VERSION_CODES.LOLLIPOP >= version); - -// final GeneratedFileSelectorApi.Result mockResult = mock(GeneratedFileSelectorApi.Result.class); -// fileSelectorApi.getDirectoryPath(null, mockResult); - -// verify(mockActivity).startActivityForResult(mockIntent, 223); - -// final ArgumentCaptor listenerArgumentCaptor = -// ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); -// verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); - -// final Intent resultMockIntent = mock(Intent.class); -// when(resultMockIntent.getData()).thenReturn(mockUri); -// listenerArgumentCaptor.getValue().onActivityResult(223, Activity.RESULT_OK, resultMockIntent); - -// verify(mockResult).success("some/path/"); -// } - -// @Test -// public void getDirectoryPath_errorsForUnsupportedVersion() { -// final FileSelectorApiImpl fileSelectorApi = -// new FileSelectorApiImpl( -// mockActivityBinding, -// mockObjectFactory, -// (version) -> Build.VERSION_CODES.KITKAT >= version); - -// @SuppressWarnings("unchecked") -// final GeneratedFileSelectorApi.Result mockResult = -// mock(GeneratedFileSelectorApi.Result.class); -// fileSelectorApi.getDirectoryPath(null, mockResult); - -// verify(mockResult).error(any()); -// } -// } +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package dev.flutter.packages.file_selector_android; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.ClipData; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.provider.DocumentsContract; +import android.provider.OpenableColumns; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.PluginRegistry; +import java.io.DataInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.Collections; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; + +public class FileSelectorAndroidPluginTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public Intent mockIntent; + + @Mock public Activity mockActivity; + + @Mock FileSelectorApiImpl.NativeObjectFactory mockObjectFactory; + + @Mock public ActivityPluginBinding mockActivityBinding; + + private void mockContentResolver( + @NonNull ContentResolver mockResolver, + @NonNull Uri uri, + @NonNull String displayName, + int size, + @NonNull String mimeType) + throws FileNotFoundException { + final Cursor mockCursor = mock(Cursor.class); + when(mockCursor.moveToFirst()).thenReturn(true); + + when(mockCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)).thenReturn(0); + when(mockCursor.getString(0)).thenReturn(displayName); + + when(mockCursor.getColumnIndex(OpenableColumns.SIZE)).thenReturn(1); + when(mockCursor.isNull(1)).thenReturn(false); + when(mockCursor.getInt(1)).thenReturn(size); + + when(mockResolver.query(uri, null, null, null, null, null)).thenReturn(mockCursor); + when(mockResolver.getType(uri)).thenReturn(mimeType); + when(mockResolver.openInputStream(uri)).thenReturn(mock(InputStream.class)); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Test + public void openFileReturnsSuccessfully() throws FileNotFoundException { + try (MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { + final ContentResolver mockContentResolver = mock(ContentResolver.class); + + final Uri mockUri = mock(Uri.class); + final String mockUriPath = "/some/path"; + mockedFileUtils + .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri))) + .thenAnswer( + (Answer) + invocation -> mockUriPath); + mockContentResolver(mockContentResolver, mockUri, "filename", 30, "text/plain"); + + when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); + when(mockObjectFactory.newDataInputStream(any())).thenReturn(mock(DataInputStream.class)); + when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); + when(mockActivityBinding.getActivity()).thenReturn(mockActivity); + final FileSelectorApiImpl fileSelectorApi = + new FileSelectorApiImpl( + mockActivityBinding, mockObjectFactory, (version) -> Build.VERSION.SDK_INT >= version); + + final GeneratedFileSelectorApi.Result mockResult = mock(GeneratedFileSelectorApi.Result.class); + fileSelectorApi.openFile( + null, + new GeneratedFileSelectorApi.FileTypes.Builder() + .setMimeTypes(Collections.emptyList()) + .setExtensions(Collections.emptyList()) + .build(), + mockResult); + verify(mockIntent).addCategory(Intent.CATEGORY_OPENABLE); + + verify(mockActivity).startActivityForResult(mockIntent, 221); + + final ArgumentCaptor listenerArgumentCaptor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); + + final Intent resultMockIntent = mock(Intent.class); + when(resultMockIntent.getData()).thenReturn(mockUri); + listenerArgumentCaptor.getValue().onActivityResult(221, Activity.RESULT_OK, resultMockIntent); + + final ArgumentCaptor fileCaptor = + ArgumentCaptor.forClass(GeneratedFileSelectorApi.FileResponse.class); + verify(mockResult).success(fileCaptor.capture()); + + final GeneratedFileSelectorApi.FileResponse file = fileCaptor.getValue(); + assertEquals(file.getBytes().length, 30); + assertEquals(file.getMimeType(), "text/plain"); + assertEquals(file.getName(), "filename"); + assertEquals(file.getSize(), (Long) 30L); + assertEquals(file.getPath(), mockUriPath); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Test + public void openFilesReturnsSuccessfully() throws FileNotFoundException { + try (MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { + + final ContentResolver mockContentResolver = mock(ContentResolver.class); + + final Uri mockUri = mock(Uri.class); + final String mockUriPath = "some/path/"; + mockedFileUtils + .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri))) + .thenAnswer( + (Answer) + invocation -> mockUriPath); + mockContentResolver(mockContentResolver, mockUri, "filename", 30, "text/plain"); + + final Uri mockUri2 = mock(Uri.class); + final String mockUri2Path = "some/other/path/"; + mockedFileUtils + .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri2))) + .thenAnswer( + (Answer) + invocation -> mockUri2Path); + mockContentResolver(mockContentResolver, mockUri2, "filename2", 40, "image/jpg"); + + when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); + when(mockObjectFactory.newDataInputStream(any())).thenReturn(mock(DataInputStream.class)); + when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); + when(mockActivityBinding.getActivity()).thenReturn(mockActivity); + final FileSelectorApiImpl fileSelectorApi = + new FileSelectorApiImpl( + mockActivityBinding, mockObjectFactory, (version) -> Build.VERSION.SDK_INT >= version); + + final GeneratedFileSelectorApi.Result mockResult = mock(GeneratedFileSelectorApi.Result.class); + fileSelectorApi.openFiles( + null, + new GeneratedFileSelectorApi.FileTypes.Builder() + .setMimeTypes(Collections.emptyList()) + .setExtensions(Collections.emptyList()) + .build(), + mockResult); + verify(mockIntent).addCategory(Intent.CATEGORY_OPENABLE); + verify(mockIntent).putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + + verify(mockActivity).startActivityForResult(mockIntent, 222); + + final ArgumentCaptor listenerArgumentCaptor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); + + final Intent resultMockIntent = mock(Intent.class); + final ClipData mockClipData = mock(ClipData.class); + when(mockClipData.getItemCount()).thenReturn(2); + + final ClipData.Item mockClipDataItem = mock(ClipData.Item.class); + when(mockClipDataItem.getUri()).thenReturn(mockUri); + when(mockClipData.getItemAt(0)).thenReturn(mockClipDataItem); + + final ClipData.Item mockClipDataItem2 = mock(ClipData.Item.class); + when(mockClipDataItem2.getUri()).thenReturn(mockUri2); + when(mockClipData.getItemAt(1)).thenReturn(mockClipDataItem2); + + when(resultMockIntent.getClipData()).thenReturn(mockClipData); + + listenerArgumentCaptor.getValue().onActivityResult(222, Activity.RESULT_OK, resultMockIntent); + + final ArgumentCaptor fileListCaptor = ArgumentCaptor.forClass(List.class); + verify(mockResult).success(fileListCaptor.capture()); + + final List fileList = fileListCaptor.getValue(); + assertEquals(fileList.get(0).getBytes().length, 30); + assertEquals(fileList.get(0).getMimeType(), "text/plain"); + assertEquals(fileList.get(0).getName(), "filename"); + assertEquals(fileList.get(0).getSize(), (Long) 30L); + assertEquals(fileList.get(0).getPath(), mockUriPath); + + assertEquals(fileList.get(1).getBytes().length, 40); + assertEquals(fileList.get(1).getMimeType(), "image/jpg"); + assertEquals(fileList.get(1).getName(), "filename2"); + assertEquals(fileList.get(1).getSize(), (Long) 40L); + assertEquals(fileList.get(1).getPath(), mockUri2Path); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Test + public void getDirectoryPathReturnsSuccessfully() { + try (MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { + final Uri mockUri = mock(Uri.class); + final String mockUriPath = "some/path/"; + final String mockUriId = "someId"; + final Uri mockUriUsingTree = mock(Uri.class); + + mockedFileUtils + .when(() -> FileUtils.getPathFromUri(any(Context.class), eq(mockUriUsingTree))) + .thenAnswer( + (Answer) + invocation -> mockUriPath); + + try (MockedStatic mockedDocumentsContract = mockStatic(DocumentsContract.class)) { + + mockedDocumentsContract + .when(() -> DocumentsContract.getTreeDocumentId(mockUri)) + .thenAnswer( + (Answer) + invocation -> mockUriId); + mockedDocumentsContract + .when(() -> DocumentsContract.buildDocumentUriUsingTree(mockUri, mockUriId)) + .thenAnswer( + (Answer) + invocation -> mockUriUsingTree); + + + when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT_TREE)).thenReturn(mockIntent); + when(mockActivityBinding.getActivity()).thenReturn(mockActivity); + final FileSelectorApiImpl fileSelectorApi = + new FileSelectorApiImpl( + mockActivityBinding, + mockObjectFactory, + (version) -> Build.VERSION_CODES.LOLLIPOP >= version); + + final GeneratedFileSelectorApi.Result mockResult = mock(GeneratedFileSelectorApi.Result.class); + fileSelectorApi.getDirectoryPath(null, mockResult); + + verify(mockActivity).startActivityForResult(mockIntent, 223); + + final ArgumentCaptor listenerArgumentCaptor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); + + final Intent resultMockIntent = mock(Intent.class); + when(resultMockIntent.getData()).thenReturn(mockUri); + listenerArgumentCaptor.getValue().onActivityResult(223, Activity.RESULT_OK, resultMockIntent); + + verify(mockResult).success(mockUriPath); + } + } + } + + @Test + public void getDirectoryPath_errorsForUnsupportedVersion() { + final FileSelectorApiImpl fileSelectorApi = + new FileSelectorApiImpl( + mockActivityBinding, + mockObjectFactory, + (version) -> Build.VERSION_CODES.KITKAT >= version); + + @SuppressWarnings("unchecked") + final GeneratedFileSelectorApi.Result mockResult = + mock(GeneratedFileSelectorApi.Result.class); + fileSelectorApi.getDirectoryPath(null, mockResult); + + verify(mockResult).error(any()); + } +} diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java index 8e778718c2c..6162a118e7b 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java @@ -52,7 +52,6 @@ public class FileUtilsTest { private Context context; - final private String externalStorageDirectoryPath = Environment.getExternalStorageDirectory().getPath(); ShadowContentResolver shadowContentResolver; ContentResolver contentResolver; @@ -79,20 +78,12 @@ public void getPathFromUri_returnsExpectedPathForExternalDocumentUri() { (Answer) invocation -> "primary:Documents/test"); String path = FileUtils.getPathFromUri(context, uri); + String externalStorageDirectoryPath = Environment.getExternalStorageDirectory().getPath(); String expectedPath = externalStorageDirectoryPath + "/Documents/test"; assertEquals(path, expectedPath); } } -// @Test -// public void getPathFromUri_returnsExpectedPathForMediaDocumentUri() { -// // Uri that represents tex -// Uri uri = Uri.parse("content://com.android.providers.media.documents/document/document%3A35"); -// String path = FileUtils.getPathFromUri(context, uri); -// String expectedPath = ""; // TODO -// assertEquals(path, expectedPath); -// } - @Test public void getPathFromUri_throwExceptionForExternalDocumentUriWithNonPrimaryStorageVolume() { // Uri that represents Documents/test directory from some external storage volume ("external" for this test): @@ -115,15 +106,12 @@ public void getPathFromUri_throwExceptionForUriWithUnhandledAuthority() { @Test public void getPathFromCopyOfFileFromUri_returnsPathWithContent() throws IOException { - Uri uri = Uri.parse("content://dummy/dummy.png"); - Context mc = mock(Context.class); - ContentResolver mcr = mock(ContentResolver.class); - when(mc.getContentResolver()).thenReturn(mcr); - when(mcr.getType(uri)).thenReturn("image/png"); - // when(contentResolver.getType(uri)).thenReturn("image/png"); // try using mock context - // shadowContentResolver.registerInputStream( - // uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); - String path = FileUtils.getPathFromCopyOfFileFromUri(mc, uri); + Uri uri = MockContentProvider.PNG_URI; + Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); + shadowContentResolver.registerInputStream( + uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); + + String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); File file = new File(path); int size = (int) file.length(); byte[] bytes = new byte[size]; @@ -133,148 +121,79 @@ public void getPathFromCopyOfFileFromUri_returnsPathWithContent() throws IOExcep buf.close(); assertTrue(bytes.length > 0); - // String fileStream = new String(bytes, UTF_8); - // assertEquals("fileStream", fileStream); + String fileStream = new String(bytes, UTF_8); + assertEquals("fileStream", fileStream); } + @Test + public void getPathFromCopyOfFileFromUri_returnsNullPathWhenSecurityExceptionThrown() throws IOException { + Uri uri = Uri.parse("content://dummy/dummy.png"); -// @Test -// public void getPathFromCopyOfFileFromUri_returnsNullPathWhenSecurityExceptionThrown() throws IOException { -// Uri uri = Uri.parse("content://dummy/dummy.png"); - -// ContentResolver mockContentResolver = mock(ContentResolver.class); -// when(mockContentResolver.openInputStream(any(Uri.class))).thenThrow(SecurityException.class); - -// Context mockContext = mock(Context.class); -// when(mockContext.getContentResolver()).thenReturn(mockContentResolver); - -// String path = FileUtils.getPathFromCopyOfFileFromUri(mockContext, uri); - -// assertNull(path); -// } - -// @Test -// public void getFileExtension_returnsExpectedTextFileExtension() throws IOException { -// Uri uri = Uri.parse("content://document/dummy.txt"); -// shadowContentResolver.registerInputStream( -// uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); -// String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); -// assertTrue(path.endsWith(".txt")); -// } - -// @Test -// public void getFileExtension_returnsExpectedImageExtension() throws IOException { -// Uri uri = Uri.parse("content://dummy/dummy.png"); -// shadowContentResolver.registerInputStream( -// uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); -// String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); -// assertTrue(path.endsWith(".jpg")); -// } - -// @Test -// public void getFileName_returnsExpectedName() throws IOException { -// Uri uri = MockContentProvider.PNG_URI; -// Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); -// shadowContentResolver.registerInputStream( -// uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); -// String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); -// assertTrue(path.endsWith("a.b.png")); -// } - -// @Test -// public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithNoExtensionInBaseName() throws IOException { -// Uri uri = MockContentProvider.NO_EXTENSION_URI; -// Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); -// shadowContentResolver.registerInputStream( -// uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); -// String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); -// assertTrue(path.endsWith("abc.png")); -// } - -// @Test -// public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithMismatchedTypeToFile() throws IOException { -// Uri uri = MockContentProvider.WEBP_URI; -// Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); -// shadowContentResolver.registerInputStream( -// uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); -// String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); -// assertTrue(path.endsWith("c.d.webp")); -// } + ContentResolver mockContentResolver = mock(ContentResolver.class); + when(mockContentResolver.openInputStream(any(Uri.class))).thenThrow(SecurityException.class); -// @Test -// public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithUnknownType() throws IOException { -// Uri uri = MockContentProvider.UNKNOWN_URI; -// Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); -// shadowContentResolver.registerInputStream( -// uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); -// String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); -// assertTrue(path.endsWith("e.f.g")); -// } + Context mockContext = mock(Context.class); + when(mockContext.getContentResolver()).thenReturn(mockContentResolver); - // @Implements(ShadowContentResolver.class) - // private static class TestShadowContentResolver extends ShadowContentResolver { - // public static final Uri PNG_URI_2 = Uri.parse("content://dummy/dummy.png"); - // public static final Uri TXT_URI = Uri.parse("content://document/dummy.txt"); - // public static final Uri PNG_URI = Uri.parse("content://dummy/a.b.png"); - // public static final Uri WEBP_URI = Uri.parse("content://dummy/c.d.png"); - // public static final Uri UNKNOWN_URI = Uri.parse("content://dummy/e.f.g"); - // public static final Uri NO_EXTENSION_URI = Uri.parse("content://dummy/abc"); + String path = FileUtils.getPathFromCopyOfFileFromUri(mockContext, uri); - // // @Override - // // public boolean onCreate() { - // // return true; - // // } + assertNull(path); + } - // // @Nullable - // // @Override - // // public Cursor query( - // // @NonNull Uri uri, - // // @Nullable String[] projection, - // // @Nullable String selection, - // // @Nullable String[] selectionArgs, - // // @Nullable String sortOrder) { - // // MatrixCursor cursor = new MatrixCursor(new String[] {MediaStore.MediaColumns.DISPLAY_NAME}); - // // cursor.addRow(new Object[] {uri.getLastPathSegment()}); - // // return cursor; - // // } + @Test + public void getFileExtension_returnsExpectedFileExtension() throws IOException { + Uri uri = MockContentProvider.TXT_URI; + Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); + shadowContentResolver.registerInputStream( + uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); + + String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); + System.out.println(path); + assertTrue(path.endsWith(".txt")); + } - // @Nullable - // @Implementation - // public String getType(@NonNull Uri uri) { - // System.out.println("HERE!!!!!"); - // if (uri.equals(TXT_URI)) return "document/txt"; - // if (uri.equals(PNG_URI)) return "image/png"; - // if (uri.equals(PNG_URI_2)) return "image/png"; - // if (uri.equals(WEBP_URI)) return "image/webp"; - // if (uri.equals(NO_EXTENSION_URI)) return "image/png"; - // return null; - // } + @Test + public void getFileName_returnsExpectedName() throws IOException { + Uri uri = MockContentProvider.PNG_URI; + Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); + shadowContentResolver.registerInputStream( + uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); + String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); + assertTrue(path.endsWith("a.b.png")); + } - // // @Nullable - // // @Override - // // public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { - // // return null; - // // } + @Test + public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithNoExtensionInBaseName() throws IOException { + Uri uri = MockContentProvider.NO_EXTENSION_URI; + Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); + shadowContentResolver.registerInputStream( + uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); + String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); + assertTrue(path.endsWith("abc.png")); + } - // // @Override - // // public int delete( - // // @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { - // // return 0; - // // } + @Test + public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithMismatchedTypeToFile() throws IOException { + Uri uri = MockContentProvider.WEBP_URI; + Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); + shadowContentResolver.registerInputStream( + uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); + String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); + assertTrue(path.endsWith("c.d.webp")); + } - // @Override - // public int update( - // @NonNull Uri uri, - // @Nullable ContentValues values, - // @Nullable String selection, - // @Nullable String[] selectionArgs) { - // return 0; - // } - // } - + @Test + public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithUnknownType() throws IOException { + Uri uri = MockContentProvider.UNKNOWN_URI; + Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); + shadowContentResolver.registerInputStream( + uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); + String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); + assertTrue(path.endsWith("e.f.g")); + } private static class MockContentProvider extends ContentProvider { - public static final Uri TXT_URI = Uri.parse("content://document/dummy.txt"); + public static final Uri TXT_URI = Uri.parse("content://dummy/dummydocument"); public static final Uri PNG_URI = Uri.parse("content://dummy/a.b.png"); public static final Uri WEBP_URI = Uri.parse("content://dummy/c.d.png"); public static final Uri UNKNOWN_URI = Uri.parse("content://dummy/e.f.g"); @@ -301,7 +220,6 @@ public Cursor query( @Nullable @Override public String getType(@NonNull Uri uri) { - System.out.println("HERE"); if (uri.equals(TXT_URI)) return "document/txt"; if (uri.equals(PNG_URI)) return "image/png"; if (uri.equals(WEBP_URI)) return "image/webp"; diff --git a/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java b/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java index e33795dfb74..46a380bed7b 100644 --- a/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java +++ b/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java @@ -1,90 +1,106 @@ -// // Copyright 2013 The Flutter Authors. All rights reserved. -// // Use of this source code is governed by a BSD-style license that can be -// // found in the LICENSE file. - -// package dev.flutter.packages.file_selector_android_example; - -// import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget; -// import static androidx.test.espresso.flutter.action.FlutterActions.click; -// import static androidx.test.espresso.flutter.assertion.FlutterAssertions.matches; -// import static androidx.test.espresso.flutter.matcher.FlutterMatchers.isExisting; -// import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withText; -// import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withValueKey; -// import static androidx.test.espresso.intent.Intents.intended; -// import static androidx.test.espresso.intent.Intents.intending; -// import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction; -// import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra; - -// import android.app.Activity; -// import android.app.Instrumentation; -// import android.content.ClipData; -// import android.content.Intent; -// import android.net.Uri; -// import androidx.test.core.app.ActivityScenario; -// import androidx.test.espresso.intent.rule.IntentsRule; -// import androidx.test.ext.junit.rules.ActivityScenarioRule; -// import org.junit.Rule; -// import org.junit.Test; - -// public class FileSelectorAndroidTest { -// @Rule -// public ActivityScenarioRule myActivityTestRule = -// new ActivityScenarioRule<>(DriverExtensionActivity.class); - -// @Rule public IntentsRule intentsRule = new IntentsRule(); - -// public void clearAnySystemDialog() { -// myActivityTestRule -// .getScenario() -// .onActivity( -// new ActivityScenario.ActivityAction() { -// @Override -// public void perform(DriverExtensionActivity activity) { -// Intent closeDialog = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); -// activity.sendBroadcast(closeDialog); -// } -// }); -// } - -// @Test -// public void openImageFile() { -// clearAnySystemDialog(); - -// final Instrumentation.ActivityResult result = -// new Instrumentation.ActivityResult( -// Activity.RESULT_OK, -// new Intent().setData(Uri.parse("content://file_selector_android_test/dummy.png"))); - -// intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(result); -// onFlutterWidget(withText("Open an image")).perform(click()); -// onFlutterWidget(withText("Press to open an image file(png, jpg)")).perform(click()); -// intended(hasAction(Intent.ACTION_OPEN_DOCUMENT)); -// onFlutterWidget(withValueKey("result_image_name")) -// .check(matches(withText("content://file_selector_android_test/dummy.png"))); // TODO: change this -// } - -// @Test -// public void openImageFiles() { -// clearAnySystemDialog(); - -// final ClipData.Item clipDataItem = -// new ClipData.Item(Uri.parse("content://file_selector_android_test/dummy.png")); // TODO: change this -// final ClipData clipData = new ClipData("", new String[0], clipDataItem); -// clipData.addItem(clipDataItem); - -// final Intent resultIntent = new Intent(); -// resultIntent.setClipData(clipData); - -// final Instrumentation.ActivityResult result = -// new Instrumentation.ActivityResult(Activity.RESULT_OK, resultIntent); -// intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(result); -// onFlutterWidget(withText("Open multiple images")).perform(click()); -// onFlutterWidget(withText("Press to open multiple images (png, jpg)")).perform(click()); - -// intended(hasAction(Intent.ACTION_OPEN_DOCUMENT)); -// intended(hasExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)); - -// onFlutterWidget(withValueKey("result_image_name0")).check(matches(isExisting())); -// onFlutterWidget(withValueKey("result_image_name1")).check(matches(isExisting())); -// } -// } +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package dev.flutter.packages.file_selector_android_example; + +import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget; +import static androidx.test.espresso.flutter.action.FlutterActions.click; +import static androidx.test.espresso.flutter.assertion.FlutterAssertions.matches; +import static androidx.test.espresso.flutter.matcher.FlutterMatchers.isExisting; +import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withText; +import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withValueKey; +import static androidx.test.espresso.intent.Intents.intended; +import static androidx.test.espresso.intent.Intents.intending; +import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction; +import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.app.Activity; +import android.app.Instrumentation; +import android.content.ClipData; +import android.content.Intent; +import android.net.Uri; +import android.view.View; +import androidx.test.core.app.ActivityScenario; +import androidx.test.espresso.flutter.api.WidgetAssertion; +import androidx.test.espresso.flutter.model.WidgetInfo; +import androidx.test.espresso.intent.rule.IntentsRule; +import androidx.test.ext.junit.rules.ActivityScenarioRule; +import org.junit.Rule; +import org.junit.Test; + +public class FileSelectorAndroidTest { + @Rule + public ActivityScenarioRule myActivityTestRule = + new ActivityScenarioRule<>(DriverExtensionActivity.class); + + @Rule public IntentsRule intentsRule = new IntentsRule(); + + public void clearAnySystemDialog() { + myActivityTestRule + .getScenario() + .onActivity( + new ActivityScenario.ActivityAction() { + @Override + public void perform(DriverExtensionActivity activity) { + Intent closeDialog = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); + activity.sendBroadcast(closeDialog); + } + }); + } + + @Test + public void openImageFile() { + clearAnySystemDialog(); + + final String fileName = "dummy.png"; + final Instrumentation.ActivityResult result = + new Instrumentation.ActivityResult( + Activity.RESULT_OK, + new Intent().setData(Uri.parse("content://file_selector_android_test/" + fileName))); + + intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(result); + onFlutterWidget(withText("Open an image")).perform(click()); + onFlutterWidget(withText("Press to open an image file(png, jpg)")).perform(click()); + intended(hasAction(Intent.ACTION_OPEN_DOCUMENT)); + onFlutterWidget(withValueKey("result_image_name")) + .check(new WidgetAssertion() { + @Override + public void check(View flutterView, WidgetInfo widgetInfo) { + String filePath = widgetInfo.getText(); + boolean isContentUri = filePath.contains("content://"); + boolean containsExpectedFileName = filePath.contains(fileName); + + assertFalse(isContentUri); + assertTrue(containsExpectedFileName); + } + }); + } + + @Test + public void openImageFiles() { + clearAnySystemDialog(); + + final ClipData.Item clipDataItem = + new ClipData.Item(Uri.parse("content://file_selector_android_test/dummy.png")); + final ClipData clipData = new ClipData("", new String[0], clipDataItem); + clipData.addItem(clipDataItem); + + final Intent resultIntent = new Intent(); + resultIntent.setClipData(clipData); + + final Instrumentation.ActivityResult result = + new Instrumentation.ActivityResult(Activity.RESULT_OK, resultIntent); + intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(result); + onFlutterWidget(withText("Open multiple images")).perform(click()); + onFlutterWidget(withText("Press to open multiple images (png, jpg)")).perform(click()); + + intended(hasAction(Intent.ACTION_OPEN_DOCUMENT)); + intended(hasExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)); + + onFlutterWidget(withValueKey("result_image_name0")).check(matches(isExisting())); + onFlutterWidget(withValueKey("result_image_name1")).check(matches(isExisting())); + } +} diff --git a/packages/file_selector/file_selector_android/example/pubspec.yaml b/packages/file_selector/file_selector_android/example/pubspec.yaml index 96261cf2f32..dfae3a01128 100644 --- a/packages/file_selector/file_selector_android/example/pubspec.yaml +++ b/packages/file_selector/file_selector_android/example/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: sdk: flutter dev_dependencies: - espresso: ^0.2.0 + espresso: ^0.3.0 flutter_test: sdk: flutter integration_test: From 6454e6a4a57ac2d93f12bd6d6807e29081507486 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Wed, 17 Apr 2024 11:10:17 -0700 Subject: [PATCH 13/21] lints + formatting + version bump --- .github/dependabot.yml | 2 + .../file_selector/file_selector/pubspec.yaml | 3 +- .../file_selector_android/CHANGELOG.md | 3 +- .../FileSelectorApiImpl.java | 12 +- .../file_selector_android/FileUtils.java | 279 +++++++------- .../FileSelectorAndroidPluginTest.java | 343 +++++++++--------- .../file_selector_android/FileUtilsTest.java | 50 +-- .../FileSelectorAndroidTest.java | 23 +- .../example/pubspec.yaml | 2 +- .../file_selector_android/pubspec.yaml | 2 +- .../plugins/imagepicker/FileUtilTest.java | 1 - 11 files changed, 366 insertions(+), 354 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d1f55572e69..65ed556ca71 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -186,6 +186,8 @@ updates: update-types: ["version-update:semver-minor", "version-update:semver-patch"] - dependency-name: "androidx.test:*" update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.robolectric:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] - package-ecosystem: "gradle" directory: "/packages/file_selector/file_selector_android/example/android/app" diff --git a/packages/file_selector/file_selector/pubspec.yaml b/packages/file_selector/file_selector/pubspec.yaml index 882d248e67e..d53b7b3efed 100644 --- a/packages/file_selector/file_selector/pubspec.yaml +++ b/packages/file_selector/file_selector/pubspec.yaml @@ -26,8 +26,7 @@ flutter: default_package: file_selector_windows dependencies: - file_selector_android: - path: ../file_selector_android/ + file_selector_android: ^0.5.0 file_selector_ios: ^0.5.0 file_selector_linux: ^0.9.2 file_selector_macos: ^0.9.3 diff --git a/packages/file_selector/file_selector_android/CHANGELOG.md b/packages/file_selector/file_selector_android/CHANGELOG.md index d71daf951fb..b8c7fc4665c 100644 --- a/packages/file_selector/file_selector_android/CHANGELOG.md +++ b/packages/file_selector/file_selector_android/CHANGELOG.md @@ -1,7 +1,8 @@ -## NEXT +## 0.5.1 * Updates minimum supported SDK version to Flutter 3.13/Dart 3.1. * Updates compileSdk version to 34. +* Modifies `getDirectoryPath`, `openFile`, and `openFiles` to return file/directory paths instead of URIs. ## 0.5.0+7 diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java index 256f441ae23..46992512968 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java @@ -4,6 +4,7 @@ package dev.flutter.packages.file_selector_android; +import android.annotation.TargetApi; import android.app.Activity; import android.content.ClipData; import android.content.ContentResolver; @@ -183,6 +184,7 @@ public void onResult(int resultCode, @Nullable Intent data) { } @Override + @TargetApi(21) public void getDirectoryPath( @Nullable String initialDirectory, @NonNull GeneratedFileSelectorApi.Result result) { if (!sdkChecker.sdkIsAtLeast(android.os.Build.VERSION_CODES.LOLLIPOP)) { @@ -204,9 +206,12 @@ public void getDirectoryPath( public void onResult(int resultCode, @Nullable Intent data) { if (resultCode == Activity.RESULT_OK && data != null) { final Uri uri = data.getData(); - final Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri)); + final Uri docUri = + DocumentsContract.buildDocumentUriUsingTree( + uri, DocumentsContract.getTreeDocumentId(uri)); try { - final String path = FileUtils.getPathFromUri(activityPluginBinding.getActivity(), docUri); + final String path = + FileUtils.getPathFromUri(activityPluginBinding.getActivity(), docUri); result.success(path); } catch (UnsupportedOperationException exception) { result.error(exception); @@ -338,7 +343,8 @@ GeneratedFileSelectorApi.FileResponse toFileResponse(@NonNull Uri uri) { return null; } - final String uriPath = FileUtils.getPathFromCopyOfFileFromUri(activityPluginBinding.getActivity(), uri); + final String uriPath = + FileUtils.getPathFromCopyOfFileFromUri(activityPluginBinding.getActivity(), uri); return new GeneratedFileSelectorApi.FileResponse.Builder() .setName(name) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index a3bd125aec0..1ee9254a094 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -23,20 +23,16 @@ package dev.flutter.packages.file_selector_android; -import android.annotation.TargetApi; import android.content.ContentResolver; -import android.content.ContentUris; import android.content.Context; import android.database.Cursor; import android.net.Uri; -import android.os.Build; import android.os.Environment; import android.provider.DocumentsContract; import android.provider.MediaStore; -import android.text.TextUtils; -import android.provider.MediaStore; import android.webkit.MimeTypeMap; -import io.flutter.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -46,147 +42,154 @@ public class FileUtils { - /** URI authority that represents access to external storage providers. */ - public static String EXTERNAL_DOCUMENT_AUTHORITY = "com.android.externalstorage.documents"; - - /** - * Retrieves path of directory represented by the specified {@code Uri}. - * - * Intended to handle any cases needed to return paths from URIS retrieved from open documents/directories - * by starting one of {@code Intent.ACTION_OPEN_FILE}, {@code Intent.ACTION_OPEN_FILES}, or - * {@code Intent.ACTION_OPEN_DOCUMENT_TREE}. - * - *

Will return the path for on-device directories, but does not handle external storage volumes. - */ - public static String getPathFromUri(Context context, Uri uri) { - String uriAuthority = uri.getAuthority(); - - if (uriAuthority.equals(EXTERNAL_DOCUMENT_AUTHORITY)) { - String uriDocumentId = DocumentsContract.getDocumentId(uri); - String documentStorageVolume = uriDocumentId.split(":")[0]; - - // Non-primary storage volumes come from SD cards, USB drives, etc. and are - // not handled here. - if (!documentStorageVolume.equals("primary")) { - throw new UnsupportedOperationException("Retrieving the path of a document from storage volume " + documentStorageVolume + " is unsupported by this plugin."); - } - String innermostDirectoryName = uriDocumentId.split(":")[1]; - String externalStorageDirectory = Environment.getExternalStorageDirectory().getPath(); - - return externalStorageDirectory + "/" + innermostDirectoryName; - } - else { - throw new UnsupportedOperationException("Retrieving the path from URIs with authority " + uriAuthority.toString() + " is unsupported by this plugin."); - } - } - - /** - * Copies the file from the given content URI to a temporary directory, retaining the original - * file name if possible. - * - *

Each file is placed in its own directory to avoid conflicts according to the following - * scheme: {cacheDir}/{randomUuid}/{fileName} - * - *

File extension is changed to match MIME type of the file, if known. Otherwise, the extension - * is left unchanged. - * - *

If the original file name is unknown, a predefined "file_selector" filename is used and the - * file extension is deduced from the mime type. - */ - public static String getPathFromCopyOfFileFromUri(final Context context, final Uri uri) { - try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { - String uuid = UUID.randomUUID().toString(); - File targetDirectory = new File(context.getCacheDir(), uuid); - targetDirectory.mkdir(); - targetDirectory.deleteOnExit(); - String fileName = getFileName(context, uri); - String extension = getFileExtension(context, uri); - - if (fileName == null) { - if (extension == null) { - throw new IllegalArgumentException("No name nor extension found for file."); - } else { - fileName = "file_selector" + extension; - } - } else if (extension != null) { - fileName = getBaseName(fileName) + extension; - } - - File file = new File(targetDirectory, fileName); - - try (OutputStream outputStream = new FileOutputStream(file)) { - copy(inputStream, outputStream); - return file.getPath(); - - } - } catch (IOException e) { - // If closing the output stream fails, we cannot be sure that the - // target file was written in full. Flushing the stream merely moves - // the bytes into the OS, not necessarily to the file. - return null; - } catch (SecurityException e) { - // Calling `ContentResolver#openInputStream()` has been reported to throw a - // `SecurityException` on some devices in certain circumstances. Instead of crashing, we - // return `null`. - // - // See https://github.com/flutter/flutter/issues/100025 for more details. - return null; - } + /** URI authority that represents access to external storage providers. */ + public static final String EXTERNAL_DOCUMENT_AUTHORITY = "com.android.externalstorage.documents"; + + /** + * Retrieves path of directory represented by the specified {@code Uri}. + * + *

Intended to handle any cases needed to return paths from URIS retrieved from open + * documents/directories by starting one of {@code Intent.ACTION_OPEN_FILE}, {@code + * Intent.ACTION_OPEN_FILES}, or {@code Intent.ACTION_OPEN_DOCUMENT_TREE}. + * + *

Will return the path for on-device directories, but does not handle external storage + * volumes. + */ + @NonNull + public static String getPathFromUri(@NonNull Context context, @NonNull Uri uri) { + String uriAuthority = uri.getAuthority(); + + if (uriAuthority.equals(EXTERNAL_DOCUMENT_AUTHORITY)) { + String uriDocumentId = DocumentsContract.getDocumentId(uri); + String documentStorageVolume = uriDocumentId.split(":")[0]; + + // Non-primary storage volumes come from SD cards, USB drives, etc. and are + // not handled here. + if (!documentStorageVolume.equals("primary")) { + throw new UnsupportedOperationException( + "Retrieving the path of a document from storage volume " + + documentStorageVolume + + " is unsupported by this plugin."); + } + String innermostDirectoryName = uriDocumentId.split(":")[1]; + String externalStorageDirectory = Environment.getExternalStorageDirectory().getPath(); + + return externalStorageDirectory + "/" + innermostDirectoryName; + } else { + throw new UnsupportedOperationException( + "Retrieving the path from URIs with authority " + + uriAuthority.toString() + + " is unsupported by this plugin."); } - - /** Returns the extension of file with dot, or null if it's empty. */ - private static String getFileExtension(Context context, Uri uriFile) { - String extension; - - try { - if (uriFile.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { - final MimeTypeMap mime = MimeTypeMap.getSingleton(); - extension = mime.getExtensionFromMimeType(context.getContentResolver().getType(uriFile)); + } + + /** + * Copies the file from the given content URI to a temporary directory, retaining the original + * file name if possible. + * + *

Each file is placed in its own directory to avoid conflicts according to the following + * scheme: {cacheDir}/{randomUuid}/{fileName} + * + *

File extension is changed to match MIME type of the file, if known. Otherwise, the extension + * is left unchanged. + * + *

If the original file name is unknown, a predefined "file_selector" filename is used and the + * file extension is deduced from the mime type. + */ + @Nullable + public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @NonNull Uri uri) { + try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { + String uuid = UUID.randomUUID().toString(); + File targetDirectory = new File(context.getCacheDir(), uuid); + targetDirectory.mkdir(); + targetDirectory.deleteOnExit(); + String fileName = getFileName(context, uri); + String extension = getFileExtension(context, uri); + + if (fileName == null) { + if (extension == null) { + throw new IllegalArgumentException("No name nor extension found for file."); } else { - extension = - MimeTypeMap.getFileExtensionFromUrl( - Uri.fromFile(new File(uriFile.getPath())).toString()); - } - } catch (Exception e) { - return null; - } - - if (extension == null || extension.isEmpty()) { - return null; + fileName = "file_selector" + extension; } - - return "." + extension; + } else if (extension != null) { + fileName = getBaseName(fileName) + extension; + } + + File file = new File(targetDirectory, fileName); + + try (OutputStream outputStream = new FileOutputStream(file)) { + copy(inputStream, outputStream); + return file.getPath(); + } + } catch (IOException e) { + // If closing the output stream fails, we cannot be sure that the + // target file was written in full. Flushing the stream merely moves + // the bytes into the OS, not necessarily to the file. + return null; + } catch (SecurityException e) { + // Calling `ContentResolver#openInputStream()` has been reported to throw a + // `SecurityException` on some devices in certain circumstances. Instead of crashing, we + // return `null`. + // + // See https://github.com/flutter/flutter/issues/100025 for more details. + return null; } - - /** Returns the name of the file provided by ContentResolver; this may be null. */ - private static String getFileName(Context context, Uri uriFile) { - try (Cursor cursor = queryFileName(context, uriFile)) { - if (cursor == null || !cursor.moveToFirst() || cursor.getColumnCount() < 1) return null; - return cursor.getString(0); - } + } + + /** Returns the extension of file with dot, or null if it's empty. */ + private static String getFileExtension(Context context, Uri uriFile) { + String extension; + + try { + if (uriFile.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { + final MimeTypeMap mime = MimeTypeMap.getSingleton(); + extension = mime.getExtensionFromMimeType(context.getContentResolver().getType(uriFile)); + } else { + extension = + MimeTypeMap.getFileExtensionFromUrl( + Uri.fromFile(new File(uriFile.getPath())).toString()); + } + } catch (Exception e) { + return null; } - private static Cursor queryFileName(Context context, Uri uriFile) { - return context - .getContentResolver() - .query(uriFile, new String[] {MediaStore.MediaColumns.DISPLAY_NAME}, null, null, null); + if (extension == null || extension.isEmpty()) { + return null; } - private static void copy(InputStream in, OutputStream out) throws IOException { - final byte[] buffer = new byte[4 * 1024]; - int bytesRead; - while ((bytesRead = in.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); - } - out.flush(); + return "." + extension; + } + + /** Returns the name of the file provided by ContentResolver; this may be null. */ + private static String getFileName(Context context, Uri uriFile) { + try (Cursor cursor = queryFileName(context, uriFile)) { + if (cursor == null || !cursor.moveToFirst() || cursor.getColumnCount() < 1) return null; + return cursor.getString(0); } + } + + private static Cursor queryFileName(Context context, Uri uriFile) { + return context + .getContentResolver() + .query(uriFile, new String[] {MediaStore.MediaColumns.DISPLAY_NAME}, null, null, null); + } + + private static void copy(InputStream in, OutputStream out) throws IOException { + final byte[] buffer = new byte[4 * 1024]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + } + out.flush(); + } - private static String getBaseName(String fileName) { - int lastDotIndex = fileName.lastIndexOf('.'); - if (lastDotIndex < 0) { - return fileName; - } - // Basename is everything before the last '.'. - return fileName.substring(0, lastDotIndex); + private static String getBaseName(String fileName) { + int lastDotIndex = fileName.lastIndexOf('.'); + if (lastDotIndex < 0) { + return fileName; } + // Basename is everything before the last '.'. + return fileName.substring(0, lastDotIndex); + } } diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java index 86a1486649d..dd4f74e0a28 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java @@ -76,195 +76,192 @@ private void mockContentResolver( @Test public void openFileReturnsSuccessfully() throws FileNotFoundException { try (MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { - final ContentResolver mockContentResolver = mock(ContentResolver.class); - - final Uri mockUri = mock(Uri.class); - final String mockUriPath = "/some/path"; - mockedFileUtils - .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri))) - .thenAnswer( - (Answer) - invocation -> mockUriPath); - mockContentResolver(mockContentResolver, mockUri, "filename", 30, "text/plain"); - - when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); - when(mockObjectFactory.newDataInputStream(any())).thenReturn(mock(DataInputStream.class)); - when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); - when(mockActivityBinding.getActivity()).thenReturn(mockActivity); - final FileSelectorApiImpl fileSelectorApi = - new FileSelectorApiImpl( - mockActivityBinding, mockObjectFactory, (version) -> Build.VERSION.SDK_INT >= version); - - final GeneratedFileSelectorApi.Result mockResult = mock(GeneratedFileSelectorApi.Result.class); - fileSelectorApi.openFile( - null, - new GeneratedFileSelectorApi.FileTypes.Builder() - .setMimeTypes(Collections.emptyList()) - .setExtensions(Collections.emptyList()) - .build(), - mockResult); - verify(mockIntent).addCategory(Intent.CATEGORY_OPENABLE); - - verify(mockActivity).startActivityForResult(mockIntent, 221); - - final ArgumentCaptor listenerArgumentCaptor = - ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); - verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); - - final Intent resultMockIntent = mock(Intent.class); - when(resultMockIntent.getData()).thenReturn(mockUri); - listenerArgumentCaptor.getValue().onActivityResult(221, Activity.RESULT_OK, resultMockIntent); - - final ArgumentCaptor fileCaptor = - ArgumentCaptor.forClass(GeneratedFileSelectorApi.FileResponse.class); - verify(mockResult).success(fileCaptor.capture()); - - final GeneratedFileSelectorApi.FileResponse file = fileCaptor.getValue(); - assertEquals(file.getBytes().length, 30); - assertEquals(file.getMimeType(), "text/plain"); - assertEquals(file.getName(), "filename"); - assertEquals(file.getSize(), (Long) 30L); - assertEquals(file.getPath(), mockUriPath); + final ContentResolver mockContentResolver = mock(ContentResolver.class); + + final Uri mockUri = mock(Uri.class); + final String mockUriPath = "/some/path"; + mockedFileUtils + .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri))) + .thenAnswer((Answer) invocation -> mockUriPath); + mockContentResolver(mockContentResolver, mockUri, "filename", 30, "text/plain"); + + when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); + when(mockObjectFactory.newDataInputStream(any())).thenReturn(mock(DataInputStream.class)); + when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); + when(mockActivityBinding.getActivity()).thenReturn(mockActivity); + final FileSelectorApiImpl fileSelectorApi = + new FileSelectorApiImpl( + mockActivityBinding, + mockObjectFactory, + (version) -> Build.VERSION.SDK_INT >= version); + + final GeneratedFileSelectorApi.Result mockResult = + mock(GeneratedFileSelectorApi.Result.class); + fileSelectorApi.openFile( + null, + new GeneratedFileSelectorApi.FileTypes.Builder() + .setMimeTypes(Collections.emptyList()) + .setExtensions(Collections.emptyList()) + .build(), + mockResult); + verify(mockIntent).addCategory(Intent.CATEGORY_OPENABLE); + + verify(mockActivity).startActivityForResult(mockIntent, 221); + + final ArgumentCaptor listenerArgumentCaptor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); + + final Intent resultMockIntent = mock(Intent.class); + when(resultMockIntent.getData()).thenReturn(mockUri); + listenerArgumentCaptor.getValue().onActivityResult(221, Activity.RESULT_OK, resultMockIntent); + + final ArgumentCaptor fileCaptor = + ArgumentCaptor.forClass(GeneratedFileSelectorApi.FileResponse.class); + verify(mockResult).success(fileCaptor.capture()); + + final GeneratedFileSelectorApi.FileResponse file = fileCaptor.getValue(); + assertEquals(file.getBytes().length, 30); + assertEquals(file.getMimeType(), "text/plain"); + assertEquals(file.getName(), "filename"); + assertEquals(file.getSize(), (Long) 30L); + assertEquals(file.getPath(), mockUriPath); } } @SuppressWarnings({"rawtypes", "unchecked"}) @Test public void openFilesReturnsSuccessfully() throws FileNotFoundException { - try (MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { - - final ContentResolver mockContentResolver = mock(ContentResolver.class); - - final Uri mockUri = mock(Uri.class); - final String mockUriPath = "some/path/"; - mockedFileUtils - .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri))) - .thenAnswer( - (Answer) - invocation -> mockUriPath); - mockContentResolver(mockContentResolver, mockUri, "filename", 30, "text/plain"); - - final Uri mockUri2 = mock(Uri.class); - final String mockUri2Path = "some/other/path/"; - mockedFileUtils - .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri2))) - .thenAnswer( - (Answer) - invocation -> mockUri2Path); - mockContentResolver(mockContentResolver, mockUri2, "filename2", 40, "image/jpg"); - - when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); - when(mockObjectFactory.newDataInputStream(any())).thenReturn(mock(DataInputStream.class)); - when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); - when(mockActivityBinding.getActivity()).thenReturn(mockActivity); - final FileSelectorApiImpl fileSelectorApi = - new FileSelectorApiImpl( - mockActivityBinding, mockObjectFactory, (version) -> Build.VERSION.SDK_INT >= version); - - final GeneratedFileSelectorApi.Result mockResult = mock(GeneratedFileSelectorApi.Result.class); - fileSelectorApi.openFiles( - null, - new GeneratedFileSelectorApi.FileTypes.Builder() - .setMimeTypes(Collections.emptyList()) - .setExtensions(Collections.emptyList()) - .build(), - mockResult); - verify(mockIntent).addCategory(Intent.CATEGORY_OPENABLE); - verify(mockIntent).putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); - - verify(mockActivity).startActivityForResult(mockIntent, 222); - - final ArgumentCaptor listenerArgumentCaptor = - ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); - verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); - - final Intent resultMockIntent = mock(Intent.class); - final ClipData mockClipData = mock(ClipData.class); - when(mockClipData.getItemCount()).thenReturn(2); - - final ClipData.Item mockClipDataItem = mock(ClipData.Item.class); - when(mockClipDataItem.getUri()).thenReturn(mockUri); - when(mockClipData.getItemAt(0)).thenReturn(mockClipDataItem); - - final ClipData.Item mockClipDataItem2 = mock(ClipData.Item.class); - when(mockClipDataItem2.getUri()).thenReturn(mockUri2); - when(mockClipData.getItemAt(1)).thenReturn(mockClipDataItem2); - - when(resultMockIntent.getClipData()).thenReturn(mockClipData); - - listenerArgumentCaptor.getValue().onActivityResult(222, Activity.RESULT_OK, resultMockIntent); - - final ArgumentCaptor fileListCaptor = ArgumentCaptor.forClass(List.class); - verify(mockResult).success(fileListCaptor.capture()); - - final List fileList = fileListCaptor.getValue(); - assertEquals(fileList.get(0).getBytes().length, 30); - assertEquals(fileList.get(0).getMimeType(), "text/plain"); - assertEquals(fileList.get(0).getName(), "filename"); - assertEquals(fileList.get(0).getSize(), (Long) 30L); - assertEquals(fileList.get(0).getPath(), mockUriPath); - - assertEquals(fileList.get(1).getBytes().length, 40); - assertEquals(fileList.get(1).getMimeType(), "image/jpg"); - assertEquals(fileList.get(1).getName(), "filename2"); - assertEquals(fileList.get(1).getSize(), (Long) 40L); - assertEquals(fileList.get(1).getPath(), mockUri2Path); - } + try (MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { + + final ContentResolver mockContentResolver = mock(ContentResolver.class); + + final Uri mockUri = mock(Uri.class); + final String mockUriPath = "some/path/"; + mockedFileUtils + .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri))) + .thenAnswer((Answer) invocation -> mockUriPath); + mockContentResolver(mockContentResolver, mockUri, "filename", 30, "text/plain"); + + final Uri mockUri2 = mock(Uri.class); + final String mockUri2Path = "some/other/path/"; + mockedFileUtils + .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri2))) + .thenAnswer((Answer) invocation -> mockUri2Path); + mockContentResolver(mockContentResolver, mockUri2, "filename2", 40, "image/jpg"); + + when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); + when(mockObjectFactory.newDataInputStream(any())).thenReturn(mock(DataInputStream.class)); + when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); + when(mockActivityBinding.getActivity()).thenReturn(mockActivity); + final FileSelectorApiImpl fileSelectorApi = + new FileSelectorApiImpl( + mockActivityBinding, + mockObjectFactory, + (version) -> Build.VERSION.SDK_INT >= version); + + final GeneratedFileSelectorApi.Result mockResult = + mock(GeneratedFileSelectorApi.Result.class); + fileSelectorApi.openFiles( + null, + new GeneratedFileSelectorApi.FileTypes.Builder() + .setMimeTypes(Collections.emptyList()) + .setExtensions(Collections.emptyList()) + .build(), + mockResult); + verify(mockIntent).addCategory(Intent.CATEGORY_OPENABLE); + verify(mockIntent).putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + + verify(mockActivity).startActivityForResult(mockIntent, 222); + + final ArgumentCaptor listenerArgumentCaptor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); + + final Intent resultMockIntent = mock(Intent.class); + final ClipData mockClipData = mock(ClipData.class); + when(mockClipData.getItemCount()).thenReturn(2); + + final ClipData.Item mockClipDataItem = mock(ClipData.Item.class); + when(mockClipDataItem.getUri()).thenReturn(mockUri); + when(mockClipData.getItemAt(0)).thenReturn(mockClipDataItem); + + final ClipData.Item mockClipDataItem2 = mock(ClipData.Item.class); + when(mockClipDataItem2.getUri()).thenReturn(mockUri2); + when(mockClipData.getItemAt(1)).thenReturn(mockClipDataItem2); + + when(resultMockIntent.getClipData()).thenReturn(mockClipData); + + listenerArgumentCaptor.getValue().onActivityResult(222, Activity.RESULT_OK, resultMockIntent); + + final ArgumentCaptor fileListCaptor = ArgumentCaptor.forClass(List.class); + verify(mockResult).success(fileListCaptor.capture()); + + final List fileList = fileListCaptor.getValue(); + assertEquals(fileList.get(0).getBytes().length, 30); + assertEquals(fileList.get(0).getMimeType(), "text/plain"); + assertEquals(fileList.get(0).getName(), "filename"); + assertEquals(fileList.get(0).getSize(), (Long) 30L); + assertEquals(fileList.get(0).getPath(), mockUriPath); + + assertEquals(fileList.get(1).getBytes().length, 40); + assertEquals(fileList.get(1).getMimeType(), "image/jpg"); + assertEquals(fileList.get(1).getName(), "filename2"); + assertEquals(fileList.get(1).getSize(), (Long) 40L); + assertEquals(fileList.get(1).getPath(), mockUri2Path); + } } @SuppressWarnings({"rawtypes", "unchecked"}) @Test public void getDirectoryPathReturnsSuccessfully() { - try (MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { - final Uri mockUri = mock(Uri.class); - final String mockUriPath = "some/path/"; - final String mockUriId = "someId"; - final Uri mockUriUsingTree = mock(Uri.class); + try (MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { + final Uri mockUri = mock(Uri.class); + final String mockUriPath = "some/path/"; + final String mockUriId = "someId"; + final Uri mockUriUsingTree = mock(Uri.class); - mockedFileUtils - .when(() -> FileUtils.getPathFromUri(any(Context.class), eq(mockUriUsingTree))) - .thenAnswer( - (Answer) - invocation -> mockUriPath); + mockedFileUtils + .when(() -> FileUtils.getPathFromUri(any(Context.class), eq(mockUriUsingTree))) + .thenAnswer((Answer) invocation -> mockUriPath); - try (MockedStatic mockedDocumentsContract = mockStatic(DocumentsContract.class)) { + try (MockedStatic mockedDocumentsContract = + mockStatic(DocumentsContract.class)) { - mockedDocumentsContract + mockedDocumentsContract .when(() -> DocumentsContract.getTreeDocumentId(mockUri)) - .thenAnswer( - (Answer) - invocation -> mockUriId); - mockedDocumentsContract + .thenAnswer((Answer) invocation -> mockUriId); + mockedDocumentsContract .when(() -> DocumentsContract.buildDocumentUriUsingTree(mockUri, mockUriId)) - .thenAnswer( - (Answer) - invocation -> mockUriUsingTree); - - - when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT_TREE)).thenReturn(mockIntent); - when(mockActivityBinding.getActivity()).thenReturn(mockActivity); - final FileSelectorApiImpl fileSelectorApi = - new FileSelectorApiImpl( - mockActivityBinding, - mockObjectFactory, - (version) -> Build.VERSION_CODES.LOLLIPOP >= version); - - final GeneratedFileSelectorApi.Result mockResult = mock(GeneratedFileSelectorApi.Result.class); - fileSelectorApi.getDirectoryPath(null, mockResult); - - verify(mockActivity).startActivityForResult(mockIntent, 223); - - final ArgumentCaptor listenerArgumentCaptor = - ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); - verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); - - final Intent resultMockIntent = mock(Intent.class); - when(resultMockIntent.getData()).thenReturn(mockUri); - listenerArgumentCaptor.getValue().onActivityResult(223, Activity.RESULT_OK, resultMockIntent); - - verify(mockResult).success(mockUriPath); - } - } + .thenAnswer((Answer) invocation -> mockUriUsingTree); + + when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT_TREE)).thenReturn(mockIntent); + when(mockActivityBinding.getActivity()).thenReturn(mockActivity); + final FileSelectorApiImpl fileSelectorApi = + new FileSelectorApiImpl( + mockActivityBinding, + mockObjectFactory, + (version) -> Build.VERSION_CODES.LOLLIPOP >= version); + + final GeneratedFileSelectorApi.Result mockResult = + mock(GeneratedFileSelectorApi.Result.class); + fileSelectorApi.getDirectoryPath(null, mockResult); + + verify(mockActivity).startActivityForResult(mockIntent, 223); + + final ArgumentCaptor listenerArgumentCaptor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); + + final Intent resultMockIntent = mock(Intent.class); + when(resultMockIntent.getData()).thenReturn(mockUri); + listenerArgumentCaptor + .getValue() + .onActivityResult(223, Activity.RESULT_OK, resultMockIntent); + + verify(mockResult).success(mockUriPath); + } + } } @Test diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java index 6162a118e7b..0f9f1a0c676 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java @@ -39,14 +39,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockedStatic; +import org.mockito.stubbing.Answer; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Implementation; -import org.robolectric.annotation.Implements; import org.robolectric.shadows.ShadowContentResolver; -import org.robolectric.shadows.ShadowEnvironment; import org.robolectric.shadows.ShadowMimeTypeMap; -import org.mockito.stubbing.Answer; @RunWith(RobolectricTestRunner.class) public class FileUtilsTest { @@ -70,13 +67,14 @@ public void before() { @Test public void getPathFromUri_returnsExpectedPathForExternalDocumentUri() { // Uri that represents Documents/test directory on device: - Uri uri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3ADocuments%2Ftest"); - try (MockedStatic mockedDocumentsContract = mockStatic(DocumentsContract.class)) { - mockedDocumentsContract - .when(() -> DocumentsContract.getDocumentId(uri)) - .thenAnswer( - (Answer) - invocation -> "primary:Documents/test"); + Uri uri = + Uri.parse( + "content://com.android.externalstorage.documents/tree/primary%3ADocuments%2Ftest"); + try (MockedStatic mockedDocumentsContract = + mockStatic(DocumentsContract.class)) { + mockedDocumentsContract + .when(() -> DocumentsContract.getDocumentId(uri)) + .thenAnswer((Answer) invocation -> "primary:Documents/test"); String path = FileUtils.getPathFromUri(context, uri); String externalStorageDirectoryPath = Environment.getExternalStorageDirectory().getPath(); String expectedPath = externalStorageDirectoryPath + "/Documents/test"; @@ -87,14 +85,16 @@ public void getPathFromUri_returnsExpectedPathForExternalDocumentUri() { @Test public void getPathFromUri_throwExceptionForExternalDocumentUriWithNonPrimaryStorageVolume() { // Uri that represents Documents/test directory from some external storage volume ("external" for this test): - Uri uri = Uri.parse("content://com.android.externalstorage.documents/tree/external%3ADocuments%2Ftest"); - try (MockedStatic mockedDocumentsContract = mockStatic(DocumentsContract.class)) { - mockedDocumentsContract - .when(() -> DocumentsContract.getDocumentId(uri)) - .thenAnswer( - (Answer) - invocation -> "external:Documents/test"); - assertThrows(UnsupportedOperationException.class, () -> FileUtils.getPathFromUri(context, uri)); + Uri uri = + Uri.parse( + "content://com.android.externalstorage.documents/tree/external%3ADocuments%2Ftest"); + try (MockedStatic mockedDocumentsContract = + mockStatic(DocumentsContract.class)) { + mockedDocumentsContract + .when(() -> DocumentsContract.getDocumentId(uri)) + .thenAnswer((Answer) invocation -> "external:Documents/test"); + assertThrows( + UnsupportedOperationException.class, () -> FileUtils.getPathFromUri(context, uri)); } } @@ -126,7 +126,8 @@ public void getPathFromCopyOfFileFromUri_returnsPathWithContent() throws IOExcep } @Test - public void getPathFromCopyOfFileFromUri_returnsNullPathWhenSecurityExceptionThrown() throws IOException { + public void getPathFromCopyOfFileFromUri_returnsNullPathWhenSecurityExceptionThrown() + throws IOException { Uri uri = Uri.parse("content://dummy/dummy.png"); ContentResolver mockContentResolver = mock(ContentResolver.class); @@ -163,7 +164,8 @@ public void getFileName_returnsExpectedName() throws IOException { } @Test - public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithNoExtensionInBaseName() throws IOException { + public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithNoExtensionInBaseName() + throws IOException { Uri uri = MockContentProvider.NO_EXTENSION_URI; Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); shadowContentResolver.registerInputStream( @@ -173,7 +175,8 @@ public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithNoExtensi } @Test - public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithMismatchedTypeToFile() throws IOException { + public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithMismatchedTypeToFile() + throws IOException { Uri uri = MockContentProvider.WEBP_URI; Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); shadowContentResolver.registerInputStream( @@ -183,7 +186,8 @@ public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithMismatched } @Test - public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithUnknownType() throws IOException { + public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithUnknownType() + throws IOException { Uri uri = MockContentProvider.UNKNOWN_URI; Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); shadowContentResolver.registerInputStream( diff --git a/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java b/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java index 46a380bed7b..d4798092746 100644 --- a/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java +++ b/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java @@ -66,17 +66,18 @@ public void openImageFile() { onFlutterWidget(withText("Press to open an image file(png, jpg)")).perform(click()); intended(hasAction(Intent.ACTION_OPEN_DOCUMENT)); onFlutterWidget(withValueKey("result_image_name")) - .check(new WidgetAssertion() { - @Override - public void check(View flutterView, WidgetInfo widgetInfo) { - String filePath = widgetInfo.getText(); - boolean isContentUri = filePath.contains("content://"); - boolean containsExpectedFileName = filePath.contains(fileName); - - assertFalse(isContentUri); - assertTrue(containsExpectedFileName); - } - }); + .check( + new WidgetAssertion() { + @Override + public void check(View flutterView, WidgetInfo widgetInfo) { + String filePath = widgetInfo.getText(); + boolean isContentUri = filePath.contains("content://"); + boolean containsExpectedFileName = filePath.contains(fileName); + + assertFalse(isContentUri); + assertTrue(containsExpectedFileName); + } + }); } @Test diff --git a/packages/file_selector/file_selector_android/example/pubspec.yaml b/packages/file_selector/file_selector_android/example/pubspec.yaml index dfae3a01128..96261cf2f32 100644 --- a/packages/file_selector/file_selector_android/example/pubspec.yaml +++ b/packages/file_selector/file_selector_android/example/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: sdk: flutter dev_dependencies: - espresso: ^0.3.0 + espresso: ^0.2.0 flutter_test: sdk: flutter integration_test: diff --git a/packages/file_selector/file_selector_android/pubspec.yaml b/packages/file_selector/file_selector_android/pubspec.yaml index 9f0843771ff..6cb289b391a 100644 --- a/packages/file_selector/file_selector_android/pubspec.yaml +++ b/packages/file_selector/file_selector_android/pubspec.yaml @@ -2,7 +2,7 @@ name: file_selector_android description: Android implementation of the file_selector package. repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.5.0+7 +version: 0.5.1 environment: sdk: ^3.1.0 diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java index 6dc9023f9c2..620bac74a17 100644 --- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java @@ -62,7 +62,6 @@ public void FileUtil_GetPathFromUri() throws IOException { shadowContentResolver.registerInputStream( uri, new ByteArrayInputStream("imageStream".getBytes(UTF_8))); String path = fileUtils.getPathFromUri(context, uri); - System.out.println(path); File file = new File(path); int size = (int) file.length(); byte[] bytes = new byte[size]; From c454c09847ade564814158a53cc28039cf45730d Mon Sep 17 00:00:00 2001 From: camsim99 Date: Wed, 17 Apr 2024 11:14:43 -0700 Subject: [PATCH 14/21] Bump roblectric --- .../file_selector/file_selector_android/android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/file_selector/file_selector_android/android/build.gradle b/packages/file_selector/file_selector_android/android/build.gradle index 2132f6b1a15..9bdba2f34f9 100644 --- a/packages/file_selector/file_selector_android/android/build.gradle +++ b/packages/file_selector/file_selector_android/android/build.gradle @@ -42,7 +42,7 @@ android { testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-inline:5.1.0' testImplementation 'androidx.test:core:1.3.0' - testImplementation "org.robolectric:robolectric:4.10.3" + testImplementation "org.robolectric:robolectric:4.12.1" // org.jetbrains.kotlin:kotlin-bom artifact purpose is to align kotlin stdlib and related code versions. // See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7 From 2874a19dd7b30863643a0c2b4169668d32f971c7 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Wed, 17 Apr 2024 11:17:24 -0700 Subject: [PATCH 15/21] Add doc --- .../dev/flutter/packages/file_selector_android/FileUtils.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 1ee9254a094..49e09853c03 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -95,6 +95,9 @@ public static String getPathFromUri(@NonNull Context context, @NonNull Uri uri) * *

If the original file name is unknown, a predefined "file_selector" filename is used and the * file extension is deduced from the mime type. + * + *

Will return null if closing the output stream when done copying file contents does not for + * sure complete or if a security exception is encountered when opening the input stream. */ @Nullable public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @NonNull Uri uri) { From 374d8643b70fe5c046e5321a0b60d5faadfeedfc Mon Sep 17 00:00:00 2001 From: camsim99 Date: Wed, 17 Apr 2024 13:33:01 -0700 Subject: [PATCH 16/21] Fix weird typo --- .../packages/file_selector_android/FileUtilsTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java index 0f9f1a0c676..760874317ef 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java @@ -58,10 +58,10 @@ public void before() { contentResolver = spy(context.getContentResolver()); shadowContentResolver = shadowOf(context.getContentResolver()); ShadowMimeTypeMap mimeTypeMap = shadowOf(MimeTypeMap.getSingleton()); - mimeTypeMap.addExtensionMimeTypMapping("txt", "document/txt"); - mimeTypeMap.addExtensionMimeTypMapping("jpg", "image/jpeg"); - mimeTypeMap.addExtensionMimeTypMapping("png", "image/png"); - mimeTypeMap.addExtensionMimeTypMapping("webp", "image/webp"); + mimeTypeMap.addExtensionMimeTypeMapping("txt", "document/txt"); + mimeTypeMap.addExtensionMimeTypeMapping("jpg", "image/jpeg"); + mimeTypeMap.addExtensionMimeTypeMapping("png", "image/png"); + mimeTypeMap.addExtensionMimeTypeMapping("webp", "image/webp"); } @Test From b4ce04cee824e648d2d9b3eb66bb83a7946b9beb Mon Sep 17 00:00:00 2001 From: Camille Simon <43054281+camsim99@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:19:47 -0700 Subject: [PATCH 17/21] Update packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java Co-authored-by: Gray Mackall <34871572+gmackall@users.noreply.github.com> --- .../dev/flutter/packages/file_selector_android/FileUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 49e09853c03..d8269522d49 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -48,7 +48,7 @@ public class FileUtils { /** * Retrieves path of directory represented by the specified {@code Uri}. * - *

Intended to handle any cases needed to return paths from URIS retrieved from open + *

Intended to handle any cases needed to return paths from URIs retrieved from open * documents/directories by starting one of {@code Intent.ACTION_OPEN_FILE}, {@code * Intent.ACTION_OPEN_FILES}, or {@code Intent.ACTION_OPEN_DOCUMENT_TREE}. * From 2173f0d0cbe0579531fa420ae5c33767f94592e9 Mon Sep 17 00:00:00 2001 From: Camille Simon <43054281+camsim99@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:20:05 -0700 Subject: [PATCH 18/21] Update packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java Co-authored-by: Gray Mackall <34871572+gmackall@users.noreply.github.com> --- .../dev/flutter/packages/file_selector_android/FileUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index d8269522d49..e730415f43c 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -59,7 +59,7 @@ public class FileUtils { public static String getPathFromUri(@NonNull Context context, @NonNull Uri uri) { String uriAuthority = uri.getAuthority(); - if (uriAuthority.equals(EXTERNAL_DOCUMENT_AUTHORITY)) { + if (EXTERNAL_DOCUMENT_AUTHORITY.equals(uriAuthority)) { String uriDocumentId = DocumentsContract.getDocumentId(uri); String documentStorageVolume = uriDocumentId.split(":")[0]; From a027a991217e13a79fd150e60785933d5a2187f9 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Thu, 18 Apr 2024 12:40:17 -0700 Subject: [PATCH 19/21] Address review --- .../FileSelectorApiImpl.java | 12 ++++++++++++ .../file_selector_android/FileUtils.java | 16 ++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java index 46992512968..a20ab00e58e 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java @@ -107,6 +107,12 @@ public void openFile( public void onResult(int resultCode, @Nullable Intent data) { if (resultCode == Activity.RESULT_OK && data != null) { final Uri uri = data.getData(); + if (uri == null) { + // No data retrieved from opening file. + result.error(new Exception("Failed to retrieve data from opening file.")); + return; + } + final GeneratedFileSelectorApi.FileResponse file = toFileResponse(uri); if (file != null) { result.success(file); @@ -206,6 +212,12 @@ public void getDirectoryPath( public void onResult(int resultCode, @Nullable Intent data) { if (resultCode == Activity.RESULT_OK && data != null) { final Uri uri = data.getData(); + if (uri == null) { + // No data retrieved from opening directory. + result.error(new Exception("Failed to retrieve data from opening directory.")); + return; + } + final Uri docUri = DocumentsContract.buildDocumentUriUsingTree( uri, DocumentsContract.getTreeDocumentId(uri)); diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 49e09853c03..d13a65eb9b4 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -61,7 +61,15 @@ public static String getPathFromUri(@NonNull Context context, @NonNull Uri uri) if (uriAuthority.equals(EXTERNAL_DOCUMENT_AUTHORITY)) { String uriDocumentId = DocumentsContract.getDocumentId(uri); - String documentStorageVolume = uriDocumentId.split(":")[0]; + String[] uriDocumentIdSplit = uriDocumentId.split(":"); + + if (uriDocumentIdSplit.length < 2) { + // We expect the URI document ID to contain its storage volume and name to determine its path. + throw new UnsupportedOperationException( + "Retrieving the path of a document with an unknown storage volume or name is unsupported by this plugin."); + } + + String documentStorageVolume = uriDocumentIdSplit[0]; // Non-primary storage volumes come from SD cards, USB drives, etc. and are // not handled here. @@ -71,7 +79,7 @@ public static String getPathFromUri(@NonNull Context context, @NonNull Uri uri) + documentStorageVolume + " is unsupported by this plugin."); } - String innermostDirectoryName = uriDocumentId.split(":")[1]; + String innermostDirectoryName = uriDocumentIdSplit[1]; String externalStorageDirectory = Environment.getExternalStorageDirectory().getPath(); return externalStorageDirectory + "/" + innermostDirectoryName; @@ -96,8 +104,8 @@ public static String getPathFromUri(@NonNull Context context, @NonNull Uri uri) *

If the original file name is unknown, a predefined "file_selector" filename is used and the * file extension is deduced from the mime type. * - *

Will return null if closing the output stream when done copying file contents does not for - * sure complete or if a security exception is encountered when opening the input stream. + *

Will return null if copying the URI contents into a new file does not complete successfully + * or if a security exception is encountered when opening the input stream to start the copying. */ @Nullable public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @NonNull Uri uri) { From 697bdfa0783739a4e40cde56af5bb6fda47ba3c3 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Fri, 19 Apr 2024 12:01:28 -0700 Subject: [PATCH 20/21] Address review pt 2 --- .../file_selector_android/FileUtils.java | 22 ++++++++-------- .../FileSelectorAndroidTest.java | 26 ++++++++++++++----- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 0f0b177123e..84a8b44f4c5 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -110,7 +110,7 @@ public static String getPathFromUri(@NonNull Context context, @NonNull Uri uri) @Nullable public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @NonNull Uri uri) { try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { - String uuid = UUID.randomUUID().toString(); + String uuid = UUID.nameUUIDFromBytes(uri.toString().getBytes()).toString(); File targetDirectory = new File(context.getCacheDir(), uuid); targetDirectory.mkdir(); targetDirectory.deleteOnExit(); @@ -152,17 +152,17 @@ public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @Non private static String getFileExtension(Context context, Uri uriFile) { String extension; - try { - if (uriFile.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { - final MimeTypeMap mime = MimeTypeMap.getSingleton(); - extension = mime.getExtensionFromMimeType(context.getContentResolver().getType(uriFile)); - } else { - extension = - MimeTypeMap.getFileExtensionFromUrl( - Uri.fromFile(new File(uriFile.getPath())).toString()); + if (uriFile.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { + final MimeTypeMap mime = MimeTypeMap.getSingleton(); + extension = mime.getExtensionFromMimeType(context.getContentResolver().getType(uriFile)); + } else { + try { + Uri uriFromFile = Uri.fromFile(new File(uriFile.getPath())); + extension = MimeTypeMap.getFileExtensionFromUrl(uriFromFile.toString()); + } catch (NullPointerException e) { + // File created from uriFile was null. + return null; } - } catch (Exception e) { - return null; } if (extension == null || extension.isEmpty()) { diff --git a/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java b/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java index d4798092746..b7f2390d019 100644 --- a/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java +++ b/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java @@ -14,8 +14,7 @@ import static androidx.test.espresso.intent.Intents.intending; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; import android.app.Activity; import android.app.Instrumentation; @@ -28,6 +27,7 @@ import androidx.test.espresso.flutter.model.WidgetInfo; import androidx.test.espresso.intent.rule.IntentsRule; import androidx.test.ext.junit.rules.ActivityScenarioRule; +import java.util.UUID; import org.junit.Rule; import org.junit.Test; @@ -71,11 +71,23 @@ public void openImageFile() { @Override public void check(View flutterView, WidgetInfo widgetInfo) { String filePath = widgetInfo.getText(); - boolean isContentUri = filePath.contains("content://"); - boolean containsExpectedFileName = filePath.contains(fileName); - - assertFalse(isContentUri); - assertTrue(containsExpectedFileName); + String expectedContentUri = "content://file_selector_android_test/dummy.png"; + String expectedContentUriUuid = + UUID.nameUUIDFromBytes(expectedContentUri.toString().getBytes()).toString(); + + myActivityTestRule + .getScenario() + .onActivity( + activity -> { + String expectedCacheDirectory = activity.getCacheDir().getPath(); + String expectedFilePath = + expectedCacheDirectory + + "/" + + expectedContentUriUuid + + "/" + + fileName; + assertEquals(filePath, expectedFilePath); + }); } }); } From ac6a8835727e87661179c8de3a9d9cbaf9399b01 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Mon, 29 Apr 2024 16:14:34 -0400 Subject: [PATCH 21/21] Add primary constant --- .../dev/flutter/packages/file_selector_android/FileUtils.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 84a8b44f4c5..5d0b61312b3 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -73,6 +73,9 @@ public static String getPathFromUri(@NonNull Context context, @NonNull Uri uri) // Non-primary storage volumes come from SD cards, USB drives, etc. and are // not handled here. + // + // Constant for primary storage volumes found at + // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/provider/DocumentsContract.java;l=255?q=Documentscont&ss=android%2Fplatform%2Fsuperproject%2Fmain. if (!documentStorageVolume.equals("primary")) { throw new UnsupportedOperationException( "Retrieving the path of a document from storage volume "