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"