[file_selector_android] Create initial Android implementation of the file_selector package (#3814)

Android implementation of the file_selector package

Related Links:
https://github.com/flutter/plugins/pull/6468
https://github.com/flutter/flutter/issues/25659
Part of https://github.com/flutter/flutter/issues/110098

Useful Resources:
https://developer.android.com/guide/topics/providers/document-provider
https://developer.android.com/training/data-storage/shared/documents-files
This commit is contained in:
Maurice Parrish
2023-06-26 14:29:07 -04:00
committed by GitHub
parent 90e3d327ff
commit b9935d1ee3
53 changed files with 2738 additions and 0 deletions

View File

@ -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:

View File

@ -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 <email address>
Google Inc.

View File

@ -0,0 +1,3 @@
## 0.5.0
* Implements file_selector_platform_interface for Android.

View File

@ -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.

View File

@ -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

View File

@ -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
}
}
}
}

View File

@ -0,0 +1 @@
rootProject.name = 'file_selector_android'

View File

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.flutter.packages.file_selector_android">
</manifest>

View File

@ -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);
}
}
}

View File

@ -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<GeneratedFileSelectorApi.FileResponse> 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<List<GeneratedFileSelectorApi.FileResponse>> 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<GeneratedFileSelectorApi.FileResponse> 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<String> 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<String> 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<String> tryConvertExtensionsToMimetypes(@NonNull List<String> extensions) {
if (extensions.isEmpty()) {
return Collections.emptyList();
}
final MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
final Set<String> 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();
}
}

View File

@ -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<Object> wrapError(@NonNull Throwable exception) {
ArrayList<Object> errorList = new ArrayList<Object>(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<Object> toList() {
ArrayList<Object> toListResult = new ArrayList<Object>(5);
toListResult.add(path);
toListResult.add(mimeType);
toListResult.add(name);
toListResult.add(size);
toListResult.add(bytes);
return toListResult;
}
static @NonNull FileResponse fromList(@NonNull ArrayList<Object> 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<String> mimeTypes;
public @NonNull List<String> getMimeTypes() {
return mimeTypes;
}
public void setMimeTypes(@NonNull List<String> setterArg) {
if (setterArg == null) {
throw new IllegalStateException("Nonnull field \"mimeTypes\" is null.");
}
this.mimeTypes = setterArg;
}
private @NonNull List<String> extensions;
public @NonNull List<String> getExtensions() {
return extensions;
}
public void setExtensions(@NonNull List<String> 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<String> mimeTypes;
public @NonNull Builder setMimeTypes(@NonNull List<String> setterArg) {
this.mimeTypes = setterArg;
return this;
}
private @Nullable List<String> extensions;
public @NonNull Builder setExtensions(@NonNull List<String> setterArg) {
this.extensions = setterArg;
return this;
}
public @NonNull FileTypes build() {
FileTypes pigeonReturn = new FileTypes();
pigeonReturn.setMimeTypes(mimeTypes);
pigeonReturn.setExtensions(extensions);
return pigeonReturn;
}
}
@NonNull
ArrayList<Object> toList() {
ArrayList<Object> toListResult = new ArrayList<Object>(2);
toListResult.add(mimeTypes);
toListResult.add(extensions);
return toListResult;
}
static @NonNull FileTypes fromList(@NonNull ArrayList<Object> list) {
FileTypes pigeonResult = new FileTypes();
Object mimeTypes = list.get(0);
pigeonResult.setMimeTypes((List<String>) mimeTypes);
Object extensions = list.get(1);
pigeonResult.setExtensions((List<String>) extensions);
return pigeonResult;
}
}
public interface Result<T> {
@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<Object>) readValue(buffer));
case (byte) 129:
return FileTypes.fromList((ArrayList<Object>) 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.
*
* <p>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.
*
* <p>Returns `null` if user cancels the operation.
*/
void openFile(
@Nullable String initialDirectory,
@NonNull FileTypes allowedTypes,
@NonNull Result<FileResponse> 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<List<FileResponse>> result);
/**
* Opens a file dialog for loading directories and returns a directory path.
*
* <p>Returns `null` if user cancels the operation.
*/
void getDirectoryPath(@Nullable String initialDirectory, @NonNull Result<String> result);
/** The codec used by FileSelectorApi. */
static @NonNull MessageCodec<Object> 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<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.FileSelectorApi.openFile", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
String initialDirectoryArg = (String) args.get(0);
FileTypes allowedTypesArg = (FileTypes) args.get(1);
Result<FileResponse> resultCallback =
new Result<FileResponse>() {
public void success(FileResponse result) {
wrapped.add(0, result);
reply.reply(wrapped);
}
public void error(Throwable error) {
ArrayList<Object> wrappedError = wrapError(error);
reply.reply(wrappedError);
}
};
api.openFile(initialDirectoryArg, allowedTypesArg, resultCallback);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.FileSelectorApi.openFiles", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
String initialDirectoryArg = (String) args.get(0);
FileTypes allowedTypesArg = (FileTypes) args.get(1);
Result<List<FileResponse>> resultCallback =
new Result<List<FileResponse>>() {
public void success(List<FileResponse> result) {
wrapped.add(0, result);
reply.reply(wrapped);
}
public void error(Throwable error) {
ArrayList<Object> wrappedError = wrapError(error);
reply.reply(wrappedError);
}
};
api.openFiles(initialDirectoryArg, allowedTypesArg, resultCallback);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.FileSelectorApi.getDirectoryPath", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
String initialDirectoryArg = (String) args.get(0);
Result<String> resultCallback =
new Result<String>() {
public void success(String result) {
wrapped.add(0, result);
reply.reply(wrapped);
}
public void error(Throwable error) {
ArrayList<Object> wrappedError = wrapError(error);
reply.reply(wrappedError);
}
};
api.getDirectoryPath(initialDirectoryArg, resultCallback);
});
} else {
channel.setMessageHandler(null);
}
}
}
}
}

View File

@ -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 <T> void setFinalStatic(
Class<T> 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<PluginRegistry.ActivityResultListener> 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<GeneratedFileSelectorApi.FileResponse> 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<PluginRegistry.ActivityResultListener> 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<List> fileListCaptor = ArgumentCaptor.forClass(List.class);
verify(mockResult).success(fileListCaptor.capture());
final List<GeneratedFileSelectorApi.FileResponse> 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<PluginRegistry.ActivityResultListener> 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/");
}
}

View File

@ -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.

View File

@ -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'
}

View File

@ -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<DriverExtensionActivity> 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()));
}
}

View File

@ -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<MainActivity> rule = new ActivityTestRule<>(MainActivity.class);
}

View File

@ -0,0 +1,24 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<application android:usesCleartextTraffic="true">
<activity
android:name="dev.flutter.packages.file_selector_android_example.DriverExtensionActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.Entrypoint"
android:value="integrationTestMain" />
</activity>
<provider
android:authorities="file_selector_android_test"
android:name=".TestContentProvider"
android:exported="true"/>
</application>
</manifest>

View File

@ -0,0 +1,33 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="file_selector_android_example"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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;
}
}

View File

@ -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 {}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -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"
}
}
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true

View File

@ -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

View File

@ -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"

View File

@ -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 {});
}

View File

@ -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: <Widget>[
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),
],
),
),
);
}
}

View File

@ -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: <String, WidgetBuilder>{
'/open/image': (BuildContext context) => const OpenImagePage(),
'/open/images': (BuildContext context) =>
const OpenMultipleImagesPage(),
'/open/text': (BuildContext context) => const OpenTextPage(),
},
);
}
}

View File

@ -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<void> _openImageFile(BuildContext context) async {
const XTypeGroup typeGroup = XTypeGroup(
label: 'images',
extensions: <String>['jpg', 'png'],
uniformTypeIdentifiers: <String>['public.image'],
);
final XFile? file = await FileSelectorPlatform.instance
.openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
if (file == null) {
// Operation was canceled by the user.
return;
}
final Uint8List bytes = await file.readAsBytes();
if (context.mounted) {
await showDialog<void>(
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: <Widget>[
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: <Widget>[
TextButton(
child: const Text('Close'),
onPressed: () {
Navigator.pop(context);
},
),
],
);
}
}

View File

@ -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<void> _openImageFile(BuildContext context) async {
const XTypeGroup jpgsTypeGroup = XTypeGroup(
label: 'JPEGs',
extensions: <String>['jpg', 'jpeg'],
uniformTypeIdentifiers: <String>['public.jpeg'],
);
const XTypeGroup pngTypeGroup = XTypeGroup(
label: 'PNGs',
extensions: <String>['png'],
uniformTypeIdentifiers: <String>['public.png'],
);
final List<XFile> files = await FileSelectorPlatform.instance
.openFiles(acceptedTypeGroups: <XTypeGroup>[
jpgsTypeGroup,
pngTypeGroup,
]);
if (files.isEmpty) {
// Operation was canceled by the user.
return;
}
final List<Uint8List> imageBytes = <Uint8List>[];
for (final XFile file in files) {
imageBytes.add(await file.readAsBytes());
}
if (context.mounted) {
await showDialog<void>(
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: <Widget>[
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<Uint8List> 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: <Widget>[
for (int i = 0; i < fileBytes.length; i++)
Flexible(
key: Key('result_image_name$i'),
child: Image.memory(fileBytes[i]),
)
],
),
),
actions: <Widget>[
TextButton(
child: const Text('Close'),
onPressed: () {
Navigator.pop(context);
},
),
],
);
}
}

View File

@ -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<void> _openTextFile(BuildContext context) async {
const XTypeGroup typeGroup = XTypeGroup(
label: 'text',
extensions: <String>['txt', 'json'],
uniformTypeIdentifiers: <String>['public.text'],
);
final XFile? file = await FileSelectorPlatform.instance
.openFile(acceptedTypeGroups: <XTypeGroup>[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<void>(
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: <Widget>[
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: <Widget>[
TextButton(
child: const Text('Close'),
onPressed: () => Navigator.pop(context),
),
],
);
}
}

View File

@ -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

View File

@ -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<void> main() => integrationDriver();

View File

@ -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';

View File

@ -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<XFile?> openFile({
List<XTypeGroup>? acceptedTypeGroups,
String? initialDirectory,
String? confirmButtonText,
}) async {
final FileResponse? file = await _api.openFile(
initialDirectory,
_fileTypesFromTypeGroups(acceptedTypeGroups),
);
return file == null ? null : _xFileFromFileResponse(file);
}
@override
Future<List<XFile>> openFiles({
List<XTypeGroup>? acceptedTypeGroups,
String? initialDirectory,
String? confirmButtonText,
}) async {
final List<FileResponse?> files = await _api.openFiles(
initialDirectory,
_fileTypesFromTypeGroups(acceptedTypeGroups),
);
return files
.cast<FileResponse>()
.map<XFile>(_xFileFromFileResponse)
.toList();
}
@override
Future<String?> 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<XTypeGroup>? typeGroups) {
if (typeGroups == null) {
return FileTypes(extensions: <String>[], mimeTypes: <String>[]);
}
final Set<String> mimeTypes = <String>{};
final Set<String> extensions = <String>{};
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 ?? <String>{});
extensions.addAll(group.extensions ?? <String>{});
}
return FileTypes(
mimeTypes: mimeTypes.toList(),
extensions: extensions.toList(),
);
}
}

View File

@ -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 <Object?>[
path,
mimeType,
name,
size,
bytes,
];
}
static FileResponse decode(Object result) {
result as List<Object?>;
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<String?> mimeTypes;
List<String?> extensions;
Object encode() {
return <Object?>[
mimeTypes,
extensions,
];
}
static FileTypes decode(Object result) {
result as List<Object?>;
return FileTypes(
mimeTypes: (result[0] as List<Object?>?)!.cast<String?>(),
extensions: (result[1] as List<Object?>?)!.cast<String?>(),
);
}
}
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<Object?> codec = _FileSelectorApiCodec();
/// Opens a file dialog for loading files and returns a file path.
///
/// Returns `null` if user cancels the operation.
Future<FileResponse?> openFile(
String? arg_initialDirectory, FileTypes arg_allowedTypes) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.FileSelectorApi.openFile', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_initialDirectory, arg_allowedTypes])
as List<Object?>?;
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<List<FileResponse?>> openFiles(
String? arg_initialDirectory, FileTypes arg_allowedTypes) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.FileSelectorApi.openFiles', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_initialDirectory, arg_allowedTypes])
as List<Object?>?;
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<Object?>?)!.cast<FileResponse?>();
}
}
/// Opens a file dialog for loading directories and returns a directory path.
///
/// Returns `null` if user cancels the operation.
Future<String?> getDirectoryPath(String? arg_initialDirectory) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.FileSelectorApi.getDirectoryPath', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_initialDirectory]) as List<Object?>?;
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?);
}
}
}

View File

@ -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.

View File

@ -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<String?> mimeTypes;
late List<String?> 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<FileResponse?> 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);
}

View File

@ -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

View File

@ -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(<Type>[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<FileSelectorAndroid>());
});
group('openFile', () {
test('passes the accepted type groups correctly', () async {
when(
mockApi.openFile(
'some/path/',
argThat(
isA<FileTypes>().having(
(FileTypes types) => types.mimeTypes,
'mimeTypes',
<String>['text/plain', 'image/jpg'],
).having(
(FileTypes types) => types.extensions,
'extensions',
<String>['txt', 'jpg'],
),
),
),
).thenAnswer(
(_) => Future<FileResponse?>.value(
FileResponse(
path: 'some/path.txt',
size: 30,
bytes: Uint8List(0),
name: 'name',
mimeType: 'text/plain',
),
),
);
const XTypeGroup group = XTypeGroup(
extensions: <String>['txt'],
mimeTypes: <String>['text/plain'],
);
const XTypeGroup group2 = XTypeGroup(
extensions: <String>['jpg'],
mimeTypes: <String>['image/jpg'],
);
final XFile? file = await plugin.openFile(
acceptedTypeGroups: <XTypeGroup>[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<FileTypes>().having(
(FileTypes types) => types.mimeTypes,
'mimeTypes',
<String>['text/plain', 'image/jpg'],
).having(
(FileTypes types) => types.extensions,
'extensions',
<String>['txt', 'jpg'],
),
),
),
).thenAnswer(
(_) => Future<List<FileResponse>>.value(
<FileResponse>[
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: <String>['txt'],
mimeTypes: <String>['text/plain'],
);
const XTypeGroup group2 = XTypeGroup(
extensions: <String>['jpg'],
mimeTypes: <String>['image/jpg'],
);
final List<XFile> files = await plugin.openFiles(
acceptedTypeGroups: <XTypeGroup>[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<String?>.value('some/path/chosen/'));
final String? path = await plugin.getDirectoryPath(
initialDirectory: 'some/path',
);
expect(path, 'some/path/chosen/');
});
}

View File

@ -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<List<_i2.FileResponse?>> openFiles(
String? arg_initialDirectory,
_i2.FileTypes? arg_allowedTypes,
) =>
(super.noSuchMethod(
Invocation.method(
#openFiles,
[
arg_initialDirectory,
arg_allowedTypes,
],
),
returnValue:
_i3.Future<List<_i2.FileResponse?>>.value(<_i2.FileResponse?>[]),
) as _i3.Future<List<_i2.FileResponse?>>);
@override
_i3.Future<String?> getDirectoryPath(String? arg_initialDirectory) =>
(super.noSuchMethod(
Invocation.method(
#getDirectoryPath,
[arg_initialDirectory],
),
returnValue: _i3.Future<String?>.value(),
) as _i3.Future<String?>);
}