diff --git a/packages/quick_actions/quick_actions_android/CHANGELOG.md b/packages/quick_actions/quick_actions_android/CHANGELOG.md index 35f9c9fd51d9..5b5f70946e91 100644 --- a/packages/quick_actions/quick_actions_android/CHANGELOG.md +++ b/packages/quick_actions/quick_actions_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.1 + +* Allows Android to trigger quick actions without restarting the app. + ## 0.6.0+11 * Updates references to the obsolete master branch. diff --git a/packages/quick_actions/quick_actions_android/android/src/main/AndroidManifest.xml b/packages/quick_actions/quick_actions_android/android/src/main/AndroidManifest.xml index 5b02f6d8aef2..5ec81f08ec6a 100644 --- a/packages/quick_actions/quick_actions_android/android/src/main/AndroidManifest.xml +++ b/packages/quick_actions/quick_actions_android/android/src/main/AndroidManifest.xml @@ -1,4 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools" + package="io.flutter.plugins.quickactions"> + + diff --git a/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java index 6316e8428288..96b141fb9c31 100644 --- a/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java +++ b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java @@ -74,7 +74,8 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { final boolean didSucceed = dynamicShortcutsSet; - // TODO(camsim99): Move re-dispatch below to background thread when Flutter 2.8+ is stable. + // TODO(camsim99): Move re-dispatch below to background thread when Flutter 2.8+ is + // stable. uiThreadExecutor.execute( () -> { if (didSucceed) { @@ -163,7 +164,7 @@ private Intent getIntentToOpenMainActivity(String type) { .setAction(Intent.ACTION_RUN) .putExtra(EXTRA_ACTION, type) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); } private static class UiThreadExecutor implements Executor { diff --git a/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java index 99ce0f8426a0..b41087816889 100644 --- a/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java +++ b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java @@ -7,6 +7,7 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.content.pm.ShortcutManager; import android.os.Build; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; @@ -21,6 +22,7 @@ public class QuickActionsPlugin implements FlutterPlugin, ActivityAware, NewInte private MethodChannel channel; private MethodCallHandlerImpl handler; + private Activity activity; /** * Plugin registration. @@ -45,9 +47,10 @@ public void onDetachedFromEngine(FlutterPluginBinding binding) { @Override public void onAttachedToActivity(ActivityPluginBinding binding) { - handler.setActivity(binding.getActivity()); + activity = binding.getActivity(); + handler.setActivity(activity); binding.addOnNewIntentListener(this); - onNewIntent(binding.getActivity().getIntent()); + onNewIntent(activity.getIntent()); } @Override @@ -74,7 +77,12 @@ public boolean onNewIntent(Intent intent) { } // Notify the Dart side if the launch intent has the intent extra relevant to quick actions. if (intent.hasExtra(MethodCallHandlerImpl.EXTRA_ACTION) && channel != null) { - channel.invokeMethod("launch", intent.getStringExtra(MethodCallHandlerImpl.EXTRA_ACTION)); + Context context = activity.getApplicationContext(); + ShortcutManager shortcutManager = + (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); + String shortcutId = intent.getStringExtra(MethodCallHandlerImpl.EXTRA_ACTION); + channel.invokeMethod("launch", shortcutId); + shortcutManager.reportShortcutUsed(shortcutId); } return false; } diff --git a/packages/quick_actions/quick_actions_android/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java b/packages/quick_actions/quick_actions_android/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java index d2e63b62f229..dc4b36e168db 100644 --- a/packages/quick_actions/quick_actions_android/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java +++ b/packages/quick_actions/quick_actions_android/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java @@ -13,7 +13,9 @@ import static org.mockito.Mockito.when; import android.app.Activity; +import android.content.Context; import android.content.Intent; +import android.content.pm.ShortcutManager; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -86,6 +88,11 @@ public void onAttachedToActivity_buildVersionSupported_invokesLaunchMethod() when(mockMainActivity.getIntent()).thenReturn(mockIntent); final ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class); when(mockActivityPluginBinding.getActivity()).thenReturn(mockMainActivity); + final Context mockContext = mock(Context.class); + when(mockMainActivity.getApplicationContext()).thenReturn(mockContext); + final ShortcutManager mockShortcutManager = mock(ShortcutManager.class); + when(mockContext.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(mockShortcutManager); + plugin.onAttachedToActivity(mockActivityPluginBinding); // Act plugin.onAttachedToActivity(mockActivityPluginBinding); @@ -123,6 +130,15 @@ public void onNewIntent_buildVersionSupported_invokesLaunchMethod() setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin); setBuildVersion(SUPPORTED_BUILD); final Intent mockIntent = createMockIntentWithQuickActionExtra(); + final Activity mockMainActivity = mock(Activity.class); + when(mockMainActivity.getIntent()).thenReturn(mockIntent); + final ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class); + when(mockActivityPluginBinding.getActivity()).thenReturn(mockMainActivity); + final Context mockContext = mock(Context.class); + when(mockMainActivity.getApplicationContext()).thenReturn(mockContext); + final ShortcutManager mockShortcutManager = mock(ShortcutManager.class); + when(mockContext.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(mockShortcutManager); + plugin.onAttachedToActivity(mockActivityPluginBinding); // Act final boolean onNewIntentReturn = plugin.onNewIntent(mockIntent); diff --git a/packages/quick_actions/quick_actions_android/example/android/app/build.gradle b/packages/quick_actions/quick_actions_android/example/android/app/build.gradle index 75fe3543e987..75920e00fcab 100644 --- a/packages/quick_actions/quick_actions_android/example/android/app/build.gradle +++ b/packages/quick_actions/quick_actions_android/example/android/app/build.gradle @@ -24,6 +24,8 @@ if (flutterVersionName == null) { apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +def androidXTestVersion = '1.2.0' + android { compileSdkVersion 31 @@ -55,5 +57,11 @@ dependencies { testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - api 'androidx.test:core:1.2.0' + api "androidx.test:core:$androidXTestVersion" + + androidTestImplementation "androidx.test:runner:$androidXTestVersion" + androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' + androidTestImplementation 'androidx.test.ext:junit:1.0.0' + androidTestImplementation 'org.mockito:mockito-core:4.3.1' + androidTestImplementation 'org.mockito:mockito-android:4.3.1' } diff --git a/packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java b/packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java index 9d2fed13fc27..8b50fd7a90eb 100644 --- a/packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java +++ b/packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java @@ -4,15 +4,55 @@ package io.flutter.plugins.quickactionsexample; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.util.Log; +import androidx.lifecycle.Lifecycle; import androidx.test.core.app.ActivityScenario; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.Until; import io.flutter.plugins.quickactions.QuickActionsPlugin; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +@RunWith(AndroidJUnit4.class) public class QuickActionsTest { + private Context context; + private UiDevice device; + private ActivityScenario scenario; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + scenario = ensureAppRunToView(); + ensureAllAppShortcutsAreCreated(); + } + + @After + public void tearDown() { + scenario.close(); + Log.i(QuickActionsTest.class.getSimpleName(), "Run to completion"); + } + @Test - public void imagePickerPluginIsAdded() { + public void quickActionPluginIsAdded() { final ActivityScenario scenario = ActivityScenario.launch(QuickActionsTestActivity.class); scenario.onActivity( @@ -20,4 +60,95 @@ public void imagePickerPluginIsAdded() { assertTrue(activity.engine.getPlugins().has(QuickActionsPlugin.class)); }); } + + @Test + public void appShortcutsAreCreated() { + List expectedShortcuts = createMockShortcuts(); + + ShortcutManager shortcutManager = + (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); + List dynamicShortcuts = shortcutManager.getDynamicShortcuts(); + + // Assert the app shortcuts defined in ../lib/main.dart. + assertFalse(dynamicShortcuts.isEmpty()); + assertEquals(expectedShortcuts.size(), dynamicShortcuts.size()); + for (ShortcutInfo expectedShortcut : expectedShortcuts) { + ShortcutInfo dynamicShortcut = + dynamicShortcuts + .stream() + .filter(s -> s.getId().equals(expectedShortcut.getId())) + .findFirst() + .get(); + + assertEquals(expectedShortcut.getShortLabel(), dynamicShortcut.getShortLabel()); + assertEquals(expectedShortcut.getLongLabel(), dynamicShortcut.getLongLabel()); + } + } + + @Test + public void appShortcutLaunchActivityAfterStarting() { + // Arrange + List shortcuts = createMockShortcuts(); + ShortcutInfo firstShortcut = shortcuts.get(0); + ShortcutManager shortcutManager = + (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); + List dynamicShortcuts = shortcutManager.getDynamicShortcuts(); + ShortcutInfo dynamicShortcut = + dynamicShortcuts + .stream() + .filter(s -> s.getId().equals(firstShortcut.getId())) + .findFirst() + .get(); + Intent dynamicShortcutIntent = dynamicShortcut.getIntent(); + AtomicReference initialActivity = new AtomicReference<>(); + scenario.onActivity(initialActivity::set); + String appReadySentinel = " has launched"; + + // Act + context.startActivity(dynamicShortcutIntent); + device.wait(Until.hasObject(By.descContains(appReadySentinel)), 2000); + AtomicReference currentActivity = new AtomicReference<>(); + scenario.onActivity(currentActivity::set); + + // Assert + Assert.assertTrue( + "AppShortcut:" + firstShortcut.getId() + " does not launch the correct activity", + // We can only find the shortcut type in content description while inspecting it in Ui + // Automator Viewer. + device.hasObject(By.desc(firstShortcut.getId() + appReadySentinel))); + // This is Android SingleTop behavior in which Android does not destroy the initial activity and + // launch a new activity. + Assert.assertEquals(initialActivity.get(), currentActivity.get()); + } + + private void ensureAllAppShortcutsAreCreated() { + device.wait(Until.hasObject(By.text("actions ready")), 1000); + } + + private List createMockShortcuts() { + List expectedShortcuts = new ArrayList<>(); + + String actionOneLocalizedTitle = "Action one"; + expectedShortcuts.add( + new ShortcutInfo.Builder(context, "action_one") + .setShortLabel(actionOneLocalizedTitle) + .setLongLabel(actionOneLocalizedTitle) + .build()); + + String actionTwoLocalizedTitle = "Action two"; + expectedShortcuts.add( + new ShortcutInfo.Builder(context, "action_two") + .setShortLabel(actionTwoLocalizedTitle) + .setLongLabel(actionTwoLocalizedTitle) + .build()); + + return expectedShortcuts; + } + + private ActivityScenario ensureAppRunToView() { + final ActivityScenario scenario = + ActivityScenario.launch(QuickActionsTestActivity.class); + scenario.moveToState(Lifecycle.State.STARTED); + return scenario; + } } diff --git a/packages/quick_actions/quick_actions_android/example/integration_test/quick_actions_test.dart b/packages/quick_actions/quick_actions_android/example/integration_test/quick_actions_test.dart index f9c42ad109e7..e0abe90f75aa 100644 --- a/packages/quick_actions/quick_actions_android/example/integration_test/quick_actions_test.dart +++ b/packages/quick_actions/quick_actions_android/example/integration_test/quick_actions_test.dart @@ -2,23 +2,21 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:quick_actions_platform_interface/quick_actions_platform_interface.dart'; +import 'package:quick_actions_example/main.dart' as app; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Can set shortcuts', (WidgetTester tester) async { - final QuickActionsPlatform quickActions = QuickActionsPlatform.instance; - await quickActions.initialize((String value) {}); + testWidgets('Can run MyApp', (WidgetTester tester) async { + app.main(); - const ShortcutItem shortCutItem = ShortcutItem( - type: 'action_one', - localizedTitle: 'Action one', - icon: 'AppIcon', - ); - expect( - quickActions.setShortcutItems([shortCutItem]), completes); + await tester.pumpAndSettle(); + await tester.pump(const Duration(seconds: 1)); + + expect(find.byType(Text), findsWidgets); + expect(find.byType(app.MyHomePage), findsOneWidget); }); } diff --git a/packages/quick_actions/quick_actions_android/example/lib/main.dart b/packages/quick_actions/quick_actions_android/example/lib/main.dart index d8b7832bf9dc..8f66e69ffb4e 100644 --- a/packages/quick_actions/quick_actions_android/example/lib/main.dart +++ b/packages/quick_actions/quick_actions_android/example/lib/main.dart @@ -44,7 +44,7 @@ class _MyHomePageState extends State { quickActions.initialize((String shortcutType) { setState(() { if (shortcutType != null) { - shortcut = shortcutType; + shortcut = '$shortcutType has launched'; } }); }); diff --git a/packages/quick_actions/quick_actions_android/pubspec.yaml b/packages/quick_actions/quick_actions_android/pubspec.yaml index fa39e36b4fab..7cb274116536 100644 --- a/packages/quick_actions/quick_actions_android/pubspec.yaml +++ b/packages/quick_actions/quick_actions_android/pubspec.yaml @@ -2,7 +2,7 @@ name: quick_actions_android description: An implementation for the Android platform of the Flutter `quick_actions` plugin. repository: https://github.com/flutter/plugins/tree/main/packages/quick_actions/quick_actions_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.6.0+11 +version: 0.6.1 environment: sdk: ">=2.15.0 <3.0.0"