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

Commit 0211034

Browse files
committed
Add Android native test for QuickActionsPlugin
1 parent 0d5cf74 commit 0211034

File tree

8 files changed

+210
-10
lines changed

8 files changed

+210
-10
lines changed

packages/quick_actions/quick_actions/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
* Updates minimum Flutter version to 2.8.
44

5+
## 0.7.0
6+
7+
* Allow Android to trigger quick actions without restarting the app.
8+
59
## 0.6.0+10
610

711
* Moves Android and iOS implementations to federated packages.

packages/quick_actions/quick_actions/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: Flutter plugin for creating shortcuts on home screen, also known as
33
Quick Actions on iOS and App Shortcuts on Android.
44
repository: https://github.com/flutter/plugins/tree/main/packages/quick_actions/quick_actions
55
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+quick_actions%22
6-
version: 0.6.0+10
6+
version: 0.7.0
77

88
environment:
99
sdk: ">=2.14.0 <3.0.0"
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2-
xmlns:tools="http://schemas.android.com/tools"
3-
package="io.flutter.plugins.quickactions">
1+
<manifest
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-3
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) {
@@ -162,8 +163,7 @@ private Intent getIntentToOpenMainActivity(String type) {
162163
.getLaunchIntentForPackage(packageName)
163164
.setAction(Intent.ACTION_RUN)
164165
.putExtra(EXTRA_ACTION, type)
165-
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
166-
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
166+
.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
167167
}
168168

169169
private static class UiThreadExecutor implements Executor {

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

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public boolean onNewIntent(Intent intent) {
7474
}
7575
// Notify the Dart side if the launch intent has the intent extra relevant to quick actions.
7676
if (intent.hasExtra(MethodCallHandlerImpl.EXTRA_ACTION) && channel != null) {
77+
channel.invokeMethod("getLaunchAction", null);
7778
channel.invokeMethod("launch", intent.getStringExtra(MethodCallHandlerImpl.EXTRA_ACTION));
7879
}
7980
return false;

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

+9-2
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

@@ -53,7 +55,12 @@ flutter {
5355

5456
dependencies {
5557
testImplementation 'junit:junit:4.12'
56-
androidTestImplementation 'androidx.test:runner:1.2.0'
5758
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
58-
api 'androidx.test:core:1.2.0'
59+
api "androidx.test:core:$androidXTestVersion"
60+
61+
androidTestImplementation "androidx.test:runner:$androidXTestVersion"
62+
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
63+
androidTestImplementation 'androidx.test.ext:junit:1.0.0'
64+
androidTestImplementation 'org.mockito:mockito-core:4.3.1'
65+
androidTestImplementation 'org.mockito:mockito-android:4.3.1'
5966
}

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

+139-1
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,47 @@
44

55
package io.flutter.plugins.quickactionsexample;
66

7-
import static org.junit.Assert.assertTrue;
7+
import static org.junit.Assert.*;
88

9+
import android.content.Context;
10+
import android.content.pm.ShortcutInfo;
11+
import android.content.pm.ShortcutManager;
12+
import android.util.Log;
13+
import androidx.lifecycle.Lifecycle;
914
import androidx.test.core.app.ActivityScenario;
15+
import androidx.test.core.app.ApplicationProvider;
16+
import androidx.test.ext.junit.runners.AndroidJUnit4;
17+
import androidx.test.platform.app.InstrumentationRegistry;
18+
import androidx.test.uiautomator.*;
1019
import io.flutter.plugins.quickactions.QuickActionsPlugin;
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
import java.util.concurrent.atomic.AtomicReference;
23+
import org.junit.After;
24+
import org.junit.Assert;
25+
import org.junit.Before;
1126
import org.junit.Test;
27+
import org.junit.runner.RunWith;
1228

29+
@RunWith(AndroidJUnit4.class)
1330
public class QuickActionsTest {
31+
private Context context;
32+
private UiDevice device;
33+
private ActivityScenario<QuickActionsTestActivity> scenario;
34+
35+
@Before
36+
public void setUp() {
37+
context = ApplicationProvider.getApplicationContext();
38+
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
39+
scenario = ensureAppRunToView();
40+
}
41+
42+
@After
43+
public void tearDown() {
44+
scenario.close();
45+
Log.i(QuickActionsTest.class.getSimpleName(), "Run to completion");
46+
}
47+
1448
@Test
1549
public void imagePickerPluginIsAdded() {
1650
final ActivityScenario<QuickActionsTestActivity> scenario =
@@ -20,4 +54,108 @@ public void imagePickerPluginIsAdded() {
2054
assertTrue(activity.engine.getPlugins().has(QuickActionsPlugin.class));
2155
});
2256
}
57+
58+
@Test
59+
public void appShortcutsAreCreated() {
60+
// Arrange
61+
List<Shortcut> expectedShortcuts = createMockShortcuts();
62+
63+
// Act
64+
ShortcutManager shortcutManager =
65+
(ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
66+
List<ShortcutInfo> dynamicShortcuts = shortcutManager.getDynamicShortcuts();
67+
Object[] shortcuts = dynamicShortcuts.stream().map(Shortcut::new).toArray();
68+
69+
// Assert the app shortcuts defined in ../lib/main.dart.
70+
assertFalse(dynamicShortcuts.isEmpty());
71+
assertEquals(2, dynamicShortcuts.size());
72+
assertArrayEquals(expectedShortcuts.toArray(), shortcuts);
73+
}
74+
75+
@Test
76+
public void appShortcutExistsAfterLongPressingAppIcon() throws UiObjectNotFoundException {
77+
// Arrange
78+
List<Shortcut> shortcuts = createMockShortcuts();
79+
String appName = context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
80+
81+
// Act
82+
findAppIcon(device, appName).longClick();
83+
84+
// Assert
85+
for (Shortcut shortcut : shortcuts) {
86+
Assert.assertTrue(
87+
"The specified shortcut label '" + shortcut.shortLabel + "' does not exists.",
88+
device.hasObject(By.text(shortcut.shortLabel)));
89+
}
90+
}
91+
92+
@Test
93+
public void appShortcutLaunchActivityAfterPressing() throws UiObjectNotFoundException {
94+
// Arrange
95+
List<Shortcut> shortcuts = createMockShortcuts();
96+
String appName = context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
97+
Shortcut firstShortcut = shortcuts.get(0);
98+
AtomicReference<QuickActionsTestActivity> initialActivity = new AtomicReference<>();
99+
scenario.onActivity(initialActivity::set);
100+
101+
// Act
102+
findAppIcon(device, appName).longClick();
103+
UiObject appShortcut = device.findObject(new UiSelector().text(firstShortcut.shortLabel));
104+
appShortcut.clickAndWaitForNewWindow();
105+
AtomicReference<QuickActionsTestActivity> currentActivity = new AtomicReference<>();
106+
scenario.onActivity(currentActivity::set);
107+
108+
// Assert
109+
Assert.assertTrue(
110+
"AppShortcut:" + firstShortcut.type + " does not launch the correct activity",
111+
// We can only find the shortcut type in content description while inspecting it in Ui
112+
// Automator Viewer.
113+
device.hasObject(By.desc(firstShortcut.type)));
114+
// This is Android SingleTop behavior in which Android does not destroy the initial activity and
115+
// launch a new activity.
116+
Assert.assertEquals(initialActivity.get(), currentActivity.get());
117+
}
118+
119+
private List<Shortcut> createMockShortcuts() {
120+
List<Shortcut> expectedShortcuts = new ArrayList<>();
121+
String actionOneLocalizedTitle = "Action one";
122+
expectedShortcuts.add(
123+
new Shortcut("action_one", actionOneLocalizedTitle, actionOneLocalizedTitle));
124+
125+
String actionTwoLocalizedTitle = "Action two";
126+
expectedShortcuts.add(
127+
new Shortcut("action_two", actionTwoLocalizedTitle, actionTwoLocalizedTitle));
128+
129+
return expectedShortcuts;
130+
}
131+
132+
private ActivityScenario<QuickActionsTestActivity> ensureAppRunToView() {
133+
final ActivityScenario<QuickActionsTestActivity> scenario =
134+
ActivityScenario.launch(QuickActionsTestActivity.class);
135+
scenario.moveToState(Lifecycle.State.STARTED);
136+
return scenario;
137+
}
138+
139+
private UiObject findAppIcon(UiDevice device, String appName) throws UiObjectNotFoundException {
140+
device.pressHome();
141+
142+
// Swipe up to open App Drawer
143+
UiScrollable homeView = new UiScrollable(new UiSelector().scrollable(true));
144+
homeView.scrollForward();
145+
146+
if (!device.hasObject(By.text(appName))) {
147+
Log.i(
148+
QuickActionsTest.class.getSimpleName(),
149+
"Attempting to scroll App Drawer for App Icon...");
150+
UiScrollable appDrawer = new UiScrollable(new UiSelector().scrollable(true));
151+
// The scrollTextIntoView scrolls to the beginning before performing searching scroll; this
152+
// causes an issue in a scenario where the view is already in the beginning. In this case, it
153+
// scrolls back to home view. Therefore, we perform a dummy forward scroll to ensure it is not
154+
// in the beginning.
155+
appDrawer.scrollForward();
156+
appDrawer.scrollTextIntoView(appName);
157+
}
158+
159+
return device.findObject(new UiSelector().text(appName));
160+
}
23161
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
3+
4+
package io.flutter.plugins.quickactionsexample;
5+
6+
import android.content.pm.ShortcutInfo;
7+
import java.util.Objects;
8+
9+
class Shortcut {
10+
final String type;
11+
final String shortLabel;
12+
final String longLabel;
13+
String icon;
14+
15+
public Shortcut(ShortcutInfo shortcutInfo) {
16+
this.type = shortcutInfo.getId();
17+
this.shortLabel = shortcutInfo.getShortLabel().toString();
18+
this.longLabel = shortcutInfo.getLongLabel().toString();
19+
}
20+
21+
public Shortcut(String type, String shortLabel, String longLabel) {
22+
this.type = type;
23+
this.shortLabel = shortLabel;
24+
this.longLabel = longLabel;
25+
}
26+
27+
@Override
28+
public boolean equals(Object o) {
29+
if (this == o) return true;
30+
if (o == null || getClass() != o.getClass()) return false;
31+
32+
Shortcut shortcut = (Shortcut) o;
33+
34+
if (!type.equals(shortcut.type)) return false;
35+
if (!shortLabel.equals(shortcut.shortLabel)) return false;
36+
if (!longLabel.equals(shortcut.longLabel)) return false;
37+
return Objects.equals(icon, shortcut.icon);
38+
}
39+
40+
@Override
41+
public int hashCode() {
42+
int result = type.hashCode();
43+
result = 31 * result + shortLabel.hashCode();
44+
result = 31 * result + longLabel.hashCode();
45+
result = 31 * result + (icon != null ? icon.hashCode() : 0);
46+
return result;
47+
}
48+
}

0 commit comments

Comments
 (0)