mirror of
https://github.com/flutter/packages.git
synced 2025-06-30 23:03:11 +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: "*"
|
- dependency-name: "*"
|
||||||
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
|
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"
|
- package-ecosystem: "gradle"
|
||||||
directory: "/packages/flutter_adaptive_scaffold/example/android/app"
|
directory: "/packages/flutter_adaptive_scaffold/example/android/app"
|
||||||
commit-message:
|
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