Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 68cdc58

Browse files
authored
[quick_actions] Android handle quick action without restart (#5048)
1 parent 50a2533 commit 68cdc58

File tree

10 files changed

+190
-22
lines changed

10 files changed

+190
-22
lines changed

packages/quick_actions/quick_actions_android/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.6.1
2+
3+
* Allows Android to trigger quick actions without restarting the app.
4+
15
## 0.6.0+11
26

37
* Updates references to the obsolete master branch.
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2-
xmlns:tools="http://schemas.android.com/tools"
3-
package="io.flutter.plugins.quickactions">
2+
xmlns:tools="http://schemas.android.com/tools"
3+
package="io.flutter.plugins.quickactions">
4+
5+
<uses-sdk tools:overrideLibrary="android_libs.ub_uiautomator"/>
46
</manifest>

packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
7474

7575
final boolean didSucceed = dynamicShortcutsSet;
7676

77-
// TODO(camsim99): Move re-dispatch below to background thread when Flutter 2.8+ is stable.
77+
// TODO(camsim99): Move re-dispatch below to background thread when Flutter 2.8+ is
78+
// stable.
7879
uiThreadExecutor.execute(
7980
() -> {
8081
if (didSucceed) {
@@ -163,7 +164,7 @@ private Intent getIntentToOpenMainActivity(String type) {
163164
.setAction(Intent.ACTION_RUN)
164165
.putExtra(EXTRA_ACTION, type)
165166
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
166-
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
167+
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
167168
}
168169

169170
private static class UiThreadExecutor implements Executor {

packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java

+11-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import android.app.Activity;
88
import android.content.Context;
99
import android.content.Intent;
10+
import android.content.pm.ShortcutManager;
1011
import android.os.Build;
1112
import io.flutter.embedding.engine.plugins.FlutterPlugin;
1213
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
@@ -21,6 +22,7 @@ public class QuickActionsPlugin implements FlutterPlugin, ActivityAware, NewInte
2122

2223
private MethodChannel channel;
2324
private MethodCallHandlerImpl handler;
25+
private Activity activity;
2426

2527
/**
2628
* Plugin registration.
@@ -45,9 +47,10 @@ public void onDetachedFromEngine(FlutterPluginBinding binding) {
4547

4648
@Override
4749
public void onAttachedToActivity(ActivityPluginBinding binding) {
48-
handler.setActivity(binding.getActivity());
50+
activity = binding.getActivity();
51+
handler.setActivity(activity);
4952
binding.addOnNewIntentListener(this);
50-
onNewIntent(binding.getActivity().getIntent());
53+
onNewIntent(activity.getIntent());
5154
}
5255

5356
@Override
@@ -74,7 +77,12 @@ public boolean onNewIntent(Intent intent) {
7477
}
7578
// Notify the Dart side if the launch intent has the intent extra relevant to quick actions.
7679
if (intent.hasExtra(MethodCallHandlerImpl.EXTRA_ACTION) && channel != null) {
77-
channel.invokeMethod("launch", intent.getStringExtra(MethodCallHandlerImpl.EXTRA_ACTION));
80+
Context context = activity.getApplicationContext();
81+
ShortcutManager shortcutManager =
82+
(ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
83+
String shortcutId = intent.getStringExtra(MethodCallHandlerImpl.EXTRA_ACTION);
84+
channel.invokeMethod("launch", shortcutId);
85+
shortcutManager.reportShortcutUsed(shortcutId);
7886
}
7987
return false;
8088
}

packages/quick_actions/quick_actions_android/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java

+16
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
import static org.mockito.Mockito.when;
1414

1515
import android.app.Activity;
16+
import android.content.Context;
1617
import android.content.Intent;
18+
import android.content.pm.ShortcutManager;
1719
import android.os.Build;
1820
import androidx.annotation.NonNull;
1921
import androidx.annotation.Nullable;
@@ -86,6 +88,11 @@ public void onAttachedToActivity_buildVersionSupported_invokesLaunchMethod()
8688
when(mockMainActivity.getIntent()).thenReturn(mockIntent);
8789
final ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class);
8890
when(mockActivityPluginBinding.getActivity()).thenReturn(mockMainActivity);
91+
final Context mockContext = mock(Context.class);
92+
when(mockMainActivity.getApplicationContext()).thenReturn(mockContext);
93+
final ShortcutManager mockShortcutManager = mock(ShortcutManager.class);
94+
when(mockContext.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(mockShortcutManager);
95+
plugin.onAttachedToActivity(mockActivityPluginBinding);
8996

9097
// Act
9198
plugin.onAttachedToActivity(mockActivityPluginBinding);
@@ -123,6 +130,15 @@ public void onNewIntent_buildVersionSupported_invokesLaunchMethod()
123130
setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin);
124131
setBuildVersion(SUPPORTED_BUILD);
125132
final Intent mockIntent = createMockIntentWithQuickActionExtra();
133+
final Activity mockMainActivity = mock(Activity.class);
134+
when(mockMainActivity.getIntent()).thenReturn(mockIntent);
135+
final ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class);
136+
when(mockActivityPluginBinding.getActivity()).thenReturn(mockMainActivity);
137+
final Context mockContext = mock(Context.class);
138+
when(mockMainActivity.getApplicationContext()).thenReturn(mockContext);
139+
final ShortcutManager mockShortcutManager = mock(ShortcutManager.class);
140+
when(mockContext.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(mockShortcutManager);
141+
plugin.onAttachedToActivity(mockActivityPluginBinding);
126142

127143
// Act
128144
final boolean onNewIntentReturn = plugin.onNewIntent(mockIntent);

packages/quick_actions/quick_actions_android/example/android/app/build.gradle

+9-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ if (flutterVersionName == null) {
2424
apply plugin: 'com.android.application'
2525
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
2626

27+
def androidXTestVersion = '1.2.0'
28+
2729
android {
2830
compileSdkVersion 31
2931

@@ -55,5 +57,11 @@ dependencies {
5557
testImplementation 'junit:junit:4.13.2'
5658
androidTestImplementation 'androidx.test:runner:1.2.0'
5759
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
58-
api 'androidx.test:core:1.2.0'
60+
api "androidx.test:core:$androidXTestVersion"
61+
62+
androidTestImplementation "androidx.test:runner:$androidXTestVersion"
63+
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
64+
androidTestImplementation 'androidx.test.ext:junit:1.0.0'
65+
androidTestImplementation 'org.mockito:mockito-core:4.3.1'
66+
androidTestImplementation 'org.mockito:mockito-android:4.3.1'
5967
}

packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java

+132-1
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,151 @@
44

55
package io.flutter.plugins.quickactionsexample;
66

7+
import static org.junit.Assert.assertEquals;
8+
import static org.junit.Assert.assertFalse;
79
import static org.junit.Assert.assertTrue;
810

11+
import android.content.Context;
12+
import android.content.Intent;
13+
import android.content.pm.ShortcutInfo;
14+
import android.content.pm.ShortcutManager;
15+
import android.util.Log;
16+
import androidx.lifecycle.Lifecycle;
917
import androidx.test.core.app.ActivityScenario;
18+
import androidx.test.core.app.ApplicationProvider;
19+
import androidx.test.ext.junit.runners.AndroidJUnit4;
20+
import androidx.test.platform.app.InstrumentationRegistry;
21+
import androidx.test.uiautomator.By;
22+
import androidx.test.uiautomator.UiDevice;
23+
import androidx.test.uiautomator.Until;
1024
import io.flutter.plugins.quickactions.QuickActionsPlugin;
25+
import java.util.ArrayList;
26+
import java.util.List;
27+
import java.util.concurrent.atomic.AtomicReference;
28+
import org.junit.After;
29+
import org.junit.Assert;
30+
import org.junit.Before;
1131
import org.junit.Test;
32+
import org.junit.runner.RunWith;
1233

34+
@RunWith(AndroidJUnit4.class)
1335
public class QuickActionsTest {
36+
private Context context;
37+
private UiDevice device;
38+
private ActivityScenario<QuickActionsTestActivity> scenario;
39+
40+
@Before
41+
public void setUp() {
42+
context = ApplicationProvider.getApplicationContext();
43+
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
44+
scenario = ensureAppRunToView();
45+
ensureAllAppShortcutsAreCreated();
46+
}
47+
48+
@After
49+
public void tearDown() {
50+
scenario.close();
51+
Log.i(QuickActionsTest.class.getSimpleName(), "Run to completion");
52+
}
53+
1454
@Test
15-
public void imagePickerPluginIsAdded() {
55+
public void quickActionPluginIsAdded() {
1656
final ActivityScenario<QuickActionsTestActivity> scenario =
1757
ActivityScenario.launch(QuickActionsTestActivity.class);
1858
scenario.onActivity(
1959
activity -> {
2060
assertTrue(activity.engine.getPlugins().has(QuickActionsPlugin.class));
2161
});
2262
}
63+
64+
@Test
65+
public void appShortcutsAreCreated() {
66+
List<ShortcutInfo> expectedShortcuts = createMockShortcuts();
67+
68+
ShortcutManager shortcutManager =
69+
(ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
70+
List<ShortcutInfo> dynamicShortcuts = shortcutManager.getDynamicShortcuts();
71+
72+
// Assert the app shortcuts defined in ../lib/main.dart.
73+
assertFalse(dynamicShortcuts.isEmpty());
74+
assertEquals(expectedShortcuts.size(), dynamicShortcuts.size());
75+
for (ShortcutInfo expectedShortcut : expectedShortcuts) {
76+
ShortcutInfo dynamicShortcut =
77+
dynamicShortcuts
78+
.stream()
79+
.filter(s -> s.getId().equals(expectedShortcut.getId()))
80+
.findFirst()
81+
.get();
82+
83+
assertEquals(expectedShortcut.getShortLabel(), dynamicShortcut.getShortLabel());
84+
assertEquals(expectedShortcut.getLongLabel(), dynamicShortcut.getLongLabel());
85+
}
86+
}
87+
88+
@Test
89+
public void appShortcutLaunchActivityAfterStarting() {
90+
// Arrange
91+
List<ShortcutInfo> shortcuts = createMockShortcuts();
92+
ShortcutInfo firstShortcut = shortcuts.get(0);
93+
ShortcutManager shortcutManager =
94+
(ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
95+
List<ShortcutInfo> dynamicShortcuts = shortcutManager.getDynamicShortcuts();
96+
ShortcutInfo dynamicShortcut =
97+
dynamicShortcuts
98+
.stream()
99+
.filter(s -> s.getId().equals(firstShortcut.getId()))
100+
.findFirst()
101+
.get();
102+
Intent dynamicShortcutIntent = dynamicShortcut.getIntent();
103+
AtomicReference<QuickActionsTestActivity> initialActivity = new AtomicReference<>();
104+
scenario.onActivity(initialActivity::set);
105+
String appReadySentinel = " has launched";
106+
107+
// Act
108+
context.startActivity(dynamicShortcutIntent);
109+
device.wait(Until.hasObject(By.descContains(appReadySentinel)), 2000);
110+
AtomicReference<QuickActionsTestActivity> currentActivity = new AtomicReference<>();
111+
scenario.onActivity(currentActivity::set);
112+
113+
// Assert
114+
Assert.assertTrue(
115+
"AppShortcut:" + firstShortcut.getId() + " does not launch the correct activity",
116+
// We can only find the shortcut type in content description while inspecting it in Ui
117+
// Automator Viewer.
118+
device.hasObject(By.desc(firstShortcut.getId() + appReadySentinel)));
119+
// This is Android SingleTop behavior in which Android does not destroy the initial activity and
120+
// launch a new activity.
121+
Assert.assertEquals(initialActivity.get(), currentActivity.get());
122+
}
123+
124+
private void ensureAllAppShortcutsAreCreated() {
125+
device.wait(Until.hasObject(By.text("actions ready")), 1000);
126+
}
127+
128+
private List<ShortcutInfo> createMockShortcuts() {
129+
List<ShortcutInfo> expectedShortcuts = new ArrayList<>();
130+
131+
String actionOneLocalizedTitle = "Action one";
132+
expectedShortcuts.add(
133+
new ShortcutInfo.Builder(context, "action_one")
134+
.setShortLabel(actionOneLocalizedTitle)
135+
.setLongLabel(actionOneLocalizedTitle)
136+
.build());
137+
138+
String actionTwoLocalizedTitle = "Action two";
139+
expectedShortcuts.add(
140+
new ShortcutInfo.Builder(context, "action_two")
141+
.setShortLabel(actionTwoLocalizedTitle)
142+
.setLongLabel(actionTwoLocalizedTitle)
143+
.build());
144+
145+
return expectedShortcuts;
146+
}
147+
148+
private ActivityScenario<QuickActionsTestActivity> ensureAppRunToView() {
149+
final ActivityScenario<QuickActionsTestActivity> scenario =
150+
ActivityScenario.launch(QuickActionsTestActivity.class);
151+
scenario.moveToState(Lifecycle.State.STARTED);
152+
return scenario;
153+
}
23154
}

packages/quick_actions/quick_actions_android/example/integration_test/quick_actions_test.dart

+9-11
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,21 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:flutter/widgets.dart';
56
import 'package:flutter_test/flutter_test.dart';
67
import 'package:integration_test/integration_test.dart';
7-
import 'package:quick_actions_platform_interface/quick_actions_platform_interface.dart';
8+
import 'package:quick_actions_example/main.dart' as app;
89

910
void main() {
1011
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
1112

12-
testWidgets('Can set shortcuts', (WidgetTester tester) async {
13-
final QuickActionsPlatform quickActions = QuickActionsPlatform.instance;
14-
await quickActions.initialize((String value) {});
13+
testWidgets('Can run MyApp', (WidgetTester tester) async {
14+
app.main();
1515

16-
const ShortcutItem shortCutItem = ShortcutItem(
17-
type: 'action_one',
18-
localizedTitle: 'Action one',
19-
icon: 'AppIcon',
20-
);
21-
expect(
22-
quickActions.setShortcutItems(<ShortcutItem>[shortCutItem]), completes);
16+
await tester.pumpAndSettle();
17+
await tester.pump(const Duration(seconds: 1));
18+
19+
expect(find.byType(Text), findsWidgets);
20+
expect(find.byType(app.MyHomePage), findsOneWidget);
2321
});
2422
}

packages/quick_actions/quick_actions_android/example/lib/main.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class _MyHomePageState extends State<MyHomePage> {
4444
quickActions.initialize((String shortcutType) {
4545
setState(() {
4646
if (shortcutType != null) {
47-
shortcut = shortcutType;
47+
shortcut = '$shortcutType has launched';
4848
}
4949
});
5050
});

packages/quick_actions/quick_actions_android/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: quick_actions_android
22
description: An implementation for the Android platform of the Flutter `quick_actions` plugin.
33
repository: https://github.com/flutter/plugins/tree/main/packages/quick_actions/quick_actions_android
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
5-
version: 0.6.0+11
5+
version: 0.6.1
66

77
environment:
88
sdk: ">=2.15.0 <3.0.0"

0 commit comments

Comments
 (0)