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

[quick_actions] Android handle quick action without restart #5048

Merged
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f4d657f
[quick_actions] Support keeping state when App Shortcut is triggered …
TabooSun Mar 12, 2022
42ca2da
Remove redundant comments in QuickActionsTest
TabooSun Apr 29, 2022
d709a8e
Merge branch 'master' into android-handle-quick-action-without-restart
TabooSun Apr 29, 2022
d00c370
Fix incorrect test method name in QuickActionsTest
TabooSun Apr 29, 2022
b61dc9c
Revert removal of xml metadata
TabooSun Apr 29, 2022
661bc5d
Refactor to use ShortcutInfo and remove custom Shortcut class
TabooSun Apr 30, 2022
28a82b3
Format code
TabooSun Apr 30, 2022
8c754e3
Correct license file formatting
TabooSun May 4, 2022
84372e8
Set version in pubspec.yaml correctly
TabooSun May 4, 2022
4c83cd4
Fix import style that violates Google Java Style Guide
TabooSun May 4, 2022
ad7b4a9
Fix calling Java MethodChannel from Java side
TabooSun May 4, 2022
24bdc10
Bump version to 0.6.1
TabooSun May 4, 2022
4784fae
Update CHANGELOG
TabooSun May 4, 2022
b2cff95
Fix test failure in QuickActionsTest
TabooSun May 4, 2022
a962152
Fix CHANGELOG style
stuartmorgan Jun 7, 2022
a623044
Merge branch 'main' into android-handle-quick-action-without-restart
stuartmorgan Jun 7, 2022
f06f961
Use shortcut id to locate actual shortcut
TabooSun Jun 8, 2022
36bc6ea
Merge branch 'main' into android-handle-quick-action-without-restart
stuartmorgan Jun 9, 2022
a3437e1
Wait for all the shortcut creations before running test
TabooSun Jun 12, 2022
ac3dd42
Merge branch 'main' into android-handle-quick-action-without-restart
stuartmorgan Jun 13, 2022
c359441
Change the integration_test to run the app
TabooSun Jun 17, 2022
d03b75c
Merge branch 'main' into android-handle-quick-action-without-restart
stuartmorgan Jun 21, 2022
a1a4546
Await the description in home page before asserting
TabooSun Jun 22, 2022
87db162
Merge remote-tracking branch 'origin/android-handle-quick-action-with…
TabooSun Jun 22, 2022
116386b
Fix flakey test
TabooSun Jul 6, 2022
791fcfb
Merge branch 'main' into android-handle-quick-action-without-restart
TabooSun Jul 6, 2022
de02c2b
Merge branch 'main' into android-handle-quick-action-without-restart
stuartmorgan Jul 6, 2022
33bcb24
Fix license comment
TabooSun Jul 8, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -3,6 +3,10 @@
* Updates minimum Flutter version to 2.8.
* Adds OS version support information to README.

## 0.7.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stuartmorgan Should this include the NEXT item since this is a version bump?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but the larger problem is that this version bump is to the wrong package. The changes are to quick_actions_android, not quick_actions, so that's what needs CHANGELOG and version updates.

@TabooSun Please make sure the publishable CI check is passing when you update your PR; the failure messages there explain the problem.


* Allow Android to trigger quick actions without restarting the app.
stuartmorgan marked this conversation as resolved.
Show resolved Hide resolved

## 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way Dart versioning works for pre-1.0, this is a breaking version change (see link in the PR checklist about Dart versions). Is there anything about this change that actually breaks clients?


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"
stuartmorgan marked this conversation as resolved.
Show resolved Hide resolved
xmlns:tools="http://schemas.android.com/tools"
package="io.flutter.plugins.quickactions">
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are calling Java code from Java code via the method channel? And why are you calling a getter but not using the result?

Copy link
Contributor Author

@TabooSun TabooSun May 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the code.

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
@@ -1,23 +1,169 @@
// 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.
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert this change; it's why the format check is failing. The licenses follow an exact format, whitespace included.


package io.flutter.plugins.quickactionsexample;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


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.*;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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() {
public void quickActionPluginIsAdded() {
camsim99 marked this conversation as resolved.
Show resolved Hide resolved
final ActivityScenario<QuickActionsTestActivity> scenario =
ActivityScenario.launch(QuickActionsTestActivity.class);
scenario.onActivity(
activity -> {
assertTrue(activity.engine.getPlugins().has(QuickActionsPlugin.class));
});
}

@Test
public void appShortcutsAreCreated() {
List<ShortcutInfo> expectedShortcuts = createMockShortcuts();

ShortcutManager shortcutManager =
(ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
List<ShortcutInfo> dynamicShortcuts = shortcutManager.getDynamicShortcuts();

// Assert the app shortcuts defined in ../lib/main.dart.
assertFalse(dynamicShortcuts.isEmpty());
assertEquals(expectedShortcuts.size(), dynamicShortcuts.size());
for (int i = 0; i < expectedShortcuts.size(); i++) {
ShortcutInfo expectedShortcut = expectedShortcuts.get(i);
ShortcutInfo dynamicShortcut = dynamicShortcuts.get(i);

assertEquals(expectedShortcut.getId(), dynamicShortcut.getId());
assertEquals(expectedShortcut.getShortLabel(), dynamicShortcut.getShortLabel());
assertEquals(expectedShortcut.getLongLabel(), dynamicShortcut.getLongLabel());
}
}

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

findAppIcon(device, appName).longClick();

for (ShortcutInfo shortcut : shortcuts) {
Assert.assertTrue(
"The specified shortcut label '" + shortcut.getShortLabel() + "' does not exist.",
device.hasObject(By.text(shortcut.getShortLabel().toString())));
}
}

@Test
public void appShortcutLaunchActivityAfterPressing() throws UiObjectNotFoundException {
// Arrange
List<ShortcutInfo> shortcuts = createMockShortcuts();
String appName = context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
ShortcutInfo 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.getShortLabel().toString()));
appShortcut.clickAndWaitForNewWindow();
AtomicReference<QuickActionsTestActivity> currentActivity = new AtomicReference<>();
scenario.onActivity(currentActivity::set);

// Assert
Assert.assertTrue(
"AppShortcut:" + firstShortcut.getId() + " 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.getId())));
// 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<ShortcutInfo> createMockShortcuts() {
List<ShortcutInfo> expectedShortcuts = new ArrayList<>();

String actionOneLocalizedTitle = "Action one";
expectedShortcuts.add(
new ShortcutInfo.Builder(context, "action_one")
.setShortLabel(actionOneLocalizedTitle)
.setLongLabel(actionOneLocalizedTitle)
.build());

String actionTwoLocalizedTitle = "Action two";
expectedShortcuts.add(
new ShortcutInfo.Builder(context, "action_two")
.setShortLabel(actionTwoLocalizedTitle)
.setLongLabel(actionTwoLocalizedTitle)
.build());

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));
}
}