diff --git a/packages/file_selector/file_selector/.metadata b/packages/file_selector/file_selector/.metadata new file mode 100644 index 000000000000..1b95e72d16de --- /dev/null +++ b/packages/file_selector/file_selector/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c + base_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c + - platform: android + create_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c + base_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/file_selector/file_selector_android/.metadata b/packages/file_selector/file_selector_android/.metadata new file mode 100644 index 000000000000..bf12a4a9748b --- /dev/null +++ b/packages/file_selector/file_selector_android/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c + channel: stable + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c + base_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c + - platform: android + create_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c + base_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/file_selector/file_selector_android/AUTHORS b/packages/file_selector/file_selector_android/AUTHORS new file mode 100644 index 000000000000..c58fa5c9c23b --- /dev/null +++ b/packages/file_selector/file_selector_android/AUTHORS @@ -0,0 +1,7 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +SOUTHWORKS \ No newline at end of file diff --git a/packages/file_selector/file_selector_android/CHANGELOG.md b/packages/file_selector/file_selector_android/CHANGELOG.md new file mode 100644 index 000000000000..efa5587e1f55 --- /dev/null +++ b/packages/file_selector/file_selector_android/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* Initial Android implementation of `file_selector`. diff --git a/packages/file_selector/file_selector_android/LICENSE b/packages/file_selector/file_selector_android/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/file_selector/file_selector_android/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/file_selector/file_selector_android/README.md b/packages/file_selector/file_selector_android/README.md new file mode 100644 index 000000000000..0e950015ac3e --- /dev/null +++ b/packages/file_selector/file_selector_android/README.md @@ -0,0 +1,11 @@ +# file\_selector\_anrdoid + +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. + +[1]: https://pub.dev/packages/file_selector +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/file_selector/file_selector_android/android/.gitignore b/packages/file_selector/file_selector_android/android/.gitignore new file mode 100644 index 000000000000..161bdcdaf88c --- /dev/null +++ b/packages/file_selector/file_selector_android/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/packages/file_selector/file_selector_android/android/build.gradle b/packages/file_selector/file_selector_android/android/build.gradle new file mode 100644 index 000000000000..3abc87311081 --- /dev/null +++ b/packages/file_selector/file_selector_android/android/build.gradle @@ -0,0 +1,63 @@ +group 'io.flutter.plugins.file_selector' +version '1.0' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 31 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion 21 + } + + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + } + + dependencies { + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:4.8.0' + testImplementation 'androidx.test:core:1.4.0' + testImplementation "org.robolectric:robolectric:4.8.1" + } + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} + +dependencies { + implementation project(path: ':flutter_plugin_android_lifecycle') +} diff --git a/packages/file_selector/file_selector_android/android/settings.gradle b/packages/file_selector/file_selector_android/android/settings.gradle new file mode 100644 index 000000000000..679b28be66a4 --- /dev/null +++ b/packages/file_selector/file_selector_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'file_selector_android' diff --git a/packages/file_selector/file_selector_android/android/src/main/AndroidManifest.xml b/packages/file_selector/file_selector_android/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..5ca240dc4b05 --- /dev/null +++ b/packages/file_selector/file_selector_android/android/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/packages/file_selector/file_selector_android/android/src/main/java/io/flutter/plugins/file_selector/ActivityStateHelper.java b/packages/file_selector/file_selector_android/android/src/main/java/io/flutter/plugins/file_selector/ActivityStateHelper.java new file mode 100644 index 000000000000..eaa926058a81 --- /dev/null +++ b/packages/file_selector/file_selector_android/android/src/main/java/io/flutter/plugins/file_selector/ActivityStateHelper.java @@ -0,0 +1,109 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.file_selector; + +import android.app.Activity; +import android.app.Application; +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.Lifecycle; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry; + +/** + * Move all activity-lifetime-bound states into this helper object, so that {@code setup} and {@code + * tearDown} would just become constructor and finalize calls of the helper object. + */ +public class ActivityStateHelper { + private Application application; + private Activity activity; + private FileSelectorDelegate delegate; + private MethodChannel channel; + private LifeCycleHelper observer; + private ActivityPluginBinding activityBinding; + + // This is null when not using v2 embedding; + private Lifecycle lifecycle; + + // Default constructor + ActivityStateHelper( + final String CHANNEL, + final Application application, + final Activity activity, + final BinaryMessenger messenger, + final MethodChannel.MethodCallHandler handler, + final PluginRegistry.Registrar registrar, + final ActivityPluginBinding activityBinding) { + this.application = application; + this.activity = activity; + this.activityBinding = activityBinding; + + delegate = constructDelegate(activity); + channel = new MethodChannel(messenger, CHANNEL); + channel.setMethodCallHandler(handler); + observer = new LifeCycleHelper(activity); + if (registrar != null) { + // V1 embedding setup for activity listeners. + application.registerActivityLifecycleCallbacks(observer); + registrar.addActivityResultListener(delegate); + registrar.addRequestPermissionsResultListener(delegate); + } else { + // V2 embedding setup for activity listeners. + activityBinding.addActivityResultListener(delegate); + activityBinding.addRequestPermissionsResultListener(delegate); + lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(activityBinding); + lifecycle.addObserver(observer); + } + } + + // Only invoked by {@link #FileSelectorPlugin(FileSelectorDelegate, Activity)} + // for testing. + ActivityStateHelper(final FileSelectorDelegate delegate, final Activity activity) { + this.activity = activity; + this.delegate = delegate; + } + + void release() { + if (activityBinding != null) { + activityBinding.removeActivityResultListener(delegate); + activityBinding.removeRequestPermissionsResultListener(delegate); + activityBinding = null; + } + + if (lifecycle != null) { + lifecycle.removeObserver(observer); + lifecycle = null; + } + + if (channel != null) { + channel.setMethodCallHandler(null); + channel = null; + } + + if (application != null) { + application.unregisterActivityLifecycleCallbacks(observer); + application = null; + } + + activity = null; + observer = null; + delegate = null; + } + + Activity getActivity() { + return activity; + } + + FileSelectorDelegate getDelegate() { + return delegate; + } + + @VisibleForTesting + FileSelectorDelegate constructDelegate(final Activity setupActivity) { + return new FileSelectorDelegate(setupActivity); + } +} diff --git a/packages/file_selector/file_selector_android/android/src/main/java/io/flutter/plugins/file_selector/FileSelectorDelegate.java b/packages/file_selector/file_selector_android/android/src/main/java/io/flutter/plugins/file_selector/FileSelectorDelegate.java new file mode 100644 index 000000000000..d65bbfb6f562 --- /dev/null +++ b/packages/file_selector/file_selector_android/android/src/main/java/io/flutter/plugins/file_selector/FileSelectorDelegate.java @@ -0,0 +1,163 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.file_selector; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry; +import java.util.ArrayList; + +/** + * A delegate class doing the heavy lifting for the plugin. + * + *

When invoked, all the "access file" methods go through the same steps: + * + *

1. Check for an existing {@link #pendingResult}. If a previous pendingResult exists, this + * means that the method was called at least twice. In this case, stop executing and finish with an + * error. + * + *

3. Launch the file explorer. + * + *

This can end up in two different outcomes: + * + *

A) User picks a file or directory. + * + *

B) User cancels picking a file or directory. Finish with null result. + */ +public class FileSelectorDelegate + implements PluginRegistry.ActivityResultListener, + PluginRegistry.RequestPermissionsResultListener { + @VisibleForTesting static final int REQUEST_CODE_GET_DIRECTORY_PATH = 2342; + + /** Constants for key types in the dart invoke methods */ + @VisibleForTesting static final String _confirmButtonText = "confirmButtonText"; + + @VisibleForTesting static final String _initialDirectory = "initialDirectory"; + + private final Activity activity; + + @Override + public boolean onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + return true; + } + + private MethodChannel.Result pendingResult; + private MethodCall methodCall; + + public FileSelectorDelegate(final Activity activity) { + this(activity, null, null); + } + + /** + * This constructor is used exclusively for testing; it can be used to provide mocks to final + * fields of this class. Otherwise those fields would have to be mutable and visible. + */ + @VisibleForTesting + FileSelectorDelegate( + final Activity activity, final MethodChannel.Result result, final MethodCall methodCall) { + this.activity = activity; + this.pendingResult = result; + this.methodCall = methodCall; + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void getDirectoryPath(MethodCall methodCall, MethodChannel.Result result) { + if (!setPendingMethodCallAndResult(methodCall, result)) { + finishWithAlreadyActiveError(result); + return; + } + + launchGetDirectoryPath(methodCall.argument(_initialDirectory)); + } + + private void launchGetDirectoryPath(@Nullable String initialDirectory) { + Intent getDirectoryPathIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + + if (initialDirectory != null && !initialDirectory.isEmpty()) { + Uri uri = getDirectoryPathIntent.getParcelableExtra("android.provider.extra.INITIAL_URI"); + String scheme = uri.toString(); + scheme = scheme.replace("/root/", initialDirectory); + uri = Uri.parse(scheme); + getDirectoryPathIntent.putExtra("android.provider.extra.INITIAL_URI", uri); + } + + activity.startActivityForResult(getDirectoryPathIntent, REQUEST_CODE_GET_DIRECTORY_PATH); + } + + @Override + public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_CODE_GET_DIRECTORY_PATH: + handleGetDirectoryPathResult(resultCode, data); + break; + default: + return false; + } + + return true; + } + + private void handleGetDirectoryPathResult(int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK && data != null) { + Uri path = data.getData(); + handleGetDirectoryPathResult(path.toString()); + return; + } + + finishWithSuccess(null); + } + + private void handleGetDirectoryPathResult(String path) { + finishWithSuccess(path); + } + + private boolean setPendingMethodCallAndResult( + MethodCall methodCall, MethodChannel.Result result) { + if (pendingResult != null) { + return false; + } + + this.methodCall = methodCall; + pendingResult = result; + + return true; + } + + private void finishWithSuccess(String srcPath) { + pendingResult.success(srcPath); + clearMethodCallAndResult(); + } + + private void finishWithListSuccess(ArrayList srcPaths) { + if (pendingResult == null) { + return; + } + pendingResult.success(srcPaths); + clearMethodCallAndResult(); + } + + private void finishWithAlreadyActiveError(MethodChannel.Result result) { + result.error("already_active", "File selector is already active", null); + } + + private void finishWithError(String errorCode, String errorMessage) { + pendingResult.error(errorCode, errorMessage, null); + clearMethodCallAndResult(); + } + + private void clearMethodCallAndResult() { + methodCall = null; + pendingResult = null; + } +} diff --git a/packages/file_selector/file_selector_android/android/src/main/java/io/flutter/plugins/file_selector/FileSelectorPlugin.java b/packages/file_selector/file_selector_android/android/src/main/java/io/flutter/plugins/file_selector/FileSelectorPlugin.java new file mode 100644 index 000000000000..98dd19083b92 --- /dev/null +++ b/packages/file_selector/file_selector_android/android/src/main/java/io/flutter/plugins/file_selector/FileSelectorPlugin.java @@ -0,0 +1,122 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.file_selector; + +import android.app.Activity; +import android.app.Application; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry; + +/** Android platform implementation of the FileSelectorPlugin. */ +public class FileSelectorPlugin + implements MethodChannel.MethodCallHandler, FlutterPlugin, ActivityAware { + + static final String METHOD_GET_DIRECTORY_PATH = "getDirectoryPath"; + static final String METHOD_OPEN_FILE = "openFile"; + static final String METHOD_GET_SAVE_PATH = "getSavePath"; + + private static final String CHANNEL = "plugins.flutter.io/file_selector_android"; + + private FlutterPluginBinding pluginBinding; + private ActivityStateHelper activityState; + + /** + * Default constructor for the plugin. + * + *

Use this constructor for production code. + */ + public FileSelectorPlugin() {} + + @VisibleForTesting + FileSelectorPlugin(final FileSelectorDelegate delegate, final Activity activity) { + activityState = new ActivityStateHelper(delegate, activity); + } + + @VisibleForTesting + final ActivityStateHelper getActivityState() { + return activityState; + } + + @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) { + setup( + pluginBinding.getBinaryMessenger(), + (Application) pluginBinding.getApplicationContext(), + binding.getActivity(), + null, + binding); + } + + @Override + public void onDetachedFromActivity() { + tearDown(); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity(); + } + + @Override + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { + onAttachedToActivity(binding); + } + + private void setup( + final BinaryMessenger messenger, + final Application application, + final Activity activity, + final PluginRegistry.Registrar registrar, + final ActivityPluginBinding activityBinding) { + activityState = + new ActivityStateHelper( + CHANNEL, application, activity, messenger, this, registrar, activityBinding); + } + + private void tearDown() { + if (activityState != null) { + activityState.release(); + activityState = null; + } + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result rawResult) { + if (activityState == null || activityState.getActivity() == null) { + rawResult.error("no_activity", "file_selector plugin requires a foreground activity.", null); + return; + } + MethodChannel.Result result = new MethodResultWrapper(rawResult); + FileSelectorDelegate delegate = activityState.getDelegate(); + switch (call.method) { + case METHOD_GET_DIRECTORY_PATH: + delegate.getDirectoryPath(call, result); + break; + case METHOD_GET_SAVE_PATH: + throw new UnsupportedOperationException("getSavePath is not supported yet"); + case METHOD_OPEN_FILE: + throw new UnsupportedOperationException("openFile is not supported yet"); + default: + throw new IllegalArgumentException("Unknown method " + call.method); + } + } +} diff --git a/packages/file_selector/file_selector_android/android/src/main/java/io/flutter/plugins/file_selector/LifeCycleHelper.java b/packages/file_selector/file_selector_android/android/src/main/java/io/flutter/plugins/file_selector/LifeCycleHelper.java new file mode 100644 index 000000000000..22471d6b4edb --- /dev/null +++ b/packages/file_selector/file_selector_android/android/src/main/java/io/flutter/plugins/file_selector/LifeCycleHelper.java @@ -0,0 +1,68 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.file_selector; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; + +public class LifeCycleHelper + implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver { + private final Activity thisActivity; + + LifeCycleHelper(Activity activity) { + this.thisActivity = activity; + } + + @Override + public void onCreate(@NonNull LifecycleOwner owner) {} + + @Override + public void onStart(@NonNull LifecycleOwner owner) {} + + @Override + public void onResume(@NonNull LifecycleOwner owner) {} + + @Override + public void onPause(@NonNull LifecycleOwner owner) {} + + @Override + public void onStop(@NonNull LifecycleOwner owner) { + onActivityStopped(thisActivity); + } + + @Override + public void onDestroy(@NonNull LifecycleOwner owner) { + onActivityDestroyed(thisActivity); + } + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) {} + + @Override + public void onActivityStarted(Activity activity) {} + + @Override + public void onActivityResumed(Activity activity) {} + + @Override + public void onActivityPaused(Activity activity) {} + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} + + @Override + public void onActivityDestroyed(Activity activity) { + if (thisActivity == activity && activity.getApplicationContext() != null) { + ((Application) activity.getApplicationContext()).unregisterActivityLifecycleCallbacks(this); + } + } + + @Override + public void onActivityStopped(Activity activity) {} +} diff --git a/packages/file_selector/file_selector_android/android/src/main/java/io/flutter/plugins/file_selector/MethodResultWrapper.java b/packages/file_selector/file_selector_android/android/src/main/java/io/flutter/plugins/file_selector/MethodResultWrapper.java new file mode 100644 index 000000000000..62ad26b5e1d3 --- /dev/null +++ b/packages/file_selector/file_selector_android/android/src/main/java/io/flutter/plugins/file_selector/MethodResultWrapper.java @@ -0,0 +1,54 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.file_selector; + +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.NonNull; +import io.flutter.plugin.common.MethodChannel; + +public class MethodResultWrapper implements MethodChannel.Result { + private final MethodChannel.Result methodResult; + private final Handler handler; + + MethodResultWrapper(MethodChannel.Result result) { + methodResult = result; + handler = new Handler(Looper.getMainLooper()); + } + + @Override + public void success(final Object result) { + handler.post( + new Runnable() { + @Override + public void run() { + methodResult.success(result); + } + }); + } + + @Override + public void error( + @NonNull final String errorCode, final String errorMessage, final Object errorDetails) { + handler.post( + new Runnable() { + @Override + public void run() { + methodResult.error(errorCode, errorMessage, errorDetails); + } + }); + } + + @Override + public void notImplemented() { + handler.post( + new Runnable() { + @Override + public void run() { + methodResult.notImplemented(); + } + }); + } +} diff --git a/packages/file_selector/file_selector_android/android/src/test/java/io/flutter/plugins/file_selector/FileSelectorDelegateTest.java b/packages/file_selector/file_selector_android/android/src/test/java/io/flutter/plugins/file_selector/FileSelectorDelegateTest.java new file mode 100644 index 000000000000..d3834be65a40 --- /dev/null +++ b/packages/file_selector/file_selector_android/android/src/test/java/io/flutter/plugins/file_selector/FileSelectorDelegateTest.java @@ -0,0 +1,84 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.file_selector; + +import static io.flutter.plugins.file_selector.FileSelectorPlugin.METHOD_GET_DIRECTORY_PATH; +import static io.flutter.plugins.file_selector.TestHelpers.buildMethodCall; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class FileSelectorDelegateTest { + @Mock Activity mockActivity; + @Mock MethodChannel.Result mockResult; + @Mock Intent mockIntent; + + @Mock Uri mockUri; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + + mockUri = mock(Uri.class); + when(mockIntent.getData()).thenReturn(mockUri); + } + + @Test + public void getDirectoryPath_WhenPendingResultExists_FinishesWithAlreadyActiveError() { + MethodCall call = buildMethodCall(METHOD_GET_DIRECTORY_PATH); + FileSelectorDelegate delegate = new FileSelectorDelegate(mockActivity, mockResult, call); + + delegate.getDirectoryPath(call, mockResult); + + verifyFinishedWithAlreadyActiveError(); + verifyNoMoreInteractions(mockResult); + } + + @Test + public void onActivityResult_WhenGetDirectoryPathCanceled_FinishesWithNull() { + MethodCall call = buildMethodCall(METHOD_GET_DIRECTORY_PATH); + FileSelectorDelegate delegate = createDelegateWithPendingResultAndMethodCall(call); + + delegate.onActivityResult( + FileSelectorDelegate.REQUEST_CODE_GET_DIRECTORY_PATH, Activity.RESULT_CANCELED, null); + + verify(mockResult).success(null); + verifyNoMoreInteractions(mockResult); + } + + @Test + public void onActivityResult_GetDirectoryPathReturnsSuccessfully() { + MethodCall call = buildMethodCall(METHOD_GET_DIRECTORY_PATH); + FileSelectorDelegate delegate = createDelegateWithPendingResultAndMethodCall(call); + delegate.onActivityResult( + FileSelectorDelegate.REQUEST_CODE_GET_DIRECTORY_PATH, Activity.RESULT_OK, mockIntent); + + verify(mockResult).success(mockUri.toString()); + verifyNoMoreInteractions(mockResult); + } + + private FileSelectorDelegate createDelegate() { + return new FileSelectorDelegate(mockActivity, null, null); + } + + private FileSelectorDelegate createDelegateWithPendingResultAndMethodCall(MethodCall call) { + return new FileSelectorDelegate(mockActivity, mockResult, call); + } + + private void verifyFinishedWithAlreadyActiveError() { + verify(mockResult).error("already_active", "File selector is already active", null); + } +} diff --git a/packages/file_selector/file_selector_android/android/src/test/java/io/flutter/plugins/file_selector/FileSelectorPluginTest.java b/packages/file_selector/file_selector_android/android/src/test/java/io/flutter/plugins/file_selector/FileSelectorPluginTest.java new file mode 100644 index 000000000000..eda968da54f3 --- /dev/null +++ b/packages/file_selector/file_selector_android/android/src/test/java/io/flutter/plugins/file_selector/FileSelectorPluginTest.java @@ -0,0 +1,115 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.file_selector; + +import static io.flutter.plugins.file_selector.FileSelectorPlugin.METHOD_GET_DIRECTORY_PATH; +import static io.flutter.plugins.file_selector.TestHelpers.buildMethodCall; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.app.Application; +import androidx.lifecycle.Lifecycle; +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class FileSelectorPluginTest { + @Mock io.flutter.plugin.common.PluginRegistry.Registrar mockRegistrar; + + @Mock ActivityPluginBinding mockActivityBinding; + @Mock FlutterPluginBinding mockPluginBinding; + + @Mock Activity mockActivity; + @Mock Application mockApplication; + @Mock FileSelectorDelegate mockFileSelectorDelegate; + @Mock MethodChannel.Result mockResult; + + FileSelectorPlugin plugin; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + when(mockRegistrar.context()).thenReturn(mockApplication); + when(mockActivityBinding.getActivity()).thenReturn(mockActivity); + when(mockPluginBinding.getApplicationContext()).thenReturn(mockApplication); + plugin = new FileSelectorPlugin(mockFileSelectorDelegate, mockActivity); + } + + @Test + public void onMethodCall_WhenActivityIsNull_FinishesWithForegroundActivityRequiredError() { + MethodCall call = buildMethodCall(METHOD_GET_DIRECTORY_PATH); + FileSelectorPlugin fileSelectorPluginWithNullActivity = + new FileSelectorPlugin(mockFileSelectorDelegate, null); + fileSelectorPluginWithNullActivity.onMethodCall(call, mockResult); + verify(mockResult) + .error("no_activity", "file_selector plugin requires a foreground activity.", null); + verifyNoInteractions(mockFileSelectorDelegate); + } + + @Test + public void onMethodCall_WhenCalledWithUnknownMethod_ThrowsException() { + String method = "unknown_test_method"; + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> plugin.onMethodCall(new MethodCall(method, null), mockResult)); + assertEquals("Unknown method " + method, exception.getMessage()); + verifyNoInteractions(mockFileSelectorDelegate); + verifyNoInteractions(mockResult); + } + + @Test + public void + onMethodCall_GetDirectoryPath_WhenCalledWithoutInitialDirectory_InvokesRootSourceFolder() { + MethodCall call = buildMethodCall(METHOD_GET_DIRECTORY_PATH, null, null); + plugin.onMethodCall(call, mockResult); + verify(mockFileSelectorDelegate).getDirectoryPath(eq(call), any()); + verifyNoInteractions(mockResult); + } + + @Test + public void onMethodCall_GetDirectoryPath_WhenCalledWithInitialDirectory_InvokesSourceFolder() { + MethodCall call = buildMethodCall(METHOD_GET_DIRECTORY_PATH, "Documents", null); + plugin.onMethodCall(call, mockResult); + verify(mockFileSelectorDelegate).getDirectoryPath(eq(call), any()); + verifyNoInteractions(mockResult); + } + + @Test + public void onDetachedFromActivity_ShouldReleaseActivityState() { + final BinaryMessenger mockBinaryMessenger = mock(BinaryMessenger.class); + when(mockPluginBinding.getBinaryMessenger()).thenReturn(mockBinaryMessenger); + + final HiddenLifecycleReference mockLifecycleReference = mock(HiddenLifecycleReference.class); + when(mockActivityBinding.getLifecycle()).thenReturn(mockLifecycleReference); + + final Lifecycle mockLifecycle = mock(Lifecycle.class); + when(mockLifecycleReference.getLifecycle()).thenReturn(mockLifecycle); + + plugin.onAttachedToEngine(mockPluginBinding); + plugin.onAttachedToActivity(mockActivityBinding); + assertNotNull(plugin.getActivityState()); + + plugin.onDetachedFromActivity(); + assertNull(plugin.getActivityState()); + } +} diff --git a/packages/file_selector/file_selector_android/android/src/test/java/io/flutter/plugins/file_selector/TestHelpers.java b/packages/file_selector/file_selector_android/android/src/test/java/io/flutter/plugins/file_selector/TestHelpers.java new file mode 100644 index 000000000000..69fbef11c10d --- /dev/null +++ b/packages/file_selector/file_selector_android/android/src/test/java/io/flutter/plugins/file_selector/TestHelpers.java @@ -0,0 +1,32 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.file_selector; + +import static io.flutter.plugins.file_selector.FileSelectorDelegate._confirmButtonText; +import static io.flutter.plugins.file_selector.FileSelectorDelegate._initialDirectory; + +import androidx.annotation.Nullable; +import io.flutter.plugin.common.MethodCall; +import java.util.HashMap; +import java.util.Map; + +public class TestHelpers { + public static MethodCall buildMethodCall( + String method, @Nullable String initialDirectory, @Nullable String confirmButtonText) { + final Map arguments = new HashMap<>(); + if (initialDirectory != null) { + arguments.put(_initialDirectory, initialDirectory); + } + if (confirmButtonText != null) { + arguments.put(_confirmButtonText, confirmButtonText); + } + + return new MethodCall(method, arguments); + } + + public static MethodCall buildMethodCall(String method) { + return new MethodCall(method, null); + } +} diff --git a/packages/file_selector/file_selector_android/example/.gitignore b/packages/file_selector/file_selector_android/example/.gitignore new file mode 100644 index 000000000000..24476c5d1eb5 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/file_selector/file_selector_android/example/README.md b/packages/file_selector/file_selector_android/example/README.md new file mode 100644 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/file_selector/file_selector_android/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/file_selector/file_selector_android/example/android/.gitignore b/packages/file_selector/file_selector_android/example/android/.gitignore new file mode 100644 index 000000000000..6f568019d3c6 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/packages/file_selector/file_selector_android/example/android/app/build.gradle b/packages/file_selector/file_selector_android/example/android/app/build.gradle new file mode 100644 index 000000000000..ae0249365ffb --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/build.gradle @@ -0,0 +1,59 @@ +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 { + 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 "io.flutter.plugins.file_selector_example" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. + minSdkVersion 21 + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + 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 '../..' +} diff --git a/packages/file_selector/file_selector_android/example/android/app/src/debug/AndroidManifest.xml b/packages/file_selector/file_selector_android/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..225a4da945bd --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/AndroidManifest.xml b/packages/file_selector/file_selector_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..80ab11495b7b --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/java/io/flutter/plugins/file_selector_example/MainActivity.java b/packages/file_selector/file_selector_android/example/android/app/src/main/java/io/flutter/plugins/file_selector_example/MainActivity.java new file mode 100644 index 000000000000..1292350022cc --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/main/java/io/flutter/plugins/file_selector_example/MainActivity.java @@ -0,0 +1,9 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.file_selector_example; + +import io.flutter.embedding.android.FlutterActivity; + +public class MainActivity extends FlutterActivity {} diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/file_selector/file_selector_android/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000000..f74085f3f6a2 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/drawable/launch_background.xml b/packages/file_selector/file_selector_android/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..304732f88420 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000000..db77bb4b7b09 Binary files /dev/null and b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000000..17987b79bb8a Binary files /dev/null and b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000000..09d4391482be Binary files /dev/null and b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000000..d5f1c8d34e7a Binary files /dev/null and b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000000..4d6372eebdb2 Binary files /dev/null and b/packages/file_selector/file_selector_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/values-night/styles.xml b/packages/file_selector/file_selector_android/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000000..06952be745f9 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/file_selector/file_selector_android/example/android/app/src/main/res/values/styles.xml b/packages/file_selector/file_selector_android/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..cb1ef88056ed --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/file_selector/file_selector_android/example/android/app/src/profile/AndroidManifest.xml b/packages/file_selector/file_selector_android/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000000..225a4da945bd --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/packages/file_selector/file_selector_android/example/android/build.gradle b/packages/file_selector/file_selector_android/example/android/build.gradle new file mode 100644 index 000000000000..83ae220041c7 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + 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') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/file_selector/file_selector_android/example/android/gradle.properties b/packages/file_selector/file_selector_android/example/android/gradle.properties new file mode 100644 index 000000000000..94adc3a3f97a --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/file_selector/file_selector_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/file_selector/file_selector_android/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..cb24abda10ae --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/packages/file_selector/file_selector_android/example/android/settings.gradle b/packages/file_selector/file_selector_android/example/android/settings.gradle new file mode 100644 index 000000000000..44e62bcf06ae --- /dev/null +++ b/packages/file_selector/file_selector_android/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/packages/file_selector/file_selector_android/example/lib/get_directory_page.dart b/packages/file_selector/file_selector_android/example/lib/get_directory_page.dart new file mode 100644 index 000000000000..8fcf9664cbfb --- /dev/null +++ b/packages/file_selector/file_selector_android/example/lib/get_directory_page.dart @@ -0,0 +1,78 @@ +// 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 directory using `getDirectoryPath`, +/// then displays the selected directory in a dialog. +class GetDirectoryPage extends StatelessWidget { + /// Constructor using keys + const GetDirectoryPage({Key? key}) : super(key: key); + + Future _getDirectoryPath(BuildContext context) async { + final String? directoryPath = + await FileSelectorPlatform.instance.getDirectoryPath(); + + // Operation was canceled by the user. + await showDialog( + context: context, + builder: (BuildContext context) => + TextDisplay(directoryPath ?? 'Something went wrong'), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Get a directory path'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + // ignore: deprecated_member_use + primary: Colors.blue, + // ignore: deprecated_member_use + onPrimary: Colors.white, + ), + child: const Text('Press to ask user to choose a directory'), + onPressed: () => _getDirectoryPath(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog. +class TextDisplay extends StatelessWidget { + /// Creates a `TextDisplay`. + const TextDisplay(this.directoryPath, {Key? key}) : super(key: key); + + /// The path selected in the dialog. + final String directoryPath; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Selected Directory'), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(directoryPath), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_android/example/lib/home_page.dart b/packages/file_selector/file_selector_android/example/lib/home_page.dart new file mode 100644 index 000000000000..838597a5383b --- /dev/null +++ b/packages/file_selector/file_selector_android/example/lib/home_page.dart @@ -0,0 +1,62 @@ +// 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({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final ButtonStyle style = ElevatedButton.styleFrom( + // ignore: deprecated_member_use + primary: Colors.blue, + // ignore: deprecated_member_use + onPrimary: Colors.white, + ); + return Scaffold( + appBar: AppBar( + title: const Text('File Selector Demo Home Page'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: style, + child: const Text('Open a text file'), + onPressed: () => Navigator.pushNamed(context, '/open/text'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open an image'), + onPressed: () => Navigator.pushNamed(context, '/open/image'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open multiple images'), + onPressed: () => Navigator.pushNamed(context, '/open/images'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Save a file'), + onPressed: () => Navigator.pushNamed(context, '/save/text'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open a get directory dialog'), + onPressed: () => Navigator.pushNamed(context, '/directory'), + ), + ], + ), + ), + ); + } +} diff --git a/packages/file_selector/file_selector_android/example/lib/main.dart b/packages/file_selector/file_selector_android/example/lib/main.dart new file mode 100644 index 000000000000..5a0deaa6d319 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/lib/main.dart @@ -0,0 +1,41 @@ +// 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'; +import 'get_directory_page.dart'; +import 'home_page.dart'; +import 'open_image_page.dart'; +import 'open_multiple_images_page.dart'; +import 'open_text_page.dart'; +import 'save_text_page.dart'; + +void main() { + runApp(const MyApp()); +} + +/// MyApp is the Main Application. +class MyApp extends StatelessWidget { + /// Default Constructor + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'File Selector Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: const HomePage(), + routes: { + '/open/text': (BuildContext context) => const OpenTextPage(), + '/open/image': (BuildContext context) => const OpenImagePage(), + '/open/images': (BuildContext context) => + const OpenMultipleImagesPage(), + '/save/text': (BuildContext context) => SaveTextPage(), + '/directory': (BuildContext context) => const GetDirectoryPage(), + }, + ); + } +} diff --git a/packages/file_selector/file_selector_android/example/lib/open_image_page.dart b/packages/file_selector/file_selector_android/example/lib/open_image_page.dart new file mode 100644 index 000000000000..5d11130a9abb --- /dev/null +++ b/packages/file_selector/file_selector_android/example/lib/open_image_page.dart @@ -0,0 +1,93 @@ +// 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:io'; + +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({Key? key}) : super(key: key); + + Future _openImageFile(BuildContext context) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'images', + extensions: ['jpg', 'png'], + ); + final XFile? file = await FileSelectorPlatform.instance + .openFile(acceptedTypeGroups: [typeGroup]); + if (file == null) { + // Operation was canceled by the user. + return; + } + final String fileName = file.name; + final String filePath = file.path; + + await showDialog( + context: context, + builder: (BuildContext context) => ImageDisplay(fileName, filePath), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open an image'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + // ignore: deprecated_member_use + primary: Colors.blue, + // ignore: deprecated_member_use + onPrimary: 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.fileName, this.filePath, {Key? key}) + : super(key: key); + + /// The name of the selected file. + final String fileName; + + /// The path to the selected file. + final String filePath; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(fileName), + // On web the filePath is a blob url + // while on other platforms it is a system path. + content: kIsWeb ? Image.network(filePath) : Image.file(File(filePath)), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_android/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector_android/example/lib/open_multiple_images_page.dart new file mode 100644 index 000000000000..2fc3be940d66 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/lib/open_multiple_images_page.dart @@ -0,0 +1,104 @@ +// 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:io'; + +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({Key? key}) : super(key: key); + + Future _openImageFile(BuildContext context) async { + const XTypeGroup jpgsTypeGroup = XTypeGroup( + label: 'JPEGs', + extensions: ['jpg', 'jpeg'], + ); + const XTypeGroup pngTypeGroup = XTypeGroup( + label: 'PNGs', + extensions: ['png'], + ); + final List files = await FileSelectorPlatform.instance + .openFiles(acceptedTypeGroups: [ + jpgsTypeGroup, + pngTypeGroup, + ]); + if (files.isEmpty) { + // Operation was canceled by the user. + return; + } + await showDialog( + context: context, + builder: (BuildContext context) => MultipleImagesDisplay(files), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open multiple images'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + // ignore: deprecated_member_use + primary: Colors.blue, + // ignore: deprecated_member_use + onPrimary: 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.files, {Key? key}) : super(key: key); + + /// The files containing the images. + final List files; + + @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: [ + ...files.map( + (XFile file) => Flexible( + child: kIsWeb + ? Image.network(file.path) + : Image.file(File(file.path))), + ) + ], + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_android/example/lib/open_text_page.dart b/packages/file_selector/file_selector_android/example/lib/open_text_page.dart new file mode 100644 index 000000000000..92a4dbe9b793 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/lib/open_text_page.dart @@ -0,0 +1,90 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/material.dart'; + +/// Screen that allows the user to select a text file using `openFile`, then +/// displays its contents in a dialog. +class OpenTextPage extends StatelessWidget { + /// Default Constructor + const OpenTextPage({Key? key}) : super(key: key); + + Future _openTextFile(BuildContext context) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'text', + extensions: ['txt', 'json'], + ); + final XFile? file = await FileSelectorPlatform.instance + .openFile(acceptedTypeGroups: [typeGroup]); + if (file == null) { + // Operation was canceled by the user. + return; + } + final String fileName = file.name; + final String fileContent = await file.readAsString(); + + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(fileName, fileContent), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open a text file'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + // ignore: deprecated_member_use + primary: Colors.blue, + // ignore: deprecated_member_use + onPrimary: 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, {Key? key}) + : super(key: key); + + /// The name of the selected file. + final String fileName; + + /// The contents of the text file. + final String fileContent; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(fileName), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(fileContent), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_android/example/lib/save_text_page.dart b/packages/file_selector/file_selector_android/example/lib/save_text_page.dart new file mode 100644 index 000000000000..232c0854ced5 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/lib/save_text_page.dart @@ -0,0 +1,83 @@ +// 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_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/material.dart'; + +/// Screen that allows the user to select a save location using `getSavePath`, +/// then writes text to a file at that location. +class SaveTextPage extends StatelessWidget { + /// Default Constructor + SaveTextPage({Key? key}) : super(key: key); + + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _contentController = TextEditingController(); + + Future _saveFile() async { + final String fileName = _nameController.text; + final String? path = await FileSelectorPlatform.instance.getSavePath( + suggestedName: fileName, + ); + if (path == null) { + // Operation was canceled by the user. + return; + } + final String text = _contentController.text; + final Uint8List fileData = Uint8List.fromList(text.codeUnits); + const String fileMimeType = 'text/plain'; + final XFile textFile = + XFile.fromData(fileData, mimeType: fileMimeType, name: fileName); + await textFile.saveTo(path); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Save text into a file'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 300, + child: TextField( + minLines: 1, + maxLines: 12, + controller: _nameController, + decoration: const InputDecoration( + hintText: '(Optional) Suggest File Name', + ), + ), + ), + SizedBox( + width: 300, + child: TextField( + minLines: 1, + maxLines: 12, + controller: _contentController, + decoration: const InputDecoration( + hintText: 'Enter File Contents', + ), + ), + ), + const SizedBox(height: 10), + ElevatedButton( + style: ElevatedButton.styleFrom( + // ignore: deprecated_member_use + primary: Colors.blue, + // ignore: deprecated_member_use + onPrimary: Colors.white, + ), + onPressed: () => _saveFile(), + child: const Text('Press to save a text file'), + ), + ], + ), + ), + ); + } +} diff --git a/packages/file_selector/file_selector_android/example/pubspec.yaml b/packages/file_selector/file_selector_android/example/pubspec.yaml new file mode 100644 index 000000000000..c7111985232f --- /dev/null +++ b/packages/file_selector/file_selector_android/example/pubspec.yaml @@ -0,0 +1,25 @@ +name: file_selector_android_example +description: Demonstrates how to use the file_selector_android plugin. + +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +dependencies: + file_selector_android: + path: ../ + file_selector_platform_interface: ^2.1.0 + flutter: + sdk: flutter + flutter_plugin_android_lifecycle: ^2.0.1 + +dev_dependencies: + flutter_lints: ^2.0.0 + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true + diff --git a/packages/file_selector/file_selector_android/example/test/widget_test.dart b/packages/file_selector/file_selector_android/example/test/widget_test.dart new file mode 100644 index 000000000000..a867c64df3f2 --- /dev/null +++ b/packages/file_selector/file_selector_android/example/test/widget_test.dart @@ -0,0 +1,66 @@ +// 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. + +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:file_selector_android_example/main.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Verify corresponding elements are displayed', + (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + expect( + find.byWidgetPredicate( + (Widget widget) => + widget is Text && widget.data!.startsWith('File Selector Demo'), + ), + findsOneWidget, + ); + expect( + find.byWidgetPredicate( + (Widget widget) => + widget is Text && widget.data!.startsWith('Open a text file'), + ), + findsOneWidget, + ); + expect( + find.byWidgetPredicate( + (Widget widget) => + widget is Text && widget.data!.startsWith('Open an image'), + ), + findsOneWidget, + ); + expect( + find.byWidgetPredicate( + (Widget widget) => + widget is Text && widget.data!.startsWith('Open multiple images'), + ), + findsOneWidget, + ); + expect( + find.byWidgetPredicate( + (Widget widget) => + widget is Text && widget.data!.startsWith('Save a file'), + ), + findsOneWidget, + ); + expect( + find.byWidgetPredicate( + (Widget widget) => + widget is Text && + widget.data!.startsWith('Open a get directory dialog'), + ), + findsOneWidget, + ); + }); +} diff --git a/packages/file_selector/file_selector_android/lib/file_selector_android.dart b/packages/file_selector/file_selector_android/lib/file_selector_android.dart new file mode 100644 index 000000000000..5c1306b9a31d --- /dev/null +++ b/packages/file_selector/file_selector_android/lib/file_selector_android.dart @@ -0,0 +1,149 @@ +// 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:async'; + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; + +const MethodChannel _channel = + MethodChannel('plugins.flutter.io/file_selector_android'); + +const String _typeGroupLabelKey = 'label'; +const String _typeGroupExtensionsKey = 'extensions'; +const String _typeGroupMimeTypesKey = 'mimeTypes'; + +const String _openFileMethod = 'openFile'; +const String _getSavePathMethod = 'getSavePath'; +const String _getDirectoryPathMethod = 'getDirectoryPath'; + +const String _acceptedTypeGroupsKey = 'acceptedTypeGroups'; +const String _confirmButtonTextKey = 'confirmButtonText'; +const String _initialDirectoryKey = 'initialDirectory'; +const String _multipleKey = 'multiple'; +const String _suggestedNameKey = 'suggestedName'; + +/// An implementation of [FileSelectorPlatform] for Android. +class FileSelectorAndroid extends FileSelectorPlatform { + /// The MethodChannel that is being used by this implementation of the plugin. + @visibleForTesting + MethodChannel get channel => _channel; + + /// Registers the Android implementation. + static void registerWith() { + FileSelectorPlatform.instance = FileSelectorAndroid(); + } + + @override + Future openFile({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + final List> serializedTypeGroups = + _serializeTypeGroups(acceptedTypeGroups); + + final List? path = await _channel + .invokeListMethod(_openFileMethod, { + if (serializedTypeGroups.isNotEmpty) + _acceptedTypeGroupsKey: serializedTypeGroups, + _initialDirectoryKey: initialDirectory, + _confirmButtonTextKey: confirmButtonText, + _multipleKey: false, + }); + + return path == null ? null : XFile(path.first); + } + + @override + Future> openFiles({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + final List> serializedTypeGroups = + _serializeTypeGroups(acceptedTypeGroups); + + final List? pathList = await _channel + .invokeListMethod(_openFileMethod, { + if (serializedTypeGroups.isNotEmpty) + _acceptedTypeGroupsKey: serializedTypeGroups, + _initialDirectoryKey: initialDirectory, + _confirmButtonTextKey: confirmButtonText, + _multipleKey: false, + }); + + return pathList?.map((String path) => XFile(path)).toList() ?? []; + } + + /// We can't currently set the Confirm Button Text + /// For references, please check the following link + /// https://developer.android.com/reference/android/content/Intent#ACTION_OPEN_DOCUMENT_TREE + @override + Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, + }) async { + return _channel.invokeMethod( + _getDirectoryPathMethod, + { + _initialDirectoryKey: initialDirectory, + }, + ); + } + + @override + Future getSavePath({ + List? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + }) async { + final List> serializedTypeGroups = + _serializeTypeGroups(acceptedTypeGroups); + + return _channel.invokeMethod( + _getSavePathMethod, + { + if (serializedTypeGroups.isNotEmpty) + _acceptedTypeGroupsKey: serializedTypeGroups, + _initialDirectoryKey: initialDirectory, + _suggestedNameKey: suggestedName, + _confirmButtonTextKey: confirmButtonText, + }, + ); + } +} + +List> _serializeTypeGroups(List? groups) { + return (groups ?? []).map(_serializeTypeGroup).toList(); +} + +Map _serializeTypeGroup(XTypeGroup group) { + final Map serialization = { + _typeGroupLabelKey: group.label ?? '', + }; + if (group.allowsAny) { + serialization[_typeGroupExtensionsKey] = ['*']; + } else { + if ((group.extensions?.isEmpty ?? true) && + (group.mimeTypes?.isEmpty ?? true)) { + throw ArgumentError('Provided type group $group does not allow ' + 'all files, but does not set any of the Linux-supported filter ' + 'categories. "extensions" or "mimeTypes" must be non-empty for Linux ' + 'if anything is non-empty.'); + } + if (group.extensions?.isNotEmpty ?? false) { + serialization[_typeGroupExtensionsKey] = group.extensions + ?.map((String extension) => '*.$extension') + .toList() ?? + []; + } + if (group.mimeTypes?.isNotEmpty ?? false) { + serialization[_typeGroupMimeTypesKey] = group.mimeTypes ?? []; + } + } + return serialization; +} diff --git a/packages/file_selector/file_selector_android/pubspec.yaml b/packages/file_selector/file_selector_android/pubspec.yaml new file mode 100644 index 000000000000..9fb7b4481993 --- /dev/null +++ b/packages/file_selector/file_selector_android/pubspec.yaml @@ -0,0 +1,34 @@ +name: file_selector_android +description: Android implementation of the file_selector plugin. +repository: https://github.com/flutter/plugins/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.0.1 + +publish_to: 'none' # Remove this line when we make the first publish to pub.dev + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.10.0" + +flutter: + plugin: + implements: file_selector + platforms: + android: + package: io.flutter.plugins.file_selector + dartPluginClass: FileSelectorAndroid + pluginClass: FileSelectorPlugin + +dependencies: + file_selector_platform_interface: ^2.2.0 + flutter: + sdk: flutter + +dev_dependencies: + build_runner: 2.1.11 + flutter_lints: ^2.0.0 + flutter_plugin_android_lifecycle: ^2.0.1 + flutter_test: + sdk: flutter + mockito: ^5.1.0 + pigeon: ^3.2.5 diff --git a/packages/file_selector/file_selector_android/test/file_selector_android_test.dart b/packages/file_selector/file_selector_android/test/file_selector_android_test.dart new file mode 100644 index 000000000000..5184fba02ea4 --- /dev/null +++ b/packages/file_selector/file_selector_android/test/file_selector_android_test.dart @@ -0,0 +1,57 @@ +// 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/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late FileSelectorAndroid plugin; + late List log; + + setUp(() { + plugin = FileSelectorAndroid(); + log = []; + plugin.channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return null; + }); + }); + + test('registers instance', () async { + FileSelectorAndroid.registerWith(); + expect(FileSelectorPlatform.instance, isA()); + }); + + group('#getDirectoryPath', () { + test('passes initialDirectory correctly', () async { + await plugin.getDirectoryPath(initialDirectory: '/example/directory'); + + expect( + log, + [ + isMethodCall('getDirectoryPath', arguments: { + 'initialDirectory': '/example/directory' + }), + ], + ); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.getDirectoryPath(confirmButtonText: 'Open File'); + + expect( + log, + [ + isMethodCall('getDirectoryPath', arguments: { + 'initialDirectory': null, + }), + ], + ); + }); + }); +}