mirror of
https://github.com/flutter/packages.git
synced 2025-06-30 14:47:22 +08:00
[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:
28
.github/dependabot.yml
vendored
28
.github/dependabot.yml
vendored
@ -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:
|
||||
|
6
packages/file_selector/file_selector_android/AUTHORS
Normal file
6
packages/file_selector/file_selector_android/AUTHORS
Normal 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.
|
@ -0,0 +1,3 @@
|
||||
## 0.5.0
|
||||
|
||||
* Implements file_selector_platform_interface for Android.
|
25
packages/file_selector/file_selector_android/LICENSE
Normal file
25
packages/file_selector/file_selector_android/LICENSE
Normal 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.
|
15
packages/file_selector/file_selector_android/README.md
Normal file
15
packages/file_selector/file_selector_android/README.md
Normal 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
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
rootProject.name = 'file_selector_android'
|
@ -0,0 +1,3 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="dev.flutter.packages.file_selector_android">
|
||||
</manifest>
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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/");
|
||||
}
|
||||
}
|
@ -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.
|
@ -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'
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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>
|
@ -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>
|
@ -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 {}
|
@ -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 {}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 {}
|
@ -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>
|
@ -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 |
@ -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>
|
@ -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>
|
@ -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>
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx4G
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
@ -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
|
@ -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"
|
@ -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 {});
|
||||
}
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
@ -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();
|
@ -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';
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
@ -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?);
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
@ -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);
|
||||
}
|
31
packages/file_selector/file_selector_android/pubspec.yaml
Normal file
31
packages/file_selector/file_selector_android/pubspec.yaml
Normal 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
|
@ -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/');
|
||||
});
|
||||
}
|
@ -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?>);
|
||||
}
|
Reference in New Issue
Block a user