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

Commit 1cf8c9e

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

File tree

8 files changed

+211
-11
lines changed

8 files changed

+211
-11
lines changed

packages/quick_actions/quick_actions/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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"
Lines changed: 5 additions & 3 deletions
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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import android.os.Looper;
1818
import io.flutter.plugin.common.MethodCall;
1919
import io.flutter.plugin.common.MethodChannel;
20+
2021
import java.util.ArrayList;
2122
import java.util.List;
2223
import java.util.Map;
@@ -74,7 +75,8 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
7475

7576
final boolean didSucceed = dynamicShortcutsSet;
7677

77-
// TODO(camsim99): Move re-dispatch below to background thread when Flutter 2.8+ is stable.
78+
// TODO(camsim99): Move re-dispatch below to background thread when Flutter 2.8+ is
79+
// stable.
7880
uiThreadExecutor.execute(
7981
() -> {
8082
if (didSucceed) {
@@ -162,8 +164,7 @@ private Intent getIntentToOpenMainActivity(String type) {
162164
.getLaunchIntentForPackage(packageName)
163165
.setAction(Intent.ACTION_RUN)
164166
.putExtra(EXTRA_ACTION, type)
165-
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
166-
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
167+
.setFlags(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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 9 additions & 2 deletions
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

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,48 @@
44

55
package io.flutter.plugins.quickactionsexample;
66

7-
import static org.junit.Assert.assertTrue;
8-
7+
import android.content.Context;
8+
import android.content.pm.ShortcutInfo;
9+
import android.content.pm.ShortcutManager;
10+
import android.util.Log;
11+
import androidx.lifecycle.Lifecycle;
912
import androidx.test.core.app.ActivityScenario;
13+
import androidx.test.core.app.ApplicationProvider;
14+
import androidx.test.ext.junit.runners.AndroidJUnit4;
15+
import androidx.test.platform.app.InstrumentationRegistry;
16+
import androidx.test.uiautomator.*;
1017
import io.flutter.plugins.quickactions.QuickActionsPlugin;
18+
import org.junit.After;
19+
import org.junit.Assert;
20+
import org.junit.Before;
1121
import org.junit.Test;
22+
import org.junit.runner.RunWith;
23+
24+
import java.util.ArrayList;
25+
import java.util.List;
26+
import java.util.concurrent.atomic.AtomicReference;
27+
28+
import static org.junit.Assert.*;
1229

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

0 commit comments

Comments
 (0)