[quick_actions] Android handle quick action without restart (#5048)

diff --git a/packages/quick_actions/quick_actions_android/CHANGELOG.md b/packages/quick_actions/quick_actions_android/CHANGELOG.md
index 35f9c9f..5b5f709 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 5b02f6d..5ec81f0 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 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    package="io.flutter.plugins.quickactions">
+        xmlns:tools="http://schemas.android.com/tools"
+        package="io.flutter.plugins.quickactions">
+
+    <uses-sdk tools:overrideLibrary="android_libs.ub_uiautomator"/>
 </manifest>
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 6316e84..96b141f 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 @@
 
                 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 @@
         .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 99ce0f8..b410878 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 @@
 
   private MethodChannel channel;
   private MethodCallHandlerImpl handler;
+  private Activity activity;
 
   /**
    * Plugin registration.
@@ -45,9 +47,10 @@
 
   @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 @@
     }
     // 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 d2e63b6..dc4b36e 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 @@
     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 @@
     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 75fe354..75920e0 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 @@
 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 @@
     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 9d2fed1..8b50fd7 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<QuickActionsTestActivity> 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<QuickActionsTestActivity> scenario =
         ActivityScenario.launch(QuickActionsTestActivity.class);
     scenario.onActivity(
@@ -20,4 +60,95 @@
           assertTrue(activity.engine.getPlugins().has(QuickActionsPlugin.class));
         });
   }
+
+  @Test
+  public void appShortcutsAreCreated() {
+    List<ShortcutInfo> expectedShortcuts = createMockShortcuts();
+
+    ShortcutManager shortcutManager =
+        (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
+    List<ShortcutInfo> 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<ShortcutInfo> shortcuts = createMockShortcuts();
+    ShortcutInfo firstShortcut = shortcuts.get(0);
+    ShortcutManager shortcutManager =
+        (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
+    List<ShortcutInfo> dynamicShortcuts = shortcutManager.getDynamicShortcuts();
+    ShortcutInfo dynamicShortcut =
+        dynamicShortcuts
+            .stream()
+            .filter(s -> s.getId().equals(firstShortcut.getId()))
+            .findFirst()
+            .get();
+    Intent dynamicShortcutIntent = dynamicShortcut.getIntent();
+    AtomicReference<QuickActionsTestActivity> initialActivity = new AtomicReference<>();
+    scenario.onActivity(initialActivity::set);
+    String appReadySentinel = " has launched";
+
+    // Act
+    context.startActivity(dynamicShortcutIntent);
+    device.wait(Until.hasObject(By.descContains(appReadySentinel)), 2000);
+    AtomicReference<QuickActionsTestActivity> 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<ShortcutInfo> createMockShortcuts() {
+    List<ShortcutInfo> 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<QuickActionsTestActivity> ensureAppRunToView() {
+    final ActivityScenario<QuickActionsTestActivity> 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 f9c42ad..e0abe90 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>[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 d8b7832..8f66e69 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 @@
     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 fa39e36..7cb2741 100644
--- a/packages/quick_actions/quick_actions_android/pubspec.yaml
+++ b/packages/quick_actions/quick_actions_android/pubspec.yaml
@@ -2,7 +2,7 @@
 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"