diff --git a/packages/image_picker/CHANGELOG.md b/packages/image_picker/CHANGELOG.md index 6592c9ee4c1a..bd0c28971a1b 100644 --- a/packages/image_picker/CHANGELOG.md +++ b/packages/image_picker/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.6.3 + +* Support Android V2 embedding. +* Migrate to using the new e2e test binding. + ## 0.6.2+3 * Remove the deprecated `author:` field from pubspec.yaml * Migrate the plugin to the pubspec platforms manifest. diff --git a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java index e34a3b5632c0..f2a0b02c821d 100644 --- a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java +++ b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java @@ -199,6 +199,7 @@ public void onScanCompleted(String path, Uri uri) { this.cache = cache; } + // Save the state of the image picker so it can be retrieved with `retrieveLostImage`. void saveStateBeforeResult() { if (methodCall == null) { return; diff --git a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java index b495a8e1a33f..950304c9c9e7 100644 --- a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java +++ b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java @@ -10,13 +10,86 @@ import android.os.Environment; import android.os.Handler; import android.os.Looper; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +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.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; +import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.PluginRegistry; import java.io.File; -public class ImagePickerPlugin implements MethodChannel.MethodCallHandler { +@SuppressWarnings("deprecation") +public class ImagePickerPlugin + implements MethodChannel.MethodCallHandler, FlutterPlugin, ActivityAware { + + private class LifeCycleObserver + implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver { + private final Activity thisActivity; + + LifeCycleObserver(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); // Use getApplicationContext() to avoid casting failures + } + } + + @Override + public void onActivityStopped(Activity activity) { + if (thisActivity == activity) { + delegate.saveStateBeforeResult(); + } + } + } static final String METHOD_CALL_IMAGE = "pickImage"; static final String METHOD_CALL_VIDEO = "pickVideo"; @@ -27,9 +100,15 @@ public class ImagePickerPlugin implements MethodChannel.MethodCallHandler { private static final int SOURCE_CAMERA = 0; private static final int SOURCE_GALLERY = 1; - private final PluginRegistry.Registrar registrar; + private MethodChannel channel; private ImagePickerDelegate delegate; - private Application.ActivityLifecycleCallbacks activityLifecycleCallbacks; + private FlutterPluginBinding pluginBinding; + private ActivityPluginBinding activityBinding; + private Application application; + private Activity activity; + // This is null when not using v2 embedding; + private Lifecycle lifecycle; + private LifeCycleObserver observer; public static void registerWith(PluginRegistry.Registrar registrar) { if (registrar.activity() == null) { @@ -37,72 +116,114 @@ public static void registerWith(PluginRegistry.Registrar registrar) { // we stop the registering process immediately because the ImagePicker requires an activity. return; } - final ImagePickerCache cache = new ImagePickerCache(registrar.activity()); + Activity activity = registrar.activity(); + Application application = null; + if (registrar.context() != null) { + application = (Application) (registrar.context().getApplicationContext()); + } + ImagePickerPlugin plugin = new ImagePickerPlugin(); + plugin.setup(registrar.messenger(), application, activity, registrar, null); + } - final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL); + /** + * Default constructor for the plugin. + * + *

Use this constructor for production code. + */ + // See also: * {@link #ImagePickerPlugin(ImagePickerDelegate, Activity)} for testing. + public ImagePickerPlugin() {} - final File externalFilesDirectory = - registrar.activity().getExternalFilesDir(Environment.DIRECTORY_PICTURES); - final ExifDataCopier exifDataCopier = new ExifDataCopier(); - final ImageResizer imageResizer = new ImageResizer(externalFilesDirectory, exifDataCopier); - final ImagePickerDelegate delegate = - new ImagePickerDelegate(registrar.activity(), externalFilesDirectory, imageResizer, cache); + @VisibleForTesting + ImagePickerPlugin(final ImagePickerDelegate delegate, final Activity activity) { + this.delegate = delegate; + this.activity = activity; + } - registrar.addActivityResultListener(delegate); - registrar.addRequestPermissionsResultListener(delegate); - final ImagePickerPlugin instance = new ImagePickerPlugin(registrar, delegate); - channel.setMethodCallHandler(instance); + @Override + public void onAttachedToEngine(FlutterPluginBinding binding) { + pluginBinding = binding; } - @VisibleForTesting - ImagePickerPlugin(final PluginRegistry.Registrar registrar, final ImagePickerDelegate delegate) { - this.registrar = registrar; - this.delegate = delegate; - this.activityLifecycleCallbacks = - new Application.ActivityLifecycleCallbacks() { - @Override - public void onActivityCreated(Activity activity, Bundle savedInstanceState) {} + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) { + pluginBinding = null; + } - @Override - public void onActivityStarted(Activity activity) {} + @Override + public void onAttachedToActivity(ActivityPluginBinding binding) { + activityBinding = binding; + setup( + pluginBinding.getBinaryMessenger(), + (Application) pluginBinding.getApplicationContext(), + activityBinding.getActivity(), + null, + activityBinding); + } - @Override - public void onActivityResumed(Activity activity) {} + @Override + public void onDetachedFromActivity() { + tearDown(); + } - @Override - public void onActivityPaused(Activity activity) {} + @Override + public void onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity(); + } - @Override - public void onActivitySaveInstanceState(Activity activity, Bundle outState) { - if (activity == registrar.activity()) { - delegate.saveStateBeforeResult(); - } - } - - @Override - public void onActivityDestroyed(Activity activity) { - if (activity == registrar.activity() - && registrar.activity().getApplicationContext() != null) { - ((Application) registrar.activity().getApplicationContext()) - .unregisterActivityLifecycleCallbacks( - this); // Use getApplicationContext() to avoid casting failures - } - } - - @Override - public void onActivityStopped(Activity activity) {} - }; - - if (this.registrar != null - && this.registrar.context() != null - && this.registrar.context().getApplicationContext() != null) { - ((Application) this.registrar.context().getApplicationContext()) - .registerActivityLifecycleCallbacks( - this - .activityLifecycleCallbacks); // Use getApplicationContext() to avoid casting failures. + @Override + public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { + onAttachedToActivity(binding); + } + + private void setup( + final BinaryMessenger messenger, + final Application application, + final Activity activity, + final PluginRegistry.Registrar registrar, + final ActivityPluginBinding activityBinding) { + this.activity = activity; + this.application = application; + this.delegate = constructDelegate(activity); + channel = new MethodChannel(messenger, CHANNEL); + channel.setMethodCallHandler(this); + observer = new LifeCycleObserver(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); } } + private void tearDown() { + activityBinding.removeActivityResultListener(delegate); + activityBinding.removeRequestPermissionsResultListener(delegate); + activityBinding = null; + lifecycle.removeObserver(observer); + lifecycle = null; + delegate = null; + channel.setMethodCallHandler(null); + channel = null; + application.unregisterActivityLifecycleCallbacks(observer); + application = null; + } + + private final ImagePickerDelegate constructDelegate(final Activity setupActivity) { + final ImagePickerCache cache = new ImagePickerCache(setupActivity); + + final File externalFilesDirectory = + setupActivity.getExternalFilesDir(Environment.DIRECTORY_PICTURES); + final ExifDataCopier exifDataCopier = new ExifDataCopier(); + final ImageResizer imageResizer = new ImageResizer(externalFilesDirectory, exifDataCopier); + return new ImagePickerDelegate(setupActivity, externalFilesDirectory, imageResizer, cache); + } + // MethodChannel.Result wrapper that responds on the platform thread. private static class MethodResultWrapper implements MethodChannel.Result { private MethodChannel.Result methodResult; @@ -150,7 +271,7 @@ public void run() { @Override public void onMethodCall(MethodCall call, MethodChannel.Result rawResult) { - if (registrar.activity() == null) { + if (activity == null) { rawResult.error("no_activity", "image_picker plugin requires a foreground activity.", null); return; } diff --git a/packages/image_picker/example/android/app/src/main/AndroidManifest.xml b/packages/image_picker/example/android/app/src/main/AndroidManifest.xml index fa2b500d6904..5de9f044dd84 100755 --- a/packages/image_picker/example/android/app/src/main/AndroidManifest.xml +++ b/packages/image_picker/example/android/app/src/main/AndroidManifest.xml @@ -4,8 +4,7 @@ - + + diff --git a/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/MainActivity.java b/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1Activity.java similarity index 78% rename from packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/MainActivity.java rename to packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1Activity.java index 4690ebce064a..79c1ca6590cc 100644 --- a/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/MainActivity.java +++ b/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1Activity.java @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2019 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -8,8 +8,7 @@ import io.flutter.app.FlutterActivity; import io.flutter.plugins.GeneratedPluginRegistrant; -public class MainActivity extends FlutterActivity { - +public class EmbeddingV1Activity extends FlutterActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1ActivityTest.java b/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1ActivityTest.java new file mode 100644 index 000000000000..924cf2fdb93c --- /dev/null +++ b/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1ActivityTest.java @@ -0,0 +1,17 @@ +// Copyright 2019 The Chromium 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.imagepickerexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.e2e.FlutterRunner; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterRunner.class) +public class EmbeddingV1ActivityTest { + @Rule + public ActivityTestRule rule = + new ActivityTestRule<>(EmbeddingV1Activity.class); +} diff --git a/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java b/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java new file mode 100644 index 000000000000..df9794c8748f --- /dev/null +++ b/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java @@ -0,0 +1,13 @@ +package io.flutter.plugins.imagepickerexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.e2e.FlutterRunner; +import io.flutter.embedding.android.FlutterActivity; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java index 94a81d3d3eac..acfd0648e763 100644 --- a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java +++ b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java @@ -40,16 +40,15 @@ public void setUp() { MockitoAnnotations.initMocks(this); when(mockRegistrar.context()).thenReturn(mockApplication); - plugin = new ImagePickerPlugin(mockRegistrar, mockImagePickerDelegate); + plugin = new ImagePickerPlugin(mockImagePickerDelegate, mockActivity); } @Test public void onMethodCall_WhenActivityIsNull_FinishesWithForegroundActivityRequiredError() { - when(mockRegistrar.activity()).thenReturn(null); MethodCall call = buildMethodCall(SOURCE_GALLERY); - - plugin.onMethodCall(call, mockResult); - + ImagePickerPlugin imagePickerPluginWithNullActivity = + new ImagePickerPlugin(mockImagePickerDelegate, null); + imagePickerPluginWithNullActivity.onMethodCall(call, mockResult); verify(mockResult) .error("no_activity", "image_picker plugin requires a foreground activity.", null); verifyZeroInteractions(mockImagePickerDelegate); @@ -57,46 +56,34 @@ public void onMethodCall_WhenActivityIsNull_FinishesWithForegroundActivityRequir @Test public void onMethodCall_WhenCalledWithUnknownMethod_ThrowsException() { - when(mockRegistrar.activity()).thenReturn(mockActivity); exception.expect(IllegalArgumentException.class); exception.expectMessage("Unknown method test"); - plugin.onMethodCall(new MethodCall("test", null), mockResult); - verifyZeroInteractions(mockImagePickerDelegate); verifyZeroInteractions(mockResult); } @Test public void onMethodCall_WhenCalledWithUnknownImageSource_ThrowsException() { - when(mockRegistrar.activity()).thenReturn(mockActivity); exception.expect(IllegalArgumentException.class); exception.expectMessage("Invalid image source: -1"); - plugin.onMethodCall(buildMethodCall(-1), mockResult); - verifyZeroInteractions(mockImagePickerDelegate); verifyZeroInteractions(mockResult); } @Test public void onMethodCall_WhenSourceIsGallery_InvokesChooseImageFromGallery() { - when(mockRegistrar.activity()).thenReturn(mockActivity); MethodCall call = buildMethodCall(SOURCE_GALLERY); - plugin.onMethodCall(call, mockResult); - verify(mockImagePickerDelegate).chooseImageFromGallery(eq(call), any()); verifyZeroInteractions(mockResult); } @Test public void onMethodCall_WhenSourceIsCamera_InvokesTakeImageWithCamera() { - when(mockRegistrar.activity()).thenReturn(mockActivity); MethodCall call = buildMethodCall(SOURCE_CAMERA); - plugin.onMethodCall(call, mockResult); - verify(mockImagePickerDelegate).takeImageWithCamera(eq(call), any()); verifyZeroInteractions(mockResult); } @@ -111,8 +98,7 @@ public void onResiter_WhenAcitivityIsNull_ShouldNotCrash() { @Test public void onConstructor_WhenContextTypeIsActivity_ShouldNotCrash() { - when(mockRegistrar.context()).thenReturn(mockActivity); - new ImagePickerPlugin(mockRegistrar, mockImagePickerDelegate); + new ImagePickerPlugin(mockImagePickerDelegate, mockActivity); assertTrue( "No exception thrown when ImagePickerPlugin() ran with context instanceof Activity", true); } diff --git a/packages/image_picker/example/lib/main.dart b/packages/image_picker/example/lib/main.dart index 3ece64b13d65..919c8388266f 100755 --- a/packages/image_picker/example/lib/main.dart +++ b/packages/image_picker/example/lib/main.dart @@ -305,13 +305,13 @@ class _MyHomePageState extends State { FlatButton( child: const Text('PICK'), onPressed: () { - double width = maxWidthController.text.length > 0 + double width = maxWidthController.text.isNotEmpty ? double.parse(maxWidthController.text) : null; - double height = maxHeightController.text.length > 0 + double height = maxHeightController.text.isNotEmpty ? double.parse(maxHeightController.text) : null; - int quality = qualityController.text.length > 0 + int quality = qualityController.text.isNotEmpty ? int.parse(qualityController.text) : null; onPick(width, height, quality); diff --git a/packages/image_picker/example/pubspec.yaml b/packages/image_picker/example/pubspec.yaml index 1f793d7aa91f..b84ee9fc08a4 100755 --- a/packages/image_picker/example/pubspec.yaml +++ b/packages/image_picker/example/pubspec.yaml @@ -3,12 +3,22 @@ description: Demonstrates how to use the image_picker plugin. author: Flutter Team dependencies: - video_player: 0.10.1+5 + video_player: ^0.10.3 flutter: sdk: flutter image_picker: path: ../ + flutter_plugin_android_lifecycle: ^1.0.2 + +dev_dependencies: + flutter_driver: + sdk: flutter + e2e: ^0.2.1 flutter: uses-material-design: true +environment: + sdk: ">=2.0.0-dev.28.0 <3.0.0" + flutter: ">=1.10.0 <2.0.0" + diff --git a/packages/image_picker/example/test_driver/test/image_picker_e2e_test.dart b/packages/image_picker/example/test_driver/test/image_picker_e2e_test.dart new file mode 100644 index 000000000000..f3aa9e218d82 --- /dev/null +++ b/packages/image_picker/example/test_driver/test/image_picker_e2e_test.dart @@ -0,0 +1,15 @@ +// Copyright 2019, the Chromium project authors. Please see the AUTHORS file +// for details. 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 'dart:io'; +import 'package:flutter_driver/flutter_driver.dart'; + +Future main() async { + final FlutterDriver driver = await FlutterDriver.connect(); + final String result = + await driver.requestData(null, timeout: const Duration(minutes: 1)); + await driver.close(); + exit(result == 'pass' ? 0 : 1); +} diff --git a/packages/image_picker/pubspec.yaml b/packages/image_picker/pubspec.yaml index 217764c0f21a..5b88c7187bb8 100755 --- a/packages/image_picker/pubspec.yaml +++ b/packages/image_picker/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker -version: 0.6.2+3 +version: 0.6.3 flutter: plugin: @@ -16,11 +16,13 @@ flutter: dependencies: flutter: sdk: flutter + flutter_plugin_android_lifecycle: ^1.0.2 dev_dependencies: - video_player: 0.10.1+5 + video_player: ^0.10.3 flutter_test: sdk: flutter + e2e: ^0.2.1 environment: sdk: ">=2.0.0-dev.28.0 <3.0.0" diff --git a/packages/image_picker/test/image_picker_e2e.dart b/packages/image_picker/test/image_picker_e2e.dart new file mode 100644 index 000000000000..b19e37dd6541 --- /dev/null +++ b/packages/image_picker/test/image_picker_e2e.dart @@ -0,0 +1,5 @@ +import 'package:e2e/e2e.dart'; + +void main() { + E2EWidgetsFlutterBinding.ensureInitialized(); +}