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 27 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_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.6.1

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

## 0.6.0+11

* Updates references to the obsolete master branch.
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 @@ -163,7 +164,7 @@ private Intent getIntentToOpenMainActivity(String type) {
.setAction(Intent.ACTION_RUN)
.putExtra(EXTRA_ACTION, type)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
.addFlags(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 @@ -7,6 +7,7 @@
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutManager;
import android.os.Build;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
Expand All @@ -21,6 +22,7 @@ public class QuickActionsPlugin implements FlutterPlugin, ActivityAware, NewInte

private MethodChannel channel;
private MethodCallHandlerImpl handler;
private Activity activity;

/**
* Plugin registration.
Expand All @@ -45,9 +47,10 @@ public void onDetachedFromEngine(FlutterPluginBinding binding) {

@Override
public void onAttachedToActivity(ActivityPluginBinding binding) {
handler.setActivity(binding.getActivity());
activity = binding.getActivity();
handler.setActivity(activity);
binding.addOnNewIntentListener(this);
onNewIntent(binding.getActivity().getIntent());
onNewIntent(activity.getIntent());
}

@Override
Expand All @@ -74,7 +77,12 @@ 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("launch", intent.getStringExtra(MethodCallHandlerImpl.EXTRA_ACTION));
Context context = activity.getApplicationContext();
ShortcutManager shortcutManager =
(ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
String shortcutId = intent.getStringExtra(MethodCallHandlerImpl.EXTRA_ACTION);
channel.invokeMethod("launch", shortcutId);
shortcutManager.reportShortcutUsed(shortcutId);
}
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
import static org.mockito.Mockito.when;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutManager;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand Down Expand Up @@ -86,6 +88,11 @@ public void onAttachedToActivity_buildVersionSupported_invokesLaunchMethod()
when(mockMainActivity.getIntent()).thenReturn(mockIntent);
final ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class);
when(mockActivityPluginBinding.getActivity()).thenReturn(mockMainActivity);
final Context mockContext = mock(Context.class);
when(mockMainActivity.getApplicationContext()).thenReturn(mockContext);
final ShortcutManager mockShortcutManager = mock(ShortcutManager.class);
when(mockContext.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(mockShortcutManager);
plugin.onAttachedToActivity(mockActivityPluginBinding);

// Act
plugin.onAttachedToActivity(mockActivityPluginBinding);
Expand Down Expand Up @@ -123,6 +130,15 @@ public void onNewIntent_buildVersionSupported_invokesLaunchMethod()
setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin);
setBuildVersion(SUPPORTED_BUILD);
final Intent mockIntent = createMockIntentWithQuickActionExtra();
final Activity mockMainActivity = mock(Activity.class);
when(mockMainActivity.getIntent()).thenReturn(mockIntent);
final ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class);
when(mockActivityPluginBinding.getActivity()).thenReturn(mockMainActivity);
final Context mockContext = mock(Context.class);
when(mockMainActivity.getApplicationContext()).thenReturn(mockContext);
final ShortcutManager mockShortcutManager = mock(ShortcutManager.class);
when(mockContext.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(mockShortcutManager);
plugin.onAttachedToActivity(mockActivityPluginBinding);

// Act
final boolean onNewIntentReturn = plugin.onNewIntent(mockIntent);
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 @@ -55,5 +57,11 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
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,154 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
/// Copyright 2013 The Flutter Authors. All rights reserved.
TabooSun marked this conversation as resolved.
Show resolved Hide resolved
// 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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import android.content.Context;
import android.content.Intent;
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.By;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.Until;
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();
ensureAllAppShortcutsAreCreated();
}

@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 (ShortcutInfo expectedShortcut : expectedShortcuts) {
ShortcutInfo dynamicShortcut =
dynamicShortcuts
.stream()
.filter(s -> s.getId().equals(expectedShortcut.getId()))
.findFirst()
.get();

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

@Test
public void appShortcutLaunchActivityAfterStarting() {
// Arrange
List<ShortcutInfo> shortcuts = createMockShortcuts();
ShortcutInfo firstShortcut = shortcuts.get(0);
ShortcutManager shortcutManager =
(ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
List<ShortcutInfo> dynamicShortcuts = shortcutManager.getDynamicShortcuts();
ShortcutInfo dynamicShortcut =
dynamicShortcuts
.stream()
.filter(s -> s.getId().equals(firstShortcut.getId()))
.findFirst()
.get();
Intent dynamicShortcutIntent = dynamicShortcut.getIntent();
AtomicReference<QuickActionsTestActivity> initialActivity = new AtomicReference<>();
scenario.onActivity(initialActivity::set);
String appReadySentinel = " has launched";

// Act
context.startActivity(dynamicShortcutIntent);
device.wait(Until.hasObject(By.descContains(appReadySentinel)), 2000);
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() + appReadySentinel)));
// 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 void ensureAllAppShortcutsAreCreated() {
device.wait(Until.hasObject(By.text("actions ready")), 1000);
}

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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,21 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:quick_actions_platform_interface/quick_actions_platform_interface.dart';
import 'package:quick_actions_example/main.dart' as app;

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

testWidgets('Can set shortcuts', (WidgetTester tester) async {
final QuickActionsPlatform quickActions = QuickActionsPlatform.instance;
await quickActions.initialize((String value) {});
testWidgets('Can run MyApp', (WidgetTester tester) async {
app.main();

const ShortcutItem shortCutItem = ShortcutItem(
type: 'action_one',
localizedTitle: 'Action one',
icon: 'AppIcon',
);
expect(
quickActions.setShortcutItems(<ShortcutItem>[shortCutItem]), completes);
await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1));

expect(find.byType(Text), findsWidgets);
expect(find.byType(app.MyHomePage), findsOneWidget);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ class _MyHomePageState extends State<MyHomePage> {

final QuickActionsAndroid quickActions = QuickActionsAndroid();
quickActions.initialize((String shortcutType) {
print('Handling shortcut: $shortcutType');
setState(() {
if (shortcutType != null) {
shortcut = shortcutType;
shortcut = '$shortcutType has launched';
}
});
});
Expand Down Expand Up @@ -75,6 +76,7 @@ class _MyHomePageState extends State<MyHomePage> {
title: Text(shortcut),
),
body: const Center(
// Remember to update android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java appShortcutLaunchActivityAfterPressing if you change this.
child: Text('On home screen, long press the app icon to '
'get Action one or Action two options. Tapping on that action should '
'set the toolbar title.'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: quick_actions_android
description: An implementation for the Android platform of the Flutter `quick_actions` plugin.
repository: https://github.com/flutter/plugins/tree/main/packages/quick_actions/quick_actions_android
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
version: 0.6.0+11
version: 0.6.1

environment:
sdk: ">=2.15.0 <3.0.0"
Expand Down