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

Commit 8f32876

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

File tree

8 files changed

+212
-14
lines changed

8 files changed

+212
-14
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 & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Copyright 2013 The Flutter Authors. All rights reserved.
2-
// Use of this source code is governed by a BSD-style license that can be
3-
// found in the LICENSE file.
2+
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
43

54
package io.flutter.plugins.quickactions;
65

@@ -74,7 +73,8 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
7473

7574
final boolean didSucceed = dynamicShortcutsSet;
7675

77-
// TODO(camsim99): Move re-dispatch below to background thread when Flutter 2.8+ is stable.
76+
// TODO(camsim99): Move re-dispatch below to background thread when Flutter 2.8+ is
77+
// stable.
7878
uiThreadExecutor.execute(
7979
() -> {
8080
if (didSucceed) {
@@ -162,8 +162,7 @@ private Intent getIntentToOpenMainActivity(String type) {
162162
.getLaunchIntentForPackage(packageName)
163163
.setAction(Intent.ACTION_RUN)
164164
.putExtra(EXTRA_ACTION, type)
165-
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
166-
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
165+
.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
167166
}
168167

169168
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
}
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,49 @@
11
// Copyright 2013 The Flutter Authors. All rights reserved.
2-
// Use of this source code is governed by a BSD-style license that can be
3-
// found in the LICENSE file.
2+
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
43

54
package io.flutter.plugins.quickactionsexample;
65

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

8+
import android.content.Context;
9+
import android.content.pm.ShortcutInfo;
10+
import android.content.pm.ShortcutManager;
11+
import android.util.Log;
12+
import androidx.lifecycle.Lifecycle;
913
import androidx.test.core.app.ActivityScenario;
14+
import androidx.test.core.app.ApplicationProvider;
15+
import androidx.test.ext.junit.runners.AndroidJUnit4;
16+
import androidx.test.platform.app.InstrumentationRegistry;
17+
import androidx.test.uiautomator.*;
1018
import io.flutter.plugins.quickactions.QuickActionsPlugin;
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.concurrent.atomic.AtomicReference;
22+
import org.junit.After;
23+
import org.junit.Assert;
24+
import org.junit.Before;
1125
import org.junit.Test;
26+
import org.junit.runner.RunWith;
1227

28+
@RunWith(AndroidJUnit4.class)
1329
public class QuickActionsTest {
30+
private Context context;
31+
private UiDevice device;
32+
private ActivityScenario<QuickActionsTestActivity> scenario;
33+
34+
@Before
35+
public void setUp() {
36+
context = ApplicationProvider.getApplicationContext();
37+
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
38+
scenario = ensureAppRunToView();
39+
}
40+
41+
@After
42+
public void tearDown() {
43+
scenario.close();
44+
Log.i(QuickActionsTest.class.getSimpleName(), "Run to completion");
45+
}
46+
1447
@Test
1548
public void imagePickerPluginIsAdded() {
1649
final ActivityScenario<QuickActionsTestActivity> scenario =
@@ -20,4 +53,108 @@ public void imagePickerPluginIsAdded() {
2053
assertTrue(activity.engine.getPlugins().has(QuickActionsPlugin.class));
2154
});
2255
}
56+
57+
@Test
58+
public void appShortcutsAreCreated() {
59+
// Arrange
60+
List<Shortcut> expectedShortcuts = createMockShortcuts();
61+
62+
// Act
63+
ShortcutManager shortcutManager =
64+
(ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
65+
List<ShortcutInfo> dynamicShortcuts = shortcutManager.getDynamicShortcuts();
66+
Object[] shortcuts = dynamicShortcuts.stream().map(Shortcut::new).toArray();
67+
68+
// Assert the app shortcuts defined in ../lib/main.dart.
69+
assertFalse(dynamicShortcuts.isEmpty());
70+
assertEquals(2, dynamicShortcuts.size());
71+
assertArrayEquals(expectedShortcuts.toArray(), shortcuts);
72+
}
73+
74+
@Test
75+
public void appShortcutExistsAfterLongPressingAppIcon() throws UiObjectNotFoundException {
76+
// Arrange
77+
List<Shortcut> shortcuts = createMockShortcuts();
78+
String appName = context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
79+
80+
// Act
81+
findAppIcon(device, appName).longClick();
82+
83+
// Assert
84+
for (Shortcut shortcut : shortcuts) {
85+
Assert.assertTrue(
86+
"The specified shortcut label '" + shortcut.shortLabel + "' does not exists.",
87+
device.hasObject(By.text(shortcut.shortLabel)));
88+
}
89+
}
90+
91+
@Test
92+
public void appShortcutLaunchActivityAfterPressing() throws UiObjectNotFoundException {
93+
// Arrange
94+
List<Shortcut> shortcuts = createMockShortcuts();
95+
String appName = context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
96+
Shortcut firstShortcut = shortcuts.get(0);
97+
AtomicReference<QuickActionsTestActivity> initialActivity = new AtomicReference<>();
98+
scenario.onActivity(initialActivity::set);
99+
100+
// Act
101+
findAppIcon(device, appName).longClick();
102+
UiObject appShortcut = device.findObject(new UiSelector().text(firstShortcut.shortLabel));
103+
appShortcut.clickAndWaitForNewWindow();
104+
AtomicReference<QuickActionsTestActivity> currentActivity = new AtomicReference<>();
105+
scenario.onActivity(currentActivity::set);
106+
107+
// Assert
108+
Assert.assertTrue(
109+
"AppShortcut:" + firstShortcut.type + " does not launch the correct activity",
110+
// We can only find the shortcut type in content description while inspecting it in Ui
111+
// Automator Viewer.
112+
device.hasObject(By.desc(firstShortcut.type)));
113+
// This is Android SingleTop behavior in which Android does not destroy the initial activity and
114+
// launch a new activity.
115+
Assert.assertEquals(initialActivity.get(), currentActivity.get());
116+
}
117+
118+
private List<Shortcut> createMockShortcuts() {
119+
List<Shortcut> expectedShortcuts = new ArrayList<>();
120+
String actionOneLocalizedTitle = "Action one";
121+
expectedShortcuts.add(
122+
new Shortcut("action_one", actionOneLocalizedTitle, actionOneLocalizedTitle));
123+
124+
String actionTwoLocalizedTitle = "Action two";
125+
expectedShortcuts.add(
126+
new Shortcut("action_two", actionTwoLocalizedTitle, actionTwoLocalizedTitle));
127+
128+
return expectedShortcuts;
129+
}
130+
131+
private ActivityScenario<QuickActionsTestActivity> ensureAppRunToView() {
132+
final ActivityScenario<QuickActionsTestActivity> scenario =
133+
ActivityScenario.launch(QuickActionsTestActivity.class);
134+
scenario.moveToState(Lifecycle.State.STARTED);
135+
return scenario;
136+
}
137+
138+
private UiObject findAppIcon(UiDevice device, String appName) throws UiObjectNotFoundException {
139+
device.pressHome();
140+
141+
// Swipe up to open App Drawer
142+
UiScrollable homeView = new UiScrollable(new UiSelector().scrollable(true));
143+
homeView.scrollForward();
144+
145+
if (!device.hasObject(By.text(appName))) {
146+
Log.i(
147+
QuickActionsTest.class.getSimpleName(),
148+
"Attempting to scroll App Drawer for App Icon...");
149+
UiScrollable appDrawer = new UiScrollable(new UiSelector().scrollable(true));
150+
// The scrollTextIntoView scrolls to the beginning before performing searching scroll; this
151+
// causes an issue in a scenario where the view is already in the beginning. In this case, it
152+
// scrolls back to home view. Therefore, we perform a dummy forward scroll to ensure it is not
153+
// in the beginning.
154+
appDrawer.scrollForward();
155+
appDrawer.scrollTextIntoView(appName);
156+
}
157+
158+
return device.findObject(new UiSelector().text(appName));
159+
}
23160
}
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)