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

Commit

Permalink
[quick_actions] Support keeping state when App Shortcut is triggered …
Browse files Browse the repository at this point in the history
…in Android
  • Loading branch information
TabooSun committed Mar 13, 2022
1 parent 0d5cf74 commit 54648a7
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 10 deletions.
4 changes: 4 additions & 0 deletions packages/quick_actions/quick_actions/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

* Updates minimum Flutter version to 2.8.

## 0.7.0

* Allow Android to trigger quick actions without restarting the app.

## 0.6.0+10

* Moves Android and iOS implementations to federated packages.
Expand Down
2 changes: 1 addition & 1 deletion packages/quick_actions/quick_actions/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: Flutter plugin for creating shortcuts on home screen, also known as
Quick Actions on iOS and App Shortcuts on Android.
repository: https://github.com/flutter/plugins/tree/main/packages/quick_actions/quick_actions
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+quick_actions%22
version: 0.6.0+10
version: 0.7.0

environment:
sdk: ">=2.14.0 <3.0.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -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">
<manifest
xmlns:tools="http://schemas.android.com/tools"
package="io.flutter.plugins.quickactions">

<uses-sdk tools:overrideLibrary="android_libs.ub_uiautomator"/>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {

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) {
Expand Down Expand Up @@ -162,8 +163,7 @@ private Intent getIntentToOpenMainActivity(String type) {
.getLaunchIntentForPackage(packageName)
.setAction(Intent.ACTION_RUN)
.putExtra(EXTRA_ACTION, type)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
}

private static class UiThreadExecutor implements Executor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public boolean onNewIntent(Intent intent) {
}
// 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("getLaunchAction", null);
channel.invokeMethod("launch", intent.getStringExtra(MethodCallHandlerImpl.EXTRA_ACTION));
}
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ if (flutterVersionName == null) {
apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

def androidXTestVersion = '1.2.0'

android {
compileSdkVersion 31

Expand Down Expand Up @@ -53,7 +55,12 @@ flutter {

dependencies {
testImplementation 'junit:junit:4.12'
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'
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,47 @@

package io.flutter.plugins.quickactionsexample;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;

import android.content.Context;
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.*;
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();
}

@After
public void tearDown() {
scenario.close();
Log.i(QuickActionsTest.class.getSimpleName(), "Run to completion");
}

@Test
public void imagePickerPluginIsAdded() {
final ActivityScenario<QuickActionsTestActivity> scenario =
Expand All @@ -20,4 +54,108 @@ public void imagePickerPluginIsAdded() {
assertTrue(activity.engine.getPlugins().has(QuickActionsPlugin.class));
});
}

@Test
public void appShortcutsAreCreated() {
// Arrange
List<Shortcut> expectedShortcuts = createMockShortcuts();

// Act
ShortcutManager shortcutManager =
(ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
List<ShortcutInfo> dynamicShortcuts = shortcutManager.getDynamicShortcuts();
Object[] shortcuts = dynamicShortcuts.stream().map(Shortcut::new).toArray();

// Assert the app shortcuts defined in ../lib/main.dart.
assertFalse(dynamicShortcuts.isEmpty());
assertEquals(2, dynamicShortcuts.size());
assertArrayEquals(expectedShortcuts.toArray(), shortcuts);
}

@Test
public void appShortcutExistsAfterLongPressingAppIcon() throws UiObjectNotFoundException {
// Arrange
List<Shortcut> shortcuts = createMockShortcuts();
String appName = context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();

// Act
findAppIcon(device, appName).longClick();

// Assert
for (Shortcut shortcut : shortcuts) {
Assert.assertTrue(
"The specified shortcut label '" + shortcut.shortLabel + "' does not exists.",
device.hasObject(By.text(shortcut.shortLabel)));
}
}

@Test
public void appShortcutLaunchActivityAfterPressing() throws UiObjectNotFoundException {
// Arrange
List<Shortcut> shortcuts = createMockShortcuts();
String appName = context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
Shortcut firstShortcut = shortcuts.get(0);
AtomicReference<QuickActionsTestActivity> initialActivity = new AtomicReference<>();
scenario.onActivity(initialActivity::set);

// Act
findAppIcon(device, appName).longClick();
UiObject appShortcut = device.findObject(new UiSelector().text(firstShortcut.shortLabel));
appShortcut.clickAndWaitForNewWindow();
AtomicReference<QuickActionsTestActivity> currentActivity = new AtomicReference<>();
scenario.onActivity(currentActivity::set);

// Assert
Assert.assertTrue(
"AppShortcut:" + firstShortcut.type + " 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.type)));
// 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 List<Shortcut> createMockShortcuts() {
List<Shortcut> expectedShortcuts = new ArrayList<>();
String actionOneLocalizedTitle = "Action one";
expectedShortcuts.add(
new Shortcut("action_one", actionOneLocalizedTitle, actionOneLocalizedTitle));

String actionTwoLocalizedTitle = "Action two";
expectedShortcuts.add(
new Shortcut("action_two", actionTwoLocalizedTitle, actionTwoLocalizedTitle));

return expectedShortcuts;
}

private ActivityScenario<QuickActionsTestActivity> ensureAppRunToView() {
final ActivityScenario<QuickActionsTestActivity> scenario =
ActivityScenario.launch(QuickActionsTestActivity.class);
scenario.moveToState(Lifecycle.State.STARTED);
return scenario;
}

private UiObject findAppIcon(UiDevice device, String appName) throws UiObjectNotFoundException {
device.pressHome();

// Swipe up to open App Drawer
UiScrollable homeView = new UiScrollable(new UiSelector().scrollable(true));
homeView.scrollForward();

if (!device.hasObject(By.text(appName))) {
Log.i(
QuickActionsTest.class.getSimpleName(),
"Attempting to scroll App Drawer for App Icon...");
UiScrollable appDrawer = new UiScrollable(new UiSelector().scrollable(true));
// The scrollTextIntoView scrolls to the beginning before performing searching scroll; this
// causes an issue in a scenario where the view is already in the beginning. In this case, it
// scrolls back to home view. Therefore, we perform a dummy forward scroll to ensure it is not
// in the beginning.
appDrawer.scrollForward();
appDrawer.scrollTextIntoView(appName);
}

return device.findObject(new UiSelector().text(appName));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package io.flutter.plugins.quickactionsexample;

import android.content.pm.ShortcutInfo;
import java.util.Objects;

class Shortcut {
final String type;
final String shortLabel;
final String longLabel;
String icon;

public Shortcut(ShortcutInfo shortcutInfo) {
this.type = shortcutInfo.getId();
this.shortLabel = shortcutInfo.getShortLabel().toString();
this.longLabel = shortcutInfo.getLongLabel().toString();
}

public Shortcut(String type, String shortLabel, String longLabel) {
this.type = type;
this.shortLabel = shortLabel;
this.longLabel = longLabel;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

Shortcut shortcut = (Shortcut) o;

if (!type.equals(shortcut.type)) return false;
if (!shortLabel.equals(shortcut.shortLabel)) return false;
if (!longLabel.equals(shortcut.longLabel)) return false;
return Objects.equals(icon, shortcut.icon);
}

@Override
public int hashCode() {
int result = type.hashCode();
result = 31 * result + shortLabel.hashCode();
result = 31 * result + longLabel.hashCode();
result = 31 * result + (icon != null ? icon.hashCode() : 0);
return result;
}
}

0 comments on commit 54648a7

Please sign in to comment.