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

Commit

Permalink
[quick_actions] Android handle quick action without restart (#5048)
Browse files Browse the repository at this point in the history
  • Loading branch information
TabooSun authored Jul 11, 2022
1 parent 50a2533 commit 68cdc58
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 22 deletions.
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"
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
Expand Up @@ -4,20 +4,151 @@

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() {
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 @@ -44,7 +44,7 @@ class _MyHomePageState extends State<MyHomePage> {
quickActions.initialize((String shortcutType) {
setState(() {
if (shortcutType != null) {
shortcut = shortcutType;
shortcut = '$shortcutType has launched';
}
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/quick_actions/quick_actions_android/pubspec.yaml
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

0 comments on commit 68cdc58

Please sign in to comment.