diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3b91af30f5..f010a280e1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -159,6 +159,34 @@ updates: - dependency-name: "*" update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - package-ecosystem: "gradle" + directory: "/packages/file_selector/file_selector_android/android" + commit-message: + prefix: "[file_selector]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "com.android.tools.build:gradle" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "junit:junit" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.mockito:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "androidx.test:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/file_selector/file_selector_android/example/android/app" + commit-message: + prefix: "[file_selector]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - package-ecosystem: "gradle" directory: "/packages/flutter_adaptive_scaffold/example/android/app" commit-message: diff --git a/packages/file_selector/file_selector_android/AUTHORS b/packages/file_selector/file_selector_android/AUTHORS new file mode 100644 index 0000000000..557dff9793 --- /dev/null +++ b/packages/file_selector/file_selector_android/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. diff --git a/packages/file_selector/file_selector_android/CHANGELOG.md b/packages/file_selector/file_selector_android/CHANGELOG.md new file mode 100644 index 0000000000..bf1b6dba45 --- /dev/null +++ b/packages/file_selector/file_selector_android/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.5.0 + +* Implements file_selector_platform_interface for Android. diff --git a/packages/file_selector/file_selector_android/LICENSE b/packages/file_selector/file_selector_android/LICENSE new file mode 100644 index 0000000000..c6823b81eb --- /dev/null +++ b/packages/file_selector/file_selector_android/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/file_selector/file_selector_android/README.md b/packages/file_selector/file_selector_android/README.md new file mode 100644 index 0000000000..0a24663376 --- /dev/null +++ b/packages/file_selector/file_selector_android/README.md @@ -0,0 +1,15 @@ +# file\_selector\_android + +The Android implementation of [`file_selector`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `file_selector` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. + +[1]: https://pub.dev/packages/file_selector +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/file_selector/file_selector_android/android/build.gradle b/packages/file_selector/file_selector_android/android/build.gradle new file mode 100644 index 0000000000..4978befd6f --- /dev/null +++ b/packages/file_selector/file_selector_android/android/build.gradle @@ -0,0 +1,64 @@ +group 'dev.flutter.packages.file_selector_android' +version '1.0' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'dev.flutter.packages.file_selector_android' + } + compileSdkVersion 33 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion 19 + } + + dependencies { + implementation 'androidx.annotation:annotation:1.5.0' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-inline:5.1.0' + testImplementation 'androidx.test:core:1.3.0' + } + + lintOptions { + checkAllWarnings true + warningsAsErrors true + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' + } + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/packages/file_selector/file_selector_android/android/settings.gradle b/packages/file_selector/file_selector_android/android/settings.gradle new file mode 100644 index 0000000000..679b28be66 --- /dev/null +++ b/packages/file_selector/file_selector_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'file_selector_android' diff --git a/packages/file_selector/file_selector_android/android/src/main/AndroidManifest.xml b/packages/file_selector/file_selector_android/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..69446ed3d5 --- /dev/null +++ b/packages/file_selector/file_selector_android/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPlugin.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPlugin.java new file mode 100644 index 0000000000..27b79068ea --- /dev/null +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPlugin.java @@ -0,0 +1,59 @@ +// 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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; + +/** Native portion of the Android platform implementation of the file_selector plugin. */ +public class FileSelectorAndroidPlugin implements FlutterPlugin, ActivityAware { + @Nullable private FileSelectorApiImpl fileSelectorApi; + private FlutterPluginBinding pluginBinding; + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + pluginBinding = binding; + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + pluginBinding = null; + } + + @Override + public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { + fileSelectorApi = new FileSelectorApiImpl(binding); + GeneratedFileSelectorApi.FileSelectorApi.setup( + pluginBinding.getBinaryMessenger(), fileSelectorApi); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + if (fileSelectorApi != null) { + fileSelectorApi.setActivityPluginBinding(null); + } + } + + @Override + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { + if (fileSelectorApi != null) { + fileSelectorApi.setActivityPluginBinding(binding); + } else { + fileSelectorApi = new FileSelectorApiImpl(binding); + GeneratedFileSelectorApi.FileSelectorApi.setup( + pluginBinding.getBinaryMessenger(), fileSelectorApi); + } + } + + @Override + public void onDetachedFromActivity() { + if (fileSelectorApi != null) { + fileSelectorApi.setActivityPluginBinding(null); + } + } +} 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 new file mode 100644 index 0000000000..523ea30f68 --- /dev/null +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java @@ -0,0 +1,327 @@ +// 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.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.DocumentsContract; +import android.provider.OpenableColumns; +import android.util.Log; +import android.webkit.MimeTypeMap; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.PluginRegistry; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class FileSelectorApiImpl implements GeneratedFileSelectorApi.FileSelectorApi { + private static final String TAG = "FileSelectorApiImpl"; + // Request code for selecting a file. + private static final int OPEN_FILE = 221; + // Request code for selecting files. + private static final int OPEN_FILES = 222; + // Request code for selecting a directory. + private static final int OPEN_DIR = 223; + + private final NativeObjectFactory objectFactory; + @Nullable ActivityPluginBinding activityPluginBinding; + + private abstract static class OnResultListener { + public abstract void onResult(int resultCode, @Nullable Intent data); + } + + // Handles instantiating class objects that are needed by this class. This is provided to be + // overridden for tests. + @VisibleForTesting + static class NativeObjectFactory { + @NonNull + Intent newIntent(@NonNull String action) { + return new Intent(action); + } + + @NonNull + DataInputStream newDataInputStream(InputStream inputStream) { + return new DataInputStream(inputStream); + } + } + + public FileSelectorApiImpl(@NonNull ActivityPluginBinding activityPluginBinding) { + this(activityPluginBinding, new NativeObjectFactory()); + } + + @VisibleForTesting + FileSelectorApiImpl( + @NonNull ActivityPluginBinding activityPluginBinding, + @NonNull NativeObjectFactory objectFactory) { + this.activityPluginBinding = activityPluginBinding; + this.objectFactory = objectFactory; + } + + @Override + public void openFile( + @Nullable String initialDirectory, + @NonNull GeneratedFileSelectorApi.FileTypes allowedTypes, + @NonNull GeneratedFileSelectorApi.Result result) { + final Intent intent = objectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + + setMimeTypes(intent, allowedTypes); + trySetInitialDirectory(intent, initialDirectory); + + try { + startActivityForResult( + intent, + OPEN_FILE, + new OnResultListener() { + @Override + public void onResult(int resultCode, @Nullable Intent data) { + if (resultCode == Activity.RESULT_OK && data != null) { + final Uri uri = data.getData(); + final GeneratedFileSelectorApi.FileResponse file = toFileResponse(uri); + if (file != null) { + result.success(file); + } else { + result.error(new Exception("Failed to read file: " + uri)); + } + } else { + result.success(null); + } + } + }); + } catch (Exception exception) { + result.error(exception); + } + } + + @Override + public void openFiles( + @Nullable String initialDirectory, + @NonNull GeneratedFileSelectorApi.FileTypes allowedTypes, + @NonNull + GeneratedFileSelectorApi.Result> result) { + final Intent intent = objectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + + setMimeTypes(intent, allowedTypes); + trySetInitialDirectory(intent, initialDirectory); + + try { + startActivityForResult( + intent, + OPEN_FILES, + new OnResultListener() { + @Override + public void onResult(int resultCode, @Nullable Intent data) { + if (resultCode == Activity.RESULT_OK && data != null) { + // Only one file was returned. + final Uri uri = data.getData(); + if (uri != null) { + final GeneratedFileSelectorApi.FileResponse file = toFileResponse(uri); + if (file != null) { + result.success(Collections.singletonList(file)); + } else { + result.error(new Exception("Failed to read file: " + uri)); + } + } + + // Multiple files were returned. + final ClipData clipData = data.getClipData(); + if (clipData != null) { + final List files = + new ArrayList<>(clipData.getItemCount()); + for (int i = 0; i < clipData.getItemCount(); i++) { + final ClipData.Item clipItem = clipData.getItemAt(i); + final GeneratedFileSelectorApi.FileResponse file = + toFileResponse(clipItem.getUri()); + if (file != null) { + files.add(file); + } else { + result.error(new Exception("Failed to read file: " + uri)); + return; + } + } + result.success(files); + } + } else { + result.success(new ArrayList<>()); + } + } + }); + } catch (Exception exception) { + result.error(exception); + } + } + + @Override + public void getDirectoryPath( + @Nullable String initialDirectory, @NonNull GeneratedFileSelectorApi.Result result) { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) { + throw new UnsupportedOperationException( + "Selecting a directory is only supported on versions >= 21"); + } + + final Intent intent = objectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT_TREE); + trySetInitialDirectory(intent, initialDirectory); + + try { + startActivityForResult( + intent, + OPEN_DIR, + new OnResultListener() { + @Override + public void onResult(int resultCode, @Nullable Intent data) { + if (resultCode == Activity.RESULT_OK && data != null) { + final Uri uri = data.getData(); + result.success(uri.toString()); + } else { + result.success(null); + } + } + }); + } catch (Exception exception) { + result.error(exception); + } + } + + public void setActivityPluginBinding(@Nullable ActivityPluginBinding activityPluginBinding) { + this.activityPluginBinding = activityPluginBinding; + } + + // Setting the mimeType with `setType` is required when opening files. This handles setting the + // mimeType based on the `mimeTypes` list and converts extensions to mimeTypes. + // See https://developer.android.com/guide/components/intents-common#OpenFile + private void setMimeTypes( + @NonNull Intent intent, @NonNull GeneratedFileSelectorApi.FileTypes allowedTypes) { + final Set allMimetypes = new HashSet<>(); + allMimetypes.addAll(allowedTypes.getMimeTypes()); + allMimetypes.addAll(tryConvertExtensionsToMimetypes(allowedTypes.getExtensions())); + + if (allMimetypes.isEmpty()) { + intent.setType("*/*"); + } else if (allMimetypes.size() == 1) { + intent.setType(allMimetypes.iterator().next()); + } else { + intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_MIME_TYPES, allMimetypes.toArray(new String[0])); + } + } + + // Attempts to convert each extension to Android compatible mimeType. Logs a warning if an + // extension could not be converted. + @NonNull + private List tryConvertExtensionsToMimetypes(@NonNull List extensions) { + if (extensions.isEmpty()) { + return Collections.emptyList(); + } + + final MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); + final Set mimeTypes = new HashSet<>(); + for (String extension : extensions) { + final String mimetype = mimeTypeMap.getMimeTypeFromExtension(extension); + if (mimetype != null) { + mimeTypes.add(mimetype); + } else { + Log.w(TAG, "Extension not supported: " + extension); + } + } + + return new ArrayList<>(mimeTypes); + } + + private void trySetInitialDirectory(@NonNull Intent intent, @Nullable String initialDirectory) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && initialDirectory != null) { + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.parse(initialDirectory)); + } + } + + private void startActivityForResult( + @NonNull Intent intent, int attemptRequestCode, @NonNull OnResultListener resultListener) + throws Exception { + if (activityPluginBinding == null) { + throw new Exception("No activity is available."); + } + activityPluginBinding.addActivityResultListener( + new PluginRegistry.ActivityResultListener() { + @Override + public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + if (requestCode == attemptRequestCode) { + resultListener.onResult(resultCode, data); + activityPluginBinding.removeActivityResultListener(this); + return true; + } + + return false; + } + }); + activityPluginBinding.getActivity().startActivityForResult(intent, attemptRequestCode); + } + + @Nullable + GeneratedFileSelectorApi.FileResponse toFileResponse(@NonNull Uri uri) { + if (activityPluginBinding == null) { + Log.d(TAG, "Activity is not available."); + return null; + } + + final ContentResolver contentResolver = + activityPluginBinding.getActivity().getContentResolver(); + + String name = null; + Integer size = null; + try (Cursor cursor = contentResolver.query(uri, null, null, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + // Note it's called "Display Name". This is + // provider-specific, and might not necessarily be the file name. + final int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + if (nameIndex >= 0) { + name = cursor.getString(nameIndex); + } + + final int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE); + // If the size is unknown, the value stored is null. This will + // happen often: The storage API allows for remote files, whose + // size might not be locally known. + if (!cursor.isNull(sizeIndex)) { + size = cursor.getInt(sizeIndex); + } + } + } + + if (size == null) { + return null; + } + + final byte[] bytes = new byte[size]; + try (InputStream inputStream = contentResolver.openInputStream(uri)) { + final DataInputStream dataInputStream = objectFactory.newDataInputStream(inputStream); + dataInputStream.readFully(bytes); + } catch (IOException exception) { + Log.w(TAG, exception.getMessage()); + return null; + } + + return new GeneratedFileSelectorApi.FileResponse.Builder() + .setName(name) + .setBytes(bytes) + .setPath(uri.toString()) + .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/GeneratedFileSelectorApi.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/GeneratedFileSelectorApi.java new file mode 100644 index 0000000000..5f4261563b --- /dev/null +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/GeneratedFileSelectorApi.java @@ -0,0 +1,438 @@ +// 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. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +package dev.flutter.packages.file_selector_android; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StandardMessageCodec; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** Generated class from Pigeon. */ +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) +public class GeneratedFileSelectorApi { + + /** Error class for passing custom error details to Flutter via a thrown PlatformException. */ + public static class FlutterError extends RuntimeException { + + /** The error code. */ + public final String code; + + /** The error details. Must be a datatype supported by the api codec. */ + public final Object details; + + public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) { + super(message); + this.code = code; + this.details = details; + } + } + + @NonNull + protected static ArrayList wrapError(@NonNull Throwable exception) { + ArrayList errorList = new ArrayList(3); + if (exception instanceof FlutterError) { + FlutterError error = (FlutterError) exception; + errorList.add(error.code); + errorList.add(error.getMessage()); + errorList.add(error.details); + } else { + errorList.add(exception.toString()); + errorList.add(exception.getClass().getSimpleName()); + errorList.add( + "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + } + return errorList; + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class FileResponse { + private @NonNull String path; + + public @NonNull String getPath() { + return path; + } + + public void setPath(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"path\" is null."); + } + this.path = setterArg; + } + + private @Nullable String mimeType; + + public @Nullable String getMimeType() { + return mimeType; + } + + public void setMimeType(@Nullable String setterArg) { + this.mimeType = setterArg; + } + + private @Nullable String name; + + public @Nullable String getName() { + return name; + } + + public void setName(@Nullable String setterArg) { + this.name = setterArg; + } + + private @NonNull Long size; + + public @NonNull Long getSize() { + return size; + } + + public void setSize(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"size\" is null."); + } + this.size = setterArg; + } + + private @NonNull byte[] bytes; + + public @NonNull byte[] getBytes() { + return bytes; + } + + public void setBytes(@NonNull byte[] setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"bytes\" is null."); + } + this.bytes = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + FileResponse() {} + + public static final class Builder { + + private @Nullable String path; + + public @NonNull Builder setPath(@NonNull String setterArg) { + this.path = setterArg; + return this; + } + + private @Nullable String mimeType; + + public @NonNull Builder setMimeType(@Nullable String setterArg) { + this.mimeType = setterArg; + return this; + } + + private @Nullable String name; + + public @NonNull Builder setName(@Nullable String setterArg) { + this.name = setterArg; + return this; + } + + private @Nullable Long size; + + public @NonNull Builder setSize(@NonNull Long setterArg) { + this.size = setterArg; + return this; + } + + private @Nullable byte[] bytes; + + public @NonNull Builder setBytes(@NonNull byte[] setterArg) { + this.bytes = setterArg; + return this; + } + + public @NonNull FileResponse build() { + FileResponse pigeonReturn = new FileResponse(); + pigeonReturn.setPath(path); + pigeonReturn.setMimeType(mimeType); + pigeonReturn.setName(name); + pigeonReturn.setSize(size); + pigeonReturn.setBytes(bytes); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(5); + toListResult.add(path); + toListResult.add(mimeType); + toListResult.add(name); + toListResult.add(size); + toListResult.add(bytes); + return toListResult; + } + + static @NonNull FileResponse fromList(@NonNull ArrayList list) { + FileResponse pigeonResult = new FileResponse(); + Object path = list.get(0); + pigeonResult.setPath((String) path); + Object mimeType = list.get(1); + pigeonResult.setMimeType((String) mimeType); + Object name = list.get(2); + pigeonResult.setName((String) name); + Object size = list.get(3); + pigeonResult.setSize( + (size == null) ? null : ((size instanceof Integer) ? (Integer) size : (Long) size)); + Object bytes = list.get(4); + pigeonResult.setBytes((byte[]) bytes); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class FileTypes { + private @NonNull List mimeTypes; + + public @NonNull List getMimeTypes() { + return mimeTypes; + } + + public void setMimeTypes(@NonNull List setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"mimeTypes\" is null."); + } + this.mimeTypes = setterArg; + } + + private @NonNull List extensions; + + public @NonNull List getExtensions() { + return extensions; + } + + public void setExtensions(@NonNull List setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"extensions\" is null."); + } + this.extensions = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + FileTypes() {} + + public static final class Builder { + + private @Nullable List mimeTypes; + + public @NonNull Builder setMimeTypes(@NonNull List setterArg) { + this.mimeTypes = setterArg; + return this; + } + + private @Nullable List extensions; + + public @NonNull Builder setExtensions(@NonNull List setterArg) { + this.extensions = setterArg; + return this; + } + + public @NonNull FileTypes build() { + FileTypes pigeonReturn = new FileTypes(); + pigeonReturn.setMimeTypes(mimeTypes); + pigeonReturn.setExtensions(extensions); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(mimeTypes); + toListResult.add(extensions); + return toListResult; + } + + static @NonNull FileTypes fromList(@NonNull ArrayList list) { + FileTypes pigeonResult = new FileTypes(); + Object mimeTypes = list.get(0); + pigeonResult.setMimeTypes((List) mimeTypes); + Object extensions = list.get(1); + pigeonResult.setExtensions((List) extensions); + return pigeonResult; + } + } + + public interface Result { + @SuppressWarnings("UnknownNullness") + void success(T result); + + void error(@NonNull Throwable error); + } + + private static class FileSelectorApiCodec extends StandardMessageCodec { + public static final FileSelectorApiCodec INSTANCE = new FileSelectorApiCodec(); + + private FileSelectorApiCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return FileResponse.fromList((ArrayList) readValue(buffer)); + case (byte) 129: + return FileTypes.fromList((ArrayList) readValue(buffer)); + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof FileResponse) { + stream.write(128); + writeValue(stream, ((FileResponse) value).toList()); + } else if (value instanceof FileTypes) { + stream.write(129); + writeValue(stream, ((FileTypes) value).toList()); + } else { + super.writeValue(stream, value); + } + } + } + + /** + * An API to call to native code to select files or directories. + * + *

Generated interface from Pigeon that represents a handler of messages from Flutter. + */ + public interface FileSelectorApi { + /** + * Opens a file dialog for loading files and returns a file path. + * + *

Returns `null` if user cancels the operation. + */ + void openFile( + @Nullable String initialDirectory, + @NonNull FileTypes allowedTypes, + @NonNull Result result); + /** + * Opens a file dialog for loading files and returns a list of file responses chosen by the + * user. + */ + void openFiles( + @Nullable String initialDirectory, + @NonNull FileTypes allowedTypes, + @NonNull Result> result); + /** + * Opens a file dialog for loading directories and returns a directory path. + * + *

Returns `null` if user cancels the operation. + */ + void getDirectoryPath(@Nullable String initialDirectory, @NonNull Result result); + + /** The codec used by FileSelectorApi. */ + static @NonNull MessageCodec getCodec() { + return FileSelectorApiCodec.INSTANCE; + } + /** + * Sets up an instance of `FileSelectorApi` to handle messages through the `binaryMessenger`. + */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable FileSelectorApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.FileSelectorApi.openFile", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String initialDirectoryArg = (String) args.get(0); + FileTypes allowedTypesArg = (FileTypes) args.get(1); + Result resultCallback = + new Result() { + public void success(FileResponse result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.openFile(initialDirectoryArg, allowedTypesArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.FileSelectorApi.openFiles", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String initialDirectoryArg = (String) args.get(0); + FileTypes allowedTypesArg = (FileTypes) args.get(1); + Result> resultCallback = + new Result>() { + public void success(List result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.openFiles(initialDirectoryArg, allowedTypesArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.FileSelectorApi.getDirectoryPath", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String initialDirectoryArg = (String) args.get(0); + Result resultCallback = + new Result() { + public void success(String result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.getDirectoryPath(initialDirectoryArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } +} 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 new file mode 100644 index 0000000000..5253fc578f --- /dev/null +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java @@ -0,0 +1,235 @@ +// 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.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.List; +import org.junit.Assert; +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("JavaReflectionMemberAccess") + private static void setFinalStatic( + Class classToModify, String fieldName, Object newValue) { + try { + Field field = classToModify.getField(fieldName); + field.setAccessible(true); + + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + + field.set(null, newValue); + } catch (Exception e) { + Assert.fail("Unable to mock static field: " + fieldName); + } + } + + @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); + + 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); + + 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() { + setFinalStatic(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.LOLLIPOP); + + final Uri mockUri = mock(Uri.class); + when(mockUri.toString()).thenReturn("some/path/"); + + when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT_TREE)).thenReturn(mockIntent); + when(mockActivityBinding.getActivity()).thenReturn(mockActivity); + final FileSelectorApiImpl fileSelectorApi = + new FileSelectorApiImpl(mockActivityBinding, mockObjectFactory); + + 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/"); + } +} diff --git a/packages/file_selector/file_selector_android/example/README.md b/packages/file_selector/file_selector_android/example/README.md new file mode 100644 index 0000000000..96b8bb17db --- /dev/null +++ b/packages/file_selector/file_selector_android/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/file_selector/file_selector_android/example/android/app/build.gradle b/packages/file_selector/file_selector_android/example/android/app/build.gradle new file mode 100644 index 0000000000..8e9407350c --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/build.gradle @@ -0,0 +1,72 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + namespace "dev.flutter.packages.file_selector_android_example" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "dev.flutter.packages.file_selector_android_example" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion 21 + targetSdkVersion 33 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0' + androidTestImplementation 'androidx.test:runner:1.4.0' + androidTestImplementation 'androidx.test:rules:1.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation project(':file_selector_android') + implementation project(':espresso') + api 'androidx.test:core:1.4.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 new file mode 100644 index 0000000000..dd3eff8fd2 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/FileSelectorAndroidTest.java @@ -0,0 +1,71 @@ +// 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.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(); + + @Test + public void openImageFile() { + 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"))); + } + + @Test + public void openImageFiles() { + 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/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/MainActivityTest.java b/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/MainActivityTest.java new file mode 100644 index 0000000000..f26b7aee6a --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/androidTest/java/dev/flutter/packages/file_selector_android_example/MainActivityTest.java @@ -0,0 +1,17 @@ +// 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 androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class MainActivityTest { + @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); +} diff --git a/packages/file_selector/file_selector_android/example/android/app/src/debug/AndroidManifest.xml b/packages/file_selector/file_selector_android/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000000..efc03d7e23 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + 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 new file mode 100644 index 0000000000..ce8443c7bd --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/java/dev/flutter/packages/file_selector_android_example/DriverExtensionActivity.java b/packages/file_selector/file_selector_android/example/android/app/src/main/java/dev/flutter/packages/file_selector_android_example/DriverExtensionActivity.java new file mode 100644 index 0000000000..436d5ce479 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/main/java/dev/flutter/packages/file_selector_android_example/DriverExtensionActivity.java @@ -0,0 +1,10 @@ +// 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 io.flutter.embedding.android.FlutterActivity; + +/** Test Activity that sets the name of the Dart method entrypoint in the manifest. */ +public class DriverExtensionActivity extends FlutterActivity {} diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/java/dev/flutter/packages/file_selector_android_example/MainActivity.java b/packages/file_selector/file_selector_android/example/android/app/src/main/java/dev/flutter/packages/file_selector_android_example/MainActivity.java new file mode 100644 index 0000000000..0f2f0c7ab0 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/main/java/dev/flutter/packages/file_selector_android_example/MainActivity.java @@ -0,0 +1,9 @@ +// 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 io.flutter.embedding.android.FlutterActivity; + +public class MainActivity extends FlutterActivity {} diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/java/dev/flutter/packages/file_selector_android_example/TestContentProvider.java b/packages/file_selector/file_selector_android/example/android/app/src/main/java/dev/flutter/packages/file_selector_android_example/TestContentProvider.java new file mode 100644 index 0000000000..9f8a2d55c4 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/main/java/dev/flutter/packages/file_selector_android_example/TestContentProvider.java @@ -0,0 +1,71 @@ +// 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 android.content.ContentProvider; +import android.content.ContentValues; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.provider.OpenableColumns; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class TestContentProvider extends ContentProvider { + @Override + public boolean onCreate() { + return true; + } + + @Nullable + @Override + public Cursor query( + @NonNull Uri uri, + @Nullable String[] strings, + @Nullable String s, + @Nullable String[] strings1, + @Nullable String s1) { + MatrixCursor cursor = + new MatrixCursor(new String[] {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}); + cursor.addRow( + new Object[] { + "dummy.png", getContext().getResources().openRawResourceFd(R.raw.ic_launcher).getLength() + }); + return cursor; + } + + @Nullable + @Override + public String getType(@NonNull Uri uri) { + return "image/png"; + } + + @Nullable + @Override + public AssetFileDescriptor openAssetFile(@NonNull Uri uri, @NonNull String mode) { + return getContext().getResources().openRawResourceFd(R.raw.ic_launcher); + } + + @Nullable + @Override + public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) { + return null; + } + + @Override + public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) { + return 0; + } + + @Override + public int update( + @NonNull Uri uri, + @Nullable ContentValues contentValues, + @Nullable String s, + @Nullable String[] strings) { + return 0; + } +} diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/file_selector/file_selector_android/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 0000000000..0f0c1464e0 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,21 @@ +// 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; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/* + * Annotation to aid repository tooling in determining if a test is + * a native java unit test or a java class with a dart integration. + * + * See: https://github.com/flutter/flutter/wiki/Plugin-Tests#enabling-android-ui-tests + * for more infomation. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/file_selector/file_selector_android/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000000..f74085f3f6 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/drawable/launch_background.xml b/packages/file_selector/file_selector_android/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000000..304732f884 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..db77bb4b7b Binary files /dev/null and b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..17987b79bb Binary files /dev/null and b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..09d4391482 Binary files /dev/null and b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..d5f1c8d34e Binary files /dev/null and b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..4d6372eebd Binary files /dev/null and b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/raw/ic_launcher.png b/packages/file_selector/file_selector_android/example/android/app/src/main/res/raw/ic_launcher.png new file mode 100644 index 0000000000..db77bb4b7b Binary files /dev/null and b/packages/file_selector/file_selector_android/example/android/app/src/main/res/raw/ic_launcher.png differ diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/values-night/styles.xml b/packages/file_selector/file_selector_android/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000000..06952be745 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/values/styles.xml b/packages/file_selector/file_selector_android/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000..cb1ef88056 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/file_selector/file_selector_android/example/android/app/src/profile/AndroidManifest.xml b/packages/file_selector/file_selector_android/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000000..399f6981d5 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/file_selector/file_selector_android/example/android/build.gradle b/packages/file_selector/file_selector_android/example/android/build.gradle new file mode 100644 index 0000000000..d4d51942c4 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/build.gradle @@ -0,0 +1,43 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} + +// Build the plugin project with warnings enabled. This is here rather than +// in the plugin itself to avoid breaking clients that have different +// warnings (e.g., deprecation warnings from a newer SDK than this project +// builds with). +gradle.projectsEvaluated { + project(":file_selector_android") { + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:all" << "-Werror" + } + } +} diff --git a/packages/file_selector/file_selector_android/example/android/gradle.properties b/packages/file_selector/file_selector_android/example/android/gradle.properties new file mode 100644 index 0000000000..598d13fee4 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/file_selector/file_selector_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/file_selector/file_selector_android/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..3c472b99c6 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/packages/file_selector/file_selector_android/example/android/settings.gradle b/packages/file_selector/file_selector_android/example/android/settings.gradle new file mode 100644 index 0000000000..44e62bcf06 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/packages/file_selector/file_selector_android/example/integration_test/file_selector_android_test.dart b/packages/file_selector/file_selector_android/example/integration_test/file_selector_android_test.dart new file mode 100644 index 0000000000..1f95676216 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/integration_test/file_selector_android_test.dart @@ -0,0 +1,22 @@ +// 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. + +import 'package:file_selector_android_example/main.dart' as app; +import 'package:flutter_driver/driver_extension.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +/// Entry point for integration tests that require espresso. +void integrationTestMain() { + enableFlutterDriverExtension(); + app.main(); +} + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Since this test is lacking integration tests, this test ensures the example + // app can be launched on an emulator/device. + testWidgets('Launch Test', (WidgetTester tester) async {}); +} diff --git a/packages/file_selector/file_selector_android/example/lib/home_page.dart b/packages/file_selector/file_selector_android/example/lib/home_page.dart new file mode 100644 index 0000000000..38f534f89d --- /dev/null +++ b/packages/file_selector/file_selector_android/example/lib/home_page.dart @@ -0,0 +1,49 @@ +// 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. + +import 'package:flutter/material.dart'; + +/// Home Page of the application. +class HomePage extends StatelessWidget { + /// Default Constructor + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + final ButtonStyle style = ElevatedButton.styleFrom( + foregroundColor: Colors.blue, + backgroundColor: Colors.white, + ); + return Scaffold( + appBar: AppBar( + title: const Text('File Selector Demo Home Page'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: style, + child: const Text('Open a text file'), + onPressed: () => Navigator.pushNamed(context, '/open/text'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open an image'), + onPressed: () => Navigator.pushNamed(context, '/open/image'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open multiple images'), + onPressed: () => Navigator.pushNamed(context, '/open/images'), + ), + const SizedBox(height: 10), + ], + ), + ), + ); + } +} diff --git a/packages/file_selector/file_selector_android/example/lib/main.dart b/packages/file_selector/file_selector_android/example/lib/main.dart new file mode 100644 index 0000000000..a63fc41c23 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/lib/main.dart @@ -0,0 +1,48 @@ +// 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. + +import 'package:file_selector_android/file_selector_android.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_driver/driver_extension.dart'; + +import 'home_page.dart'; +import 'open_image_page.dart'; +import 'open_multiple_images_page.dart'; +import 'open_text_page.dart'; + +/// Entry point for integration tests that require espresso. +void integrationTestMain() { + enableFlutterDriverExtension(); + main(); +} + +void main() { + FileSelectorPlatform.instance = FileSelectorAndroid(); + runApp(const MyApp()); +} + +/// MyApp is the Main Application. +class MyApp extends StatelessWidget { + /// Default Constructor + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'File Selector Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: const HomePage(), + routes: { + '/open/image': (BuildContext context) => const OpenImagePage(), + '/open/images': (BuildContext context) => + const OpenMultipleImagesPage(), + '/open/text': (BuildContext context) => const OpenTextPage(), + }, + ); + } +} diff --git a/packages/file_selector/file_selector_android/example/lib/open_image_page.dart b/packages/file_selector/file_selector_android/example/lib/open_image_page.dart new file mode 100644 index 0000000000..08a1ec968b --- /dev/null +++ b/packages/file_selector/file_selector_android/example/lib/open_image_page.dart @@ -0,0 +1,89 @@ +// 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. + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Screen that allows the user to select an image file using +/// `openFiles`, then displays the selected images in a gallery dialog. +class OpenImagePage extends StatelessWidget { + /// Default Constructor + const OpenImagePage({super.key}); + + Future _openImageFile(BuildContext context) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'images', + extensions: ['jpg', 'png'], + uniformTypeIdentifiers: ['public.image'], + ); + final XFile? file = await FileSelectorPlatform.instance + .openFile(acceptedTypeGroups: [typeGroup]); + if (file == null) { + // Operation was canceled by the user. + return; + } + + final Uint8List bytes = await file.readAsBytes(); + + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => ImageDisplay(file.path, bytes), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open an image'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + foregroundColor: Colors.blue, + backgroundColor: Colors.white, + ), + child: const Text('Press to open an image file(png, jpg)'), + onPressed: () => _openImageFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays an image in a dialog. +class ImageDisplay extends StatelessWidget { + /// Default Constructor. + const ImageDisplay(this.filePath, this.bytes, {super.key}); + + /// The path to the selected file. + final String filePath; + + /// The bytes of the selected file. + final Uint8List bytes; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(key: const Key('result_image_name'), filePath), + content: Image.memory(bytes), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_android/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector_android/example/lib/open_multiple_images_page.dart new file mode 100644 index 0000000000..305ce38ec9 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/lib/open_multiple_images_page.dart @@ -0,0 +1,108 @@ +// 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. + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Screen that allows the user to select multiple image files using +/// `openFiles`, then displays the selected images in a gallery dialog. +class OpenMultipleImagesPage extends StatelessWidget { + /// Default Constructor + const OpenMultipleImagesPage({super.key}); + + Future _openImageFile(BuildContext context) async { + const XTypeGroup jpgsTypeGroup = XTypeGroup( + label: 'JPEGs', + extensions: ['jpg', 'jpeg'], + uniformTypeIdentifiers: ['public.jpeg'], + ); + const XTypeGroup pngTypeGroup = XTypeGroup( + label: 'PNGs', + extensions: ['png'], + uniformTypeIdentifiers: ['public.png'], + ); + final List files = await FileSelectorPlatform.instance + .openFiles(acceptedTypeGroups: [ + jpgsTypeGroup, + pngTypeGroup, + ]); + if (files.isEmpty) { + // Operation was canceled by the user. + return; + } + + final List imageBytes = []; + for (final XFile file in files) { + imageBytes.add(await file.readAsBytes()); + } + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => MultipleImagesDisplay(imageBytes), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open multiple images'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + foregroundColor: Colors.blue, + backgroundColor: Colors.white, + ), + child: const Text('Press to open multiple images (png, jpg)'), + onPressed: () => _openImageFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog. +class MultipleImagesDisplay extends StatelessWidget { + /// Default Constructor. + const MultipleImagesDisplay(this.fileBytes, {super.key}); + + /// The bytes containing the images. + final List fileBytes; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Gallery'), + // On web the filePath is a blob url + // while on other platforms it is a system path. + content: Center( + child: Row( + children: [ + for (int i = 0; i < fileBytes.length; i++) + Flexible( + key: Key('result_image_name$i'), + child: Image.memory(fileBytes[i]), + ) + ], + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_android/example/lib/open_text_page.dart b/packages/file_selector/file_selector_android/example/lib/open_text_page.dart new file mode 100644 index 0000000000..8d21e4337c --- /dev/null +++ b/packages/file_selector/file_selector_android/example/lib/open_text_page.dart @@ -0,0 +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. + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/material.dart'; + +/// Screen that allows the user to select a text file using `openFile`, then +/// displays its contents in a dialog. +class OpenTextPage extends StatelessWidget { + /// Default Constructor + const OpenTextPage({super.key}); + + Future _openTextFile(BuildContext context) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'text', + extensions: ['txt', 'json'], + uniformTypeIdentifiers: ['public.text'], + ); + final XFile? file = await FileSelectorPlatform.instance + .openFile(acceptedTypeGroups: [typeGroup]); + if (file == null) { + // Operation was canceled by the user. + return; + } + final String fileName = file.name; + final String fileContent = await file.readAsString(); + + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(fileName, fileContent), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open a text file'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + foregroundColor: Colors.blue, + backgroundColor: Colors.white, + ), + child: const Text('Press to open a text file (json, txt)'), + onPressed: () => _openTextFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog. +class TextDisplay extends StatelessWidget { + /// Default Constructor. + const TextDisplay(this.fileName, this.fileContent, {super.key}); + + /// The name of the selected file. + final String fileName; + + /// The contents of the text file. + final String fileContent; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(fileName), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(fileContent), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_android/example/pubspec.yaml b/packages/file_selector/file_selector_android/example/pubspec.yaml new file mode 100644 index 0000000000..97a3e1edd3 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/pubspec.yaml @@ -0,0 +1,31 @@ +name: file_selector_android_example +description: Demonstrates how to use the file_selector_android plugin. +publish_to: 'none' + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + file_selector_android: + # When depending on this package from a real application you should use: + # file_selector_android: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + file_selector_platform_interface: ^2.5.0 + flutter: + sdk: flutter + flutter_driver: + sdk: flutter + +dev_dependencies: + espresso: ^0.2.0 + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/file_selector/file_selector_android/example/test_driver/integration_test.dart b/packages/file_selector/file_selector_android/example/test_driver/integration_test.dart new file mode 100644 index 0000000000..4f10f2a522 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// 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. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/file_selector/file_selector_android/lib/file_selector_android.dart b/packages/file_selector/file_selector_android/lib/file_selector_android.dart new file mode 100644 index 0000000000..c86f2da2fa --- /dev/null +++ b/packages/file_selector/file_selector_android/lib/file_selector_android.dart @@ -0,0 +1,5 @@ +// 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. + +export 'src/file_selector_android.dart'; diff --git a/packages/file_selector/file_selector_android/lib/src/file_selector_android.dart b/packages/file_selector/file_selector_android/lib/src/file_selector_android.dart new file mode 100644 index 0000000000..a4ff203236 --- /dev/null +++ b/packages/file_selector/file_selector_android/lib/src/file_selector_android.dart @@ -0,0 +1,101 @@ +// 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. + +// ignore_for_file: public_member_api_docs + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/cupertino.dart'; + +import 'file_selector_api.g.dart'; + +/// An implementation of [FileSelectorPlatform] for Android. +class FileSelectorAndroid extends FileSelectorPlatform { + FileSelectorAndroid({@visibleForTesting FileSelectorApi? api}) + : _api = api ?? FileSelectorApi(); + + final FileSelectorApi _api; + + /// Registers this class as the implementation of the file_selector platform interface. + static void registerWith() { + FileSelectorPlatform.instance = FileSelectorAndroid(); + } + + @override + Future openFile({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + final FileResponse? file = await _api.openFile( + initialDirectory, + _fileTypesFromTypeGroups(acceptedTypeGroups), + ); + return file == null ? null : _xFileFromFileResponse(file); + } + + @override + Future> openFiles({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + final List files = await _api.openFiles( + initialDirectory, + _fileTypesFromTypeGroups(acceptedTypeGroups), + ); + return files + .cast() + .map(_xFileFromFileResponse) + .toList(); + } + + @override + Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, + }) async { + return _api.getDirectoryPath(initialDirectory); + } + + XFile _xFileFromFileResponse(FileResponse file) { + return XFile.fromData( + file.bytes, + // Note: The name parameter is not used by XFile. The XFile.name returns + // the extracted file name from XFile.path. + name: file.name, + length: file.size, + mimeType: file.mimeType, + path: file.path, + ); + } + + FileTypes _fileTypesFromTypeGroups(List? typeGroups) { + if (typeGroups == null) { + return FileTypes(extensions: [], mimeTypes: []); + } + + final Set mimeTypes = {}; + final Set extensions = {}; + + for (final XTypeGroup group in typeGroups) { + if (!group.allowsAny && + group.mimeTypes == null && + group.extensions == null) { + throw ArgumentError( + 'Provided type group $group does not allow all files, but does not ' + 'set any of the Android supported filter categories. At least one of ' + '"extensions" or "mimeTypes" must be non-empty for Android.', + ); + } + + mimeTypes.addAll(group.mimeTypes ?? {}); + extensions.addAll(group.extensions ?? {}); + } + + return FileTypes( + mimeTypes: mimeTypes.toList(), + extensions: extensions.toList(), + ); + } +} diff --git a/packages/file_selector/file_selector_android/lib/src/file_selector_api.g.dart b/packages/file_selector/file_selector_android/lib/src/file_selector_api.g.dart new file mode 100644 index 0000000000..2f3611ecdd --- /dev/null +++ b/packages/file_selector/file_selector_android/lib/src/file_selector_api.g.dart @@ -0,0 +1,202 @@ +// 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. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +class FileResponse { + FileResponse({ + required this.path, + this.mimeType, + this.name, + required this.size, + required this.bytes, + }); + + String path; + + String? mimeType; + + String? name; + + int size; + + Uint8List bytes; + + Object encode() { + return [ + path, + mimeType, + name, + size, + bytes, + ]; + } + + static FileResponse decode(Object result) { + result as List; + return FileResponse( + path: result[0]! as String, + mimeType: result[1] as String?, + name: result[2] as String?, + size: result[3]! as int, + bytes: result[4]! as Uint8List, + ); + } +} + +class FileTypes { + FileTypes({ + required this.mimeTypes, + required this.extensions, + }); + + List mimeTypes; + + List extensions; + + Object encode() { + return [ + mimeTypes, + extensions, + ]; + } + + static FileTypes decode(Object result) { + result as List; + return FileTypes( + mimeTypes: (result[0] as List?)!.cast(), + extensions: (result[1] as List?)!.cast(), + ); + } +} + +class _FileSelectorApiCodec extends StandardMessageCodec { + const _FileSelectorApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is FileResponse) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is FileTypes) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return FileResponse.decode(readValue(buffer)!); + case 129: + return FileTypes.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +/// An API to call to native code to select files or directories. +class FileSelectorApi { + /// Constructor for [FileSelectorApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + FileSelectorApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _FileSelectorApiCodec(); + + /// Opens a file dialog for loading files and returns a file path. + /// + /// Returns `null` if user cancels the operation. + Future openFile( + String? arg_initialDirectory, FileTypes arg_allowedTypes) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileSelectorApi.openFile', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_initialDirectory, arg_allowedTypes]) + as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return (replyList[0] as FileResponse?); + } + } + + /// Opens a file dialog for loading files and returns a list of file responses + /// chosen by the user. + Future> openFiles( + String? arg_initialDirectory, FileTypes arg_allowedTypes) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileSelectorApi.openFiles', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_initialDirectory, arg_allowedTypes]) + as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as List?)!.cast(); + } + } + + /// Opens a file dialog for loading directories and returns a directory path. + /// + /// Returns `null` if user cancels the operation. + Future getDirectoryPath(String? arg_initialDirectory) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileSelectorApi.getDirectoryPath', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_initialDirectory]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return (replyList[0] as String?); + } + } +} diff --git a/packages/file_selector/file_selector_android/pigeons/copyright.txt b/packages/file_selector/file_selector_android/pigeons/copyright.txt new file mode 100644 index 0000000000..1236b63caf --- /dev/null +++ b/packages/file_selector/file_selector_android/pigeons/copyright.txt @@ -0,0 +1,3 @@ +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. diff --git a/packages/file_selector/file_selector_android/pigeons/file_selector_api.dart b/packages/file_selector/file_selector_android/pigeons/file_selector_api.dart new file mode 100644 index 0000000000..440805019a --- /dev/null +++ b/packages/file_selector/file_selector_android/pigeons/file_selector_api.dart @@ -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. + +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/src/file_selector_api.g.dart', + javaOut: + 'android/src/main/java/dev/flutter/packages/file_selector_android/GeneratedFileSelectorApi.java', + javaOptions: JavaOptions( + package: 'dev.flutter.packages.file_selector_android', + className: 'GeneratedFileSelectorApi', + ), + copyrightHeader: 'pigeons/copyright.txt', + ), +) +class FileResponse { + late final String path; + late final String? mimeType; + late final String? name; + late final int size; + late final Uint8List bytes; +} + +class FileTypes { + late List mimeTypes; + late List extensions; +} + +/// An API to call to native code to select files or directories. +@HostApi() +abstract class FileSelectorApi { + /// Opens a file dialog for loading files and returns a file path. + /// + /// Returns `null` if user cancels the operation. + @async + FileResponse? openFile(String? initialDirectory, FileTypes allowedTypes); + + /// Opens a file dialog for loading files and returns a list of file responses + /// chosen by the user. + @async + List openFiles( + String? initialDirectory, + FileTypes allowedTypes, + ); + + /// Opens a file dialog for loading directories and returns a directory path. + /// + /// Returns `null` if user cancels the operation. + @async + String? getDirectoryPath(String? initialDirectory); +} diff --git a/packages/file_selector/file_selector_android/pubspec.yaml b/packages/file_selector/file_selector_android/pubspec.yaml new file mode 100644 index 0000000000..53cf3ea440 --- /dev/null +++ b/packages/file_selector/file_selector_android/pubspec.yaml @@ -0,0 +1,31 @@ +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 + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +flutter: + plugin: + implements: file_selector + platforms: + android: + dartPluginClass: FileSelectorAndroid + package: dev.flutter.packages.file_selector_android + pluginClass: FileSelectorAndroidPlugin + +dependencies: + file_selector_platform_interface: ^2.5.0 + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + build_runner: ^2.1.4 + flutter_test: + sdk: flutter + mockito: 5.4.1 + pigeon: ^9.2.4 diff --git a/packages/file_selector/file_selector_android/test/file_selector_android_test.dart b/packages/file_selector/file_selector_android/test/file_selector_android_test.dart new file mode 100644 index 0000000000..dc4ef92fba --- /dev/null +++ b/packages/file_selector/file_selector_android/test/file_selector_android_test.dart @@ -0,0 +1,158 @@ +// 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. + +import 'dart:typed_data'; + +import 'package:file_selector_android/src/file_selector_android.dart'; +import 'package:file_selector_android/src/file_selector_api.g.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'file_selector_android_test.mocks.dart'; + +@GenerateMocks([FileSelectorApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late FileSelectorAndroid plugin; + late MockFileSelectorApi mockApi; + + setUp(() { + mockApi = MockFileSelectorApi(); + plugin = FileSelectorAndroid(api: mockApi); + }); + + test('registered instance', () { + FileSelectorAndroid.registerWith(); + expect(FileSelectorPlatform.instance, isA()); + }); + + group('openFile', () { + test('passes the accepted type groups correctly', () async { + when( + mockApi.openFile( + 'some/path/', + argThat( + isA().having( + (FileTypes types) => types.mimeTypes, + 'mimeTypes', + ['text/plain', 'image/jpg'], + ).having( + (FileTypes types) => types.extensions, + 'extensions', + ['txt', 'jpg'], + ), + ), + ), + ).thenAnswer( + (_) => Future.value( + FileResponse( + path: 'some/path.txt', + size: 30, + bytes: Uint8List(0), + name: 'name', + mimeType: 'text/plain', + ), + ), + ); + + const XTypeGroup group = XTypeGroup( + extensions: ['txt'], + mimeTypes: ['text/plain'], + ); + + const XTypeGroup group2 = XTypeGroup( + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + ); + + final XFile? file = await plugin.openFile( + acceptedTypeGroups: [group, group2], + initialDirectory: 'some/path/', + ); + + expect(file?.path, 'some/path.txt'); + expect(file?.mimeType, 'text/plain'); + expect(await file?.length(), 30); + expect(await file?.readAsBytes(), Uint8List(0)); + }); + }); + + group('openFiles', () { + test('passes the accepted type groups correctly', () async { + when( + mockApi.openFiles( + 'some/path/', + argThat( + isA().having( + (FileTypes types) => types.mimeTypes, + 'mimeTypes', + ['text/plain', 'image/jpg'], + ).having( + (FileTypes types) => types.extensions, + 'extensions', + ['txt', 'jpg'], + ), + ), + ), + ).thenAnswer( + (_) => Future>.value( + [ + FileResponse( + path: 'some/path.txt', + size: 30, + bytes: Uint8List(0), + name: 'name', + mimeType: 'text/plain', + ), + FileResponse( + path: 'other/dir.jpg', + size: 40, + bytes: Uint8List(0), + mimeType: 'image/jpg', + ), + ], + ), + ); + + const XTypeGroup group = XTypeGroup( + extensions: ['txt'], + mimeTypes: ['text/plain'], + ); + + const XTypeGroup group2 = XTypeGroup( + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + ); + + final List files = await plugin.openFiles( + acceptedTypeGroups: [group, group2], + initialDirectory: 'some/path/', + ); + + expect(files[0].path, 'some/path.txt'); + expect(files[0].mimeType, 'text/plain'); + expect(await files[0].length(), 30); + expect(await files[0].readAsBytes(), Uint8List(0)); + + expect(files[1].path, 'other/dir.jpg'); + expect(files[1].mimeType, 'image/jpg'); + expect(await files[1].length(), 40); + expect(await files[1].readAsBytes(), Uint8List(0)); + }); + }); + + test('getDirectoryPath', () async { + when(mockApi.getDirectoryPath('some/path')) + .thenAnswer((_) => Future.value('some/path/chosen/')); + + final String? path = await plugin.getDirectoryPath( + initialDirectory: 'some/path', + ); + + expect(path, 'some/path/chosen/'); + }); +} diff --git a/packages/file_selector/file_selector_android/test/file_selector_android_test.mocks.dart b/packages/file_selector/file_selector_android/test/file_selector_android_test.mocks.dart new file mode 100644 index 0000000000..63a62f6b94 --- /dev/null +++ b/packages/file_selector/file_selector_android/test/file_selector_android_test.mocks.dart @@ -0,0 +1,70 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in file_selector_android/test/file_selector_android_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:file_selector_android/src/file_selector_api.g.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [FileSelectorApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFileSelectorApi extends _i1.Mock implements _i2.FileSelectorApi { + MockFileSelectorApi() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future<_i2.FileResponse?> openFile( + String? arg_initialDirectory, + _i2.FileTypes? arg_allowedTypes, + ) => + (super.noSuchMethod( + Invocation.method( + #openFile, + [ + arg_initialDirectory, + arg_allowedTypes, + ], + ), + returnValue: _i3.Future<_i2.FileResponse?>.value(), + ) as _i3.Future<_i2.FileResponse?>); + @override + _i3.Future> openFiles( + String? arg_initialDirectory, + _i2.FileTypes? arg_allowedTypes, + ) => + (super.noSuchMethod( + Invocation.method( + #openFiles, + [ + arg_initialDirectory, + arg_allowedTypes, + ], + ), + returnValue: + _i3.Future>.value(<_i2.FileResponse?>[]), + ) as _i3.Future>); + @override + _i3.Future getDirectoryPath(String? arg_initialDirectory) => + (super.noSuchMethod( + Invocation.method( + #getDirectoryPath, + [arg_initialDirectory], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); +}