diff --git a/packages/quick_actions/CHANGELOG.md b/packages/quick_actions/CHANGELOG.md index 0069c0c5165e..c5d442932f6d 100644 --- a/packages/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.3.3 + +* Support Android V2 embedding. +* Add e2e tests. +* Migrate to using the new e2e test binding. + ## 0.3.2+4 * Remove AndroidX warnings. @@ -7,6 +13,7 @@ * Define clang module for iOS. ## 0.3.2+2 + * Fix bug that would make the shortcut not open on Android. * Report shortcut used on Android. * Improves example. @@ -17,7 +24,7 @@ ## 0.3.2 -* Fixed the quick actions launch on Android when the app is killed. +* Fixed the quick actions launch on Android when the app is killed. ## 0.3.1 diff --git a/packages/quick_actions/android/build.gradle b/packages/quick_actions/android/build.gradle index 648b654dbfcd..d474a71c7447 100644 --- a/packages/quick_actions/android/build.gradle +++ b/packages/quick_actions/android/build.gradle @@ -32,3 +32,28 @@ android { disable 'InvalidPackage' } } + +afterEvaluate { + def containsEmbeddingDependencies = false + for (def configuration : configurations.all) { + for (def dependency : configuration.dependencies) { + if (dependency.group == 'io.flutter' && + dependency.name.startsWith('flutter_embedding') && + dependency.isTransitive()) + { + containsEmbeddingDependencies = true + break + } + } + } + if (!containsEmbeddingDependencies) { + android { + dependencies { + def lifecycle_version = "1.1.1" + compileOnly "android.arch.lifecycle:runtime:$lifecycle_version" + compileOnly "android.arch.lifecycle:common:$lifecycle_version" + compileOnly "android.arch.lifecycle:common-java8:$lifecycle_version" + } + } + } +} diff --git a/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java b/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java new file mode 100644 index 000000000000..dcf2390570bd --- /dev/null +++ b/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java @@ -0,0 +1,131 @@ +// 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.quickactions; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.content.res.Resources; +import android.graphics.drawable.Icon; +import android.os.Build; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { + + private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions"; + private static final String EXTRA_ACTION = "some unique action key"; + + private final Context context; + private Activity activity; + + MethodCallHandlerImpl(Context context, Activity activity) { + this.context = context; + this.activity = activity; + } + + void setActivity(Activity activity) { + this.activity = activity; + } + + @Override + public void onMethodCall(MethodCall call, MethodChannel.Result result) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { + // We already know that this functionality does not work for anything + // lower than API 25 so we chose not to return error. Instead we do nothing. + result.success(null); + return; + } + ShortcutManager shortcutManager = + (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); + switch (call.method) { + case "setShortcutItems": + List> serializedShortcuts = call.arguments(); + List shortcuts = deserializeShortcuts(serializedShortcuts); + shortcutManager.setDynamicShortcuts(shortcuts); + break; + case "clearShortcutItems": + shortcutManager.removeAllDynamicShortcuts(); + break; + case "getLaunchAction": + if (activity == null) { + result.error( + "quick_action_getlaunchaction_no_activity", + "There is no activity available when launching action", + null); + return; + } + final Intent intent = activity.getIntent(); + final String launchAction = intent.getStringExtra(EXTRA_ACTION); + if (launchAction != null && !launchAction.isEmpty()) { + shortcutManager.reportShortcutUsed(launchAction); + intent.removeExtra(EXTRA_ACTION); + } + result.success(launchAction); + return; + default: + result.notImplemented(); + return; + } + result.success(null); + } + + @TargetApi(Build.VERSION_CODES.N_MR1) + private List deserializeShortcuts(List> shortcuts) { + final List shortcutInfos = new ArrayList<>(); + + for (Map shortcut : shortcuts) { + final String icon = shortcut.get("icon"); + final String type = shortcut.get("type"); + final String title = shortcut.get("localizedTitle"); + final ShortcutInfo.Builder shortcutBuilder = new ShortcutInfo.Builder(context, type); + + final int resourceId = loadResourceId(context, icon); + final Intent intent = getIntentToOpenMainActivity(type); + + if (resourceId > 0) { + shortcutBuilder.setIcon(Icon.createWithResource(context, resourceId)); + } + + final ShortcutInfo shortcutInfo = + shortcutBuilder.setLongLabel(title).setShortLabel(title).setIntent(intent).build(); + shortcutInfos.add(shortcutInfo); + } + return shortcutInfos; + } + + private int loadResourceId(Context context, String icon) { + if (icon == null) { + return 0; + } + final String packageName = context.getPackageName(); + final Resources res = context.getResources(); + final int resourceId = res.getIdentifier(icon, "drawable", packageName); + + if (resourceId == 0) { + return res.getIdentifier(icon, "mipmap", packageName); + } else { + return resourceId; + } + } + + private Intent getIntentToOpenMainActivity(String type) { + final String packageName = context.getPackageName(); + + return context + .getPackageManager() + .getLaunchIntentForPackage(packageName) + .setAction(Intent.ACTION_RUN) + .putExtra(EXTRA_ACTION, type) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + } +} diff --git a/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java b/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java index 3a4ba2410666..76285fb90d10 100644 --- a/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java +++ b/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java @@ -4,33 +4,21 @@ package io.flutter.plugins.quickactions; -import android.annotation.TargetApi; +import android.app.Activity; import android.content.Context; -import android.content.Intent; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager; -import android.content.res.Resources; -import android.graphics.drawable.Icon; -import android.os.Build; -import io.flutter.plugin.common.MethodCall; +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.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.PluginRegistry.Registrar; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; /** QuickActionsPlugin */ -public class QuickActionsPlugin implements MethodCallHandler { +public class QuickActionsPlugin implements FlutterPlugin, ActivityAware { private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions"; - private static final String EXTRA_ACTION = "some unique action key"; - private final Registrar registrar; - - private QuickActionsPlugin(Registrar registrar) { - this.registrar = registrar; - } + private MethodChannel channel; + private MethodCallHandlerImpl handler; /** * Plugin registration. @@ -38,96 +26,50 @@ private QuickActionsPlugin(Registrar registrar) { *

Must be called when the application is created. */ public static void registerWith(Registrar registrar) { - final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL_ID); - channel.setMethodCallHandler(new QuickActionsPlugin(registrar)); + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + plugin.setupChannel(registrar.messenger(), registrar.context(), registrar.activity()); } @Override - public void onMethodCall(MethodCall call, Result result) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { - // We already know that this functionality does not work for anything - // lower than API 25 so we chose not to return error. Instead we do nothing. - result.success(null); - return; - } - Context context = registrar.context(); - ShortcutManager shortcutManager = - (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); - switch (call.method) { - case "setShortcutItems": - List> serializedShortcuts = call.arguments(); - List shortcuts = deserializeShortcuts(serializedShortcuts); - shortcutManager.setDynamicShortcuts(shortcuts); - break; - case "clearShortcutItems": - shortcutManager.removeAllDynamicShortcuts(); - break; - case "getLaunchAction": - final Intent intent = registrar.activity().getIntent(); - final String launchAction = intent.getStringExtra(EXTRA_ACTION); - if (launchAction != null && !launchAction.isEmpty()) { - shortcutManager.reportShortcutUsed(launchAction); - intent.removeExtra(EXTRA_ACTION); - } - result.success(launchAction); - return; - default: - result.notImplemented(); - return; - } - result.success(null); + public void onAttachedToEngine(FlutterPluginBinding binding) { + setupChannel( + binding.getFlutterEngine().getDartExecutor(), binding.getApplicationContext(), null); } - @TargetApi(Build.VERSION_CODES.N_MR1) - private List deserializeShortcuts(List> shortcuts) { - final List shortcutInfos = new ArrayList<>(); - final Context context = registrar.context(); - - for (Map shortcut : shortcuts) { - final String icon = shortcut.get("icon"); - final String type = shortcut.get("type"); - final String title = shortcut.get("localizedTitle"); - final ShortcutInfo.Builder shortcutBuilder = new ShortcutInfo.Builder(context, type); - - final int resourceId = loadResourceId(context, icon); - final Intent intent = getIntentToOpenMainActivity(type); + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) { + teardownChannel(); + } - if (resourceId > 0) { - shortcutBuilder.setIcon(Icon.createWithResource(context, resourceId)); - } + @Override + public void onAttachedToActivity(ActivityPluginBinding binding) { + handler.setActivity(binding.getActivity()); + } - final ShortcutInfo shortcutInfo = - shortcutBuilder.setLongLabel(title).setShortLabel(title).setIntent(intent).build(); - shortcutInfos.add(shortcutInfo); - } - return shortcutInfos; + @Override + public void onDetachedFromActivity() { + handler.setActivity(null); } - private int loadResourceId(Context context, String icon) { - if (icon == null) { - return 0; - } - final String packageName = context.getPackageName(); - final Resources res = context.getResources(); - final int resourceId = res.getIdentifier(icon, "drawable", packageName); + @Override + public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { + onAttachedToActivity(binding); + } - if (resourceId == 0) { - return res.getIdentifier(icon, "mipmap", packageName); - } else { - return resourceId; - } + @Override + public void onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity(); } - private Intent getIntentToOpenMainActivity(String type) { - final Context context = registrar.context(); - final String packageName = context.getPackageName(); + private void setupChannel(BinaryMessenger messenger, Context context, Activity activity) { + channel = new MethodChannel(messenger, CHANNEL_ID); + handler = new MethodCallHandlerImpl(context, activity); + channel.setMethodCallHandler(handler); + } - return context - .getPackageManager() - .getLaunchIntentForPackage(packageName) - .setAction(Intent.ACTION_RUN) - .putExtra(EXTRA_ACTION, type) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + private void teardownChannel() { + channel.setMethodCallHandler(null); + channel = null; + handler = null; } } diff --git a/packages/quick_actions/example/android/app/src/main/AndroidManifest.xml b/packages/quick_actions/example/android/app/src/main/AndroidManifest.xml index bb7a1351d343..6c70f1b650cd 100644 --- a/packages/quick_actions/example/android/app/src/main/AndroidManifest.xml +++ b/packages/quick_actions/example/android/app/src/main/AndroidManifest.xml @@ -6,15 +6,22 @@ - + android:windowSoftInputMode="adjustResize" + android:exported="true"> + + diff --git a/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1Activity.java b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1Activity.java new file mode 100644 index 000000000000..ba6a64d997bc --- /dev/null +++ b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1Activity.java @@ -0,0 +1,17 @@ +// Copyright 2017 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.quickactionsexample; + +import android.os.Bundle; +import io.flutter.app.FlutterActivity; +import io.flutter.plugins.GeneratedPluginRegistrant; + +public class EmbeddingV1Activity extends FlutterActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + GeneratedPluginRegistrant.registerWith(this); + } +} diff --git a/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1ActivityTest.java b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1ActivityTest.java new file mode 100644 index 000000000000..b3e2a08c44b4 --- /dev/null +++ b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/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.quickactionsexample; + +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/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/MainActivity.java b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/MainActivity.java index 74e3eb873cbc..ce128b60648e 100644 --- a/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/MainActivity.java +++ b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/MainActivity.java @@ -1,17 +1,20 @@ -// 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. package io.flutter.plugins.quickactionsexample; -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugins.quickactions.QuickActionsPlugin; public class MainActivity extends FlutterActivity { + + // TODO(cyanglaz): Remove this once v2 of GeneratedPluginRegistrant rolls to stable. + // https://github.com/flutter/flutter/issues/42694 @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); + public void configureFlutterEngine(FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + flutterEngine.getPlugins().add(new QuickActionsPlugin()); } } diff --git a/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/MainActivityTest.java b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/MainActivityTest.java new file mode 100644 index 000000000000..4d3e1822d616 --- /dev/null +++ b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/MainActivityTest.java @@ -0,0 +1,15 @@ +// 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.quickactionsexample; + +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 MainActivityTest { + @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); +} diff --git a/packages/quick_actions/example/android/gradle.properties b/packages/quick_actions/example/android/gradle.properties index 8bd86f680510..38c8d4544ff1 100644 --- a/packages/quick_actions/example/android/gradle.properties +++ b/packages/quick_actions/example/android/gradle.properties @@ -1 +1,4 @@ org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/quick_actions/example/pubspec.yaml b/packages/quick_actions/example/pubspec.yaml index b5fe2e1a8545..6cbacb5b84d2 100644 --- a/packages/quick_actions/example/pubspec.yaml +++ b/packages/quick_actions/example/pubspec.yaml @@ -7,5 +7,14 @@ dependencies: quick_actions: path: ../ +dev_dependencies: + flutter_driver: + sdk: flutter + e2e: ^0.2.0 + flutter: uses-material-design: true + +environment: + sdk: ">=2.0.0-dev.28.0 <3.0.0" + flutter: ">=1.9.1+hotfix.2 <2.0.0" diff --git a/packages/quick_actions/example/test_driver/quick_actions_e2e.dart b/packages/quick_actions/example/test_driver/quick_actions_e2e.dart new file mode 100644 index 000000000000..41d35b874640 --- /dev/null +++ b/packages/quick_actions/example/test_driver/quick_actions_e2e.dart @@ -0,0 +1,24 @@ +// 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 'package:flutter_test/flutter_test.dart'; +import 'package:e2e/e2e.dart'; +import 'package:quick_actions/quick_actions.dart'; + +void main() { + E2EWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Can set shortcuts', (WidgetTester tester) async { + final QuickActions quickActions = QuickActions(); + quickActions.initialize(null); + + const ShortcutItem shortCutItem = ShortcutItem( + type: 'action_one', + localizedTitle: 'Action one', + icon: 'AppIcon', + ); + expect( + quickActions.setShortcutItems([shortCutItem]), completes); + }); +} diff --git a/packages/quick_actions/example/test_driver/quick_actions_e2e_test.dart b/packages/quick_actions/example/test_driver/quick_actions_e2e_test.dart new file mode 100644 index 000000000000..ff6e9ce74ad9 --- /dev/null +++ b/packages/quick_actions/example/test_driver/quick_actions_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)); + driver.close(); + exit(result == 'pass' ? 0 : 1); +} diff --git a/packages/quick_actions/pubspec.yaml b/packages/quick_actions/pubspec.yaml index da725aa55790..c11596b82f47 100644 --- a/packages/quick_actions/pubspec.yaml +++ b/packages/quick_actions/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for creating shortcuts on home screen, also known as Quick Actions on iOS and App Shortcuts on Android. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/quick_actions -version: 0.3.2+4 +version: 0.3.3 flutter: plugin: @@ -21,7 +21,8 @@ dev_dependencies: mockito: ^3.0.0 flutter_test: sdk: flutter + e2e: ^0.2.0 environment: sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.5.0 <2.0.0" + flutter: ">=1.6.7 <2.0.0"