From e8127e76d596572a1c281aaca5a4dd73912defde Mon Sep 17 00:00:00 2001 From: Tarrin Neal Date: Mon, 16 Oct 2023 17:42:54 -0700 Subject: [PATCH] [quick_actions] Convert android to pigeon (#5099) Converts from direct use of method channels to using Pigeon. Part of https://github.com/flutter/flutter/issues/117844 part of https://github.com/flutter/flutter/issues/117913 --- .../quick_actions_android/CHANGELOG.md | 4 + .../plugins/quickactions/Messages.java | 322 ++++++++++++++++++ .../quickactions/MethodCallHandlerImpl.java | 180 ---------- .../plugins/quickactions/QuickActions.java | 193 +++++++++++ .../quickactions/QuickActionsPlugin.java | 57 ++-- .../quickactions/QuickActionsTest.java | 29 +- .../lib/quick_actions_android.dart | 60 ++-- .../lib/src/messages.g.dart | 183 ++++++++++ .../pigeons/copyright.txt | 3 + .../pigeons/messages.dart | 52 +++ .../quick_actions_android/pubspec.yaml | 3 +- .../test/quick_actions_android_test.dart | 229 +++++-------- 12 files changed, 912 insertions(+), 403 deletions(-) create mode 100644 packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/Messages.java delete mode 100644 packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java create mode 100644 packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActions.java create mode 100644 packages/quick_actions/quick_actions_android/lib/src/messages.g.dart create mode 100644 packages/quick_actions/quick_actions_android/pigeons/copyright.txt create mode 100644 packages/quick_actions/quick_actions_android/pigeons/messages.dart diff --git a/packages/quick_actions/quick_actions_android/CHANGELOG.md b/packages/quick_actions/quick_actions_android/CHANGELOG.md index f63893bbc0..e31f68fee8 100644 --- a/packages/quick_actions/quick_actions_android/CHANGELOG.md +++ b/packages/quick_actions/quick_actions_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.9 + +* Changes method channels to pigeon. + ## 1.0.8 * Adds pub topics to package metadata. diff --git a/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/Messages.java b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/Messages.java new file mode 100644 index 0000000000..2057127e81 --- /dev/null +++ b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/Messages.java @@ -0,0 +1,322 @@ +// 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. +// Autogenerated from Pigeon (v11.0.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +package io.flutter.plugins.quickactions; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StandardMessageCodec; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Generated class from Pigeon. */ +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) +public class Messages { + + /** Error class for passing custom error details to Flutter via a thrown PlatformException. */ + public static class FlutterError extends RuntimeException { + + /** The error code. */ + public final String code; + + /** The error details. Must be a datatype supported by the api codec. */ + public final Object details; + + public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) { + super(message); + this.code = code; + this.details = details; + } + } + + @NonNull + protected static ArrayList wrapError(@NonNull Throwable exception) { + ArrayList errorList = new ArrayList(3); + if (exception instanceof FlutterError) { + FlutterError error = (FlutterError) exception; + errorList.add(error.code); + errorList.add(error.getMessage()); + errorList.add(error.details); + } else { + errorList.add(exception.toString()); + errorList.add(exception.getClass().getSimpleName()); + errorList.add( + "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + } + return errorList; + } + + /** + * Home screen quick-action shortcut item. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class ShortcutItemMessage { + /** The identifier of this item; should be unique within the app. */ + private @NonNull String type; + + public @NonNull String getType() { + return type; + } + + public void setType(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"type\" is null."); + } + this.type = setterArg; + } + + /** Localized title of the item. */ + private @NonNull String localizedTitle; + + public @NonNull String getLocalizedTitle() { + return localizedTitle; + } + + public void setLocalizedTitle(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"localizedTitle\" is null."); + } + this.localizedTitle = setterArg; + } + + /** Name of native resource to be displayed as the icon for this item. */ + private @Nullable String icon; + + public @Nullable String getIcon() { + return icon; + } + + public void setIcon(@Nullable String setterArg) { + this.icon = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + ShortcutItemMessage() {} + + public static final class Builder { + + private @Nullable String type; + + public @NonNull Builder setType(@NonNull String setterArg) { + this.type = setterArg; + return this; + } + + private @Nullable String localizedTitle; + + public @NonNull Builder setLocalizedTitle(@NonNull String setterArg) { + this.localizedTitle = setterArg; + return this; + } + + private @Nullable String icon; + + public @NonNull Builder setIcon(@Nullable String setterArg) { + this.icon = setterArg; + return this; + } + + public @NonNull ShortcutItemMessage build() { + ShortcutItemMessage pigeonReturn = new ShortcutItemMessage(); + pigeonReturn.setType(type); + pigeonReturn.setLocalizedTitle(localizedTitle); + pigeonReturn.setIcon(icon); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(3); + toListResult.add(type); + toListResult.add(localizedTitle); + toListResult.add(icon); + return toListResult; + } + + static @NonNull ShortcutItemMessage fromList(@NonNull ArrayList list) { + ShortcutItemMessage pigeonResult = new ShortcutItemMessage(); + Object type = list.get(0); + pigeonResult.setType((String) type); + Object localizedTitle = list.get(1); + pigeonResult.setLocalizedTitle((String) localizedTitle); + Object icon = list.get(2); + pigeonResult.setIcon((String) icon); + return pigeonResult; + } + } + + public interface Result { + @SuppressWarnings("UnknownNullness") + void success(T result); + + void error(@NonNull Throwable error); + } + + private static class AndroidQuickActionsApiCodec extends StandardMessageCodec { + public static final AndroidQuickActionsApiCodec INSTANCE = new AndroidQuickActionsApiCodec(); + + private AndroidQuickActionsApiCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return ShortcutItemMessage.fromList((ArrayList) readValue(buffer)); + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof ShortcutItemMessage) { + stream.write(128); + writeValue(stream, ((ShortcutItemMessage) value).toList()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface AndroidQuickActionsApi { + /** Checks for, and returns the action that launched the app. */ + @Nullable + String getLaunchAction(); + /** Sets the dynamic shortcuts for the app. */ + void setShortcutItems( + @NonNull List itemsList, @NonNull Result result); + /** Removes all dynamic shortcuts. */ + void clearShortcutItems(); + + /** The codec used by AndroidQuickActionsApi. */ + static @NonNull MessageCodec getCodec() { + return AndroidQuickActionsApiCodec.INSTANCE; + } + /** + * Sets up an instance of `AndroidQuickActionsApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup( + @NonNull BinaryMessenger binaryMessenger, @Nullable AndroidQuickActionsApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.quick_actions_android.AndroidQuickActionsApi.getLaunchAction", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + String output = api.getLaunchAction(); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.quick_actions_android.AndroidQuickActionsApi.setShortcutItems", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + List itemsListArg = (List) args.get(0); + Result resultCallback = + new Result() { + public void success(Void result) { + wrapped.add(0, null); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.setShortcutItems(itemsListArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.quick_actions_android.AndroidQuickActionsApi.clearShortcutItems", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.clearShortcutItems(); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class AndroidQuickActionsFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public AndroidQuickActionsFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by AndroidQuickActionsFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** Sends a string representing a shortcut from the native platform to the app. */ + public void launchAction(@NonNull String actionArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.quick_actions_android.AndroidQuickActionsFlutterApi.launchAction", + getCodec()); + channel.send( + new ArrayList(Collections.singletonList(actionArg)), + channelReply -> callback.reply(null)); + } + } +} diff --git a/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java deleted file mode 100644 index c6a0db5d0b..0000000000 --- a/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java +++ /dev/null @@ -1,180 +0,0 @@ -// 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.quickactions; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager; -import android.content.res.Resources; -import android.graphics.drawable.Icon; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import androidx.annotation.NonNull; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.Executor; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { - protected static final String EXTRA_ACTION = "some unique action key"; - - private final Context context; - private Activity activity; - - MethodCallHandlerImpl(Context context, Activity activity) { - this.context = context; - this.activity = activity; - } - - void setActivity(Activity activity) { - this.activity = activity; - } - - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { - // We already know that this functionality does not work for anything - // lower than API 25 so we chose not to return error. Instead we do nothing. - result.success(null); - return; - } - ShortcutManager shortcutManager = - (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); - switch (call.method) { - case "setShortcutItems": - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) { - List> serializedShortcuts = Objects.requireNonNull(call.arguments()); - List shortcuts = deserializeShortcuts(serializedShortcuts); - - Executor uiThreadExecutor = new UiThreadExecutor(); - ThreadPoolExecutor executor = - new ThreadPoolExecutor(0, 1, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); - - executor.execute( - () -> { - boolean dynamicShortcutsSet = false; - try { - shortcutManager.setDynamicShortcuts(shortcuts); - dynamicShortcutsSet = true; - } catch (Exception e) { - // Leave dynamicShortcutsSet as false - } - - final boolean didSucceed = dynamicShortcutsSet; - - // TODO(camsim99): Move re-dispatch below to background thread when Flutter 2.8+ is - // stable. - uiThreadExecutor.execute( - () -> { - if (didSucceed) { - result.success(null); - } else { - result.error( - "quick_action_setshortcutitems_failure", - "Exception thrown when setting dynamic shortcuts", - null); - } - }); - }); - } - return; - case "clearShortcutItems": - shortcutManager.removeAllDynamicShortcuts(); - break; - case "getLaunchAction": - if (activity == null) { - result.error( - "quick_action_getlaunchaction_no_activity", - "There is no activity available when launching action", - null); - return; - } - final Intent intent = activity.getIntent(); - final String launchAction = intent.getStringExtra(EXTRA_ACTION); - if (launchAction != null && !launchAction.isEmpty()) { - shortcutManager.reportShortcutUsed(launchAction); - intent.removeExtra(EXTRA_ACTION); - } - result.success(launchAction); - return; - default: - result.notImplemented(); - return; - } - result.success(null); - } - - @TargetApi(Build.VERSION_CODES.N_MR1) - private List deserializeShortcuts(List> shortcuts) { - final List shortcutInfos = new ArrayList<>(); - - for (Map shortcut : shortcuts) { - final String icon = shortcut.get("icon"); - final String type = shortcut.get("type"); - final String title = shortcut.get("localizedTitle"); - final ShortcutInfo.Builder shortcutBuilder = new ShortcutInfo.Builder(context, type); - - final int resourceId = loadResourceId(context, icon); - final Intent intent = getIntentToOpenMainActivity(type); - - if (resourceId > 0) { - shortcutBuilder.setIcon(Icon.createWithResource(context, resourceId)); - } - - final ShortcutInfo shortcutInfo = - shortcutBuilder.setLongLabel(title).setShortLabel(title).setIntent(intent).build(); - shortcutInfos.add(shortcutInfo); - } - return shortcutInfos; - } - - // This method requires doing dynamic resource lookup, which is a discouraged API. - @SuppressWarnings("DiscouragedApi") - private int loadResourceId(Context context, String icon) { - if (icon == null) { - return 0; - } - final String packageName = context.getPackageName(); - final Resources res = context.getResources(); - final int resourceId = res.getIdentifier(icon, "drawable", packageName); - - if (resourceId == 0) { - return res.getIdentifier(icon, "mipmap", packageName); - } else { - return resourceId; - } - } - - private Intent getIntentToOpenMainActivity(String type) { - final String packageName = context.getPackageName(); - - return context - .getPackageManager() - .getLaunchIntentForPackage(packageName) - .setAction(Intent.ACTION_RUN) - .putExtra(EXTRA_ACTION, type) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - } - - static class UiThreadExecutor implements Executor { - private final Handler handler = new Handler(Looper.getMainLooper()); - - @Override - public void execute(Runnable command) { - handler.post(command); - } - } -} diff --git a/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActions.java b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActions.java new file mode 100644 index 0000000000..1549cf143b --- /dev/null +++ b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActions.java @@ -0,0 +1,193 @@ +// 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.quickactions; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.content.res.Resources; +import android.graphics.drawable.Icon; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.ChecksSdkIntAtLeast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugins.quickactions.Messages.AndroidQuickActionsApi; +import io.flutter.plugins.quickactions.Messages.FlutterError; +import io.flutter.plugins.quickactions.Messages.Result; +import io.flutter.plugins.quickactions.Messages.ShortcutItemMessage; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +final class QuickActions implements AndroidQuickActionsApi { + protected static final String EXTRA_ACTION = "some unique action key"; + + private final Context context; + private Activity activity; + + QuickActions(Context context) { + this.context = context; + } + + void setActivity(Activity activity) { + this.activity = activity; + } + + public Activity getActivity() { + return this.activity; + } + + // Returns true when running on a version of Android that supports quick actions. + // When this returns false, methods should silently no-op, per the documented behavior (see README.md). + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.N_MR1) + boolean isVersionAllowed() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1; + } + + @Override + public void setShortcutItems( + @NonNull List itemsList, @NonNull Result result) { + if (!isVersionAllowed()) { + result.success(null); + return; + } + ShortcutManager shortcutManager = + (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); + List shortcuts = shortcutItemMessageToShortcutInfo(itemsList); + Executor uiThreadExecutor = new UiThreadExecutor(); + ThreadPoolExecutor executor = + new ThreadPoolExecutor(0, 1, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); + + executor.execute( + () -> { + boolean dynamicShortcutsSet = false; + try { + shortcutManager.setDynamicShortcuts(shortcuts); + dynamicShortcutsSet = true; + } catch (Exception e) { + // Leave dynamicShortcutsSet as false + } + + final boolean didSucceed = dynamicShortcutsSet; + + // TODO(camsim99): Investigate removing all of the executor logic in favor of background channels. + uiThreadExecutor.execute( + () -> { + if (didSucceed) { + result.success(null); + } else { + result.error( + new FlutterError( + "quick_action_setshortcutitems_failure", + "Exception thrown when setting dynamic shortcuts", + null)); + } + }); + }); + } + + @Override + public void clearShortcutItems() { + if (!isVersionAllowed()) { + return; + } + ShortcutManager shortcutManager = + (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); + shortcutManager.removeAllDynamicShortcuts(); + } + + @Override + public @Nullable String getLaunchAction() { + if (!isVersionAllowed()) { + return null; + } + ShortcutManager shortcutManager = + (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); + if (activity == null) { + throw new FlutterError( + "quick_action_getlaunchaction_no_activity", + "There is no activity available when launching action", + null); + } + final Intent intent = activity.getIntent(); + final String launchAction = intent.getStringExtra(EXTRA_ACTION); + if (launchAction != null && !launchAction.isEmpty()) { + shortcutManager.reportShortcutUsed(launchAction); + intent.removeExtra(EXTRA_ACTION); + } + return launchAction; + } + + @TargetApi(Build.VERSION_CODES.N_MR1) + private List shortcutItemMessageToShortcutInfo( + @NonNull List shortcuts) { + final List shortcutInfos = new ArrayList<>(); + + for (ShortcutItemMessage shortcut : shortcuts) { + final String icon = shortcut.getIcon(); + final String type = shortcut.getType(); + final String title = shortcut.getLocalizedTitle(); + final ShortcutInfo.Builder shortcutBuilder = new ShortcutInfo.Builder(context, type); + + final int resourceId = loadResourceId(context, icon); + final Intent intent = getIntentToOpenMainActivity(type); + + if (resourceId > 0) { + shortcutBuilder.setIcon(Icon.createWithResource(context, resourceId)); + } + + final ShortcutInfo shortcutInfo = + shortcutBuilder.setLongLabel(title).setShortLabel(title).setIntent(intent).build(); + shortcutInfos.add(shortcutInfo); + } + return shortcutInfos; + } + + // This method requires doing dynamic resource lookup, which is a discouraged API. + @SuppressWarnings("DiscouragedApi") + private int loadResourceId(Context context, String icon) { + if (icon == null) { + return 0; + } + final String packageName = context.getPackageName(); + final Resources res = context.getResources(); + final int resourceId = res.getIdentifier(icon, "drawable", packageName); + + if (resourceId == 0) { + return res.getIdentifier(icon, "mipmap", packageName); + } else { + return resourceId; + } + } + + private Intent getIntentToOpenMainActivity(String type) { + final String packageName = context.getPackageName(); + + return context + .getPackageManager() + .getLaunchIntentForPackage(packageName) + .setAction(Intent.ACTION_RUN) + .putExtra(EXTRA_ACTION, type) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + } + + static class UiThreadExecutor implements Executor { + private final Handler handler = new Handler(Looper.getMainLooper()); + + @Override + public void execute(Runnable command) { + handler.post(command); + } + } +} diff --git a/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java index b1f78447af..fd0184302a 100644 --- a/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java +++ b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java @@ -9,23 +9,22 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ShortcutManager; import android.os.Build; +import android.util.Log; import androidx.annotation.ChecksSdkIntAtLeast; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.PluginRegistry.NewIntentListener; +import io.flutter.plugins.quickactions.Messages.AndroidQuickActionsFlutterApi; /** QuickActionsPlugin */ public class QuickActionsPlugin implements FlutterPlugin, ActivityAware, NewIntentListener { - private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions_android"; + private static final String TAG = "QuickActionsAndroid"; - private MethodChannel channel; - private MethodCallHandlerImpl handler; - private Activity activity; + private QuickActions quickActions; + private AndroidQuickActionsFlutterApi quickActionsFlutterApi; private final @NonNull AndroidSdkChecker sdkChecker; // Interface for an injectable SDK version checker. @@ -52,31 +51,40 @@ public class QuickActionsPlugin implements FlutterPlugin, ActivityAware, NewInte @SuppressWarnings("deprecation") public static void registerWith( @NonNull io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - final QuickActionsPlugin plugin = new QuickActionsPlugin(); - plugin.setupChannel(registrar.messenger(), registrar.context(), registrar.activity()); + QuickActions quickActions = new QuickActions(registrar.context()); + quickActions.setActivity(registrar.activity()); + Messages.AndroidQuickActionsApi.setup(registrar.messenger(), quickActions); } @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { - setupChannel(binding.getBinaryMessenger(), binding.getApplicationContext(), null); + this.quickActions = new QuickActions(binding.getApplicationContext()); + Messages.AndroidQuickActionsApi.setup(binding.getBinaryMessenger(), quickActions); + this.quickActionsFlutterApi = new AndroidQuickActionsFlutterApi(binding.getBinaryMessenger()); } @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - teardownChannel(); + Messages.AndroidQuickActionsApi.setup(binding.getBinaryMessenger(), null); + this.quickActions = null; } @Override public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { - activity = binding.getActivity(); - handler.setActivity(activity); + if (this.quickActions == null) { + Log.wtf(TAG, "quickActions was never set."); + return; + } + + Activity activity = binding.getActivity(); + this.quickActions.setActivity(activity); binding.addOnNewIntentListener(this); onNewIntent(activity.getIntent()); } @Override public void onDetachedFromActivity() { - handler.setActivity(null); + quickActions.setActivity(null); } @Override @@ -96,27 +104,20 @@ public class QuickActionsPlugin implements FlutterPlugin, ActivityAware, NewInte if (!sdkChecker.sdkIsAtLeast(Build.VERSION_CODES.N_MR1)) { return false; } + Activity activity = this.quickActions.getActivity(); // Notify the Dart side if the launch intent has the intent extra relevant to quick actions. - if (intent.hasExtra(MethodCallHandlerImpl.EXTRA_ACTION) && channel != null) { + if (intent.hasExtra(QuickActions.EXTRA_ACTION) && activity != null) { Context context = activity.getApplicationContext(); ShortcutManager shortcutManager = (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); - String shortcutId = intent.getStringExtra(MethodCallHandlerImpl.EXTRA_ACTION); - channel.invokeMethod("launch", shortcutId); + String shortcutId = intent.getStringExtra(QuickActions.EXTRA_ACTION); + quickActionsFlutterApi.launchAction( + shortcutId, + value -> { + // noop + }); shortcutManager.reportShortcutUsed(shortcutId); } return false; } - - private void setupChannel(BinaryMessenger messenger, Context context, Activity activity) { - channel = new MethodChannel(messenger, CHANNEL_ID); - handler = new MethodCallHandlerImpl(context, activity); - channel.setMethodCallHandler(handler); - } - - private void teardownChannel() { - channel.setMethodCallHandler(null); - channel = null; - handler = null; - } } diff --git a/packages/quick_actions/quick_actions_android/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java b/packages/quick_actions/quick_actions_android/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java index ebee86645f..6c2e963a80 100644 --- a/packages/quick_actions/quick_actions_android/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java +++ b/packages/quick_actions/quick_actions_android/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java @@ -4,11 +4,9 @@ package io.flutter.plugins.quickactions; -import static io.flutter.plugins.quickactions.MethodCallHandlerImpl.EXTRA_ACTION; -import static org.junit.Assert.assertEquals; +import static io.flutter.plugins.quickactions.QuickActions.EXTRA_ACTION; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -21,15 +19,12 @@ import androidx.annotation.Nullable; import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.StandardMethodCodec; -import java.lang.reflect.Field; import java.nio.ByteBuffer; import org.junit.Test; public class QuickActionsTest { private static class TestBinaryMessenger implements BinaryMessenger { - public MethodCall lastMethodCall; + public boolean launchActionCalled; @Override public void send(@NonNull String channel, @Nullable ByteBuffer message) { @@ -41,9 +36,8 @@ public class QuickActionsTest { @NonNull String channel, @Nullable ByteBuffer message, @Nullable final BinaryReply callback) { - if (channel.equals("plugins.flutter.io/quick_actions_android")) { - lastMethodCall = - StandardMethodCodec.INSTANCE.decodeMethodCall((ByteBuffer) message.position(0)); + if (channel.contains("launchAction")) { + launchActionCalled = true; } } @@ -75,9 +69,6 @@ public class QuickActionsTest { final QuickActionsPlugin plugin = new QuickActionsPlugin((version) -> SUPPORTED_BUILD >= version); setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin); - Field handler = plugin.getClass().getDeclaredField("handler"); - handler.setAccessible(true); - handler.set(plugin, mock(MethodCallHandlerImpl.class)); final Intent mockIntent = createMockIntentWithQuickActionExtra(); final Activity mockMainActivity = mock(Activity.class); when(mockMainActivity.getIntent()).thenReturn(mockIntent); @@ -93,9 +84,7 @@ public class QuickActionsTest { plugin.onAttachedToActivity(mockActivityPluginBinding); // Assert - assertNotNull(testBinaryMessenger.lastMethodCall); - assertEquals(testBinaryMessenger.lastMethodCall.method, "launch"); - assertEquals(testBinaryMessenger.lastMethodCall.arguments, SHORTCUT_TYPE); + assertTrue(testBinaryMessenger.launchActionCalled); } @Test @@ -111,7 +100,7 @@ public class QuickActionsTest { final boolean onNewIntentReturn = plugin.onNewIntent(mockIntent); // Assert - assertNull(testBinaryMessenger.lastMethodCall); + assertFalse(testBinaryMessenger.launchActionCalled); assertFalse(onNewIntentReturn); } @@ -137,9 +126,7 @@ public class QuickActionsTest { final boolean onNewIntentReturn = plugin.onNewIntent(mockIntent); // Assert - assertNotNull(testBinaryMessenger.lastMethodCall); - assertEquals(testBinaryMessenger.lastMethodCall.method, "launch"); - assertEquals(testBinaryMessenger.lastMethodCall.arguments, SHORTCUT_TYPE); + assertTrue(testBinaryMessenger.launchActionCalled); assertFalse(onNewIntentReturn); } diff --git a/packages/quick_actions/quick_actions_android/lib/quick_actions_android.dart b/packages/quick_actions/quick_actions_android/lib/quick_actions_android.dart index 99a54e9866..811f7ffed8 100644 --- a/packages/quick_actions/quick_actions_android/lib/quick_actions_android.dart +++ b/packages/quick_actions/quick_actions_android/lib/quick_actions_android.dart @@ -3,54 +3,62 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; import 'package:quick_actions_platform_interface/quick_actions_platform_interface.dart'; +import 'src/messages.g.dart'; + export 'package:quick_actions_platform_interface/types/types.dart'; -const MethodChannel _channel = - MethodChannel('plugins.flutter.io/quick_actions_android'); +late QuickActionHandler _handler; -/// An implementation of [QuickActionsPlatform] that for Android. +/// An implementation of [QuickActionsPlatform] for Android. class QuickActionsAndroid extends QuickActionsPlatform { + /// Creates a new plugin implementation instance. + QuickActionsAndroid({ + @visibleForTesting AndroidQuickActionsApi? api, + }) : _hostApi = api ?? AndroidQuickActionsApi(); + + final AndroidQuickActionsApi _hostApi; + /// Registers this class as the default instance of [QuickActionsPlatform]. static void registerWith() { QuickActionsPlatform.instance = QuickActionsAndroid(); } - /// The MethodChannel that is being used by this implementation of the plugin. - @visibleForTesting - MethodChannel get channel => _channel; - @override Future initialize(QuickActionHandler handler) async { - channel.setMethodCallHandler((MethodCall call) async { - assert(call.method == 'launch'); - handler(call.arguments as String); - }); - final String? action = - await channel.invokeMethod('getLaunchAction'); + final _QuickActionHandlerApi quickActionsHandlerApi = + _QuickActionHandlerApi(); + AndroidQuickActionsFlutterApi.setup(quickActionsHandlerApi); + _handler = handler; + final String? action = await _hostApi.getLaunchAction(); if (action != null) { - handler(action); + _handler(action); } } @override Future setShortcutItems(List items) async { - final List> itemsList = - items.map(_serializeItem).toList(); - await channel.invokeMethod('setShortcutItems', itemsList); + await _hostApi.setShortcutItems( + items.map(_shortcutItemToShortcutItemMessage).toList(), + ); } @override - Future clearShortcutItems() => - channel.invokeMethod('clearShortcutItems'); + Future clearShortcutItems() => _hostApi.clearShortcutItems(); - Map _serializeItem(ShortcutItem item) { - return { - 'type': item.type, - 'localizedTitle': item.localizedTitle, - 'icon': item.icon, - }; + ShortcutItemMessage _shortcutItemToShortcutItemMessage(ShortcutItem item) { + return ShortcutItemMessage( + type: item.type, + localizedTitle: item.localizedTitle, + icon: item.icon, + ); + } +} + +class _QuickActionHandlerApi extends AndroidQuickActionsFlutterApi { + @override + void launchAction(String action) { + _handler(action); } } diff --git a/packages/quick_actions/quick_actions_android/lib/src/messages.g.dart b/packages/quick_actions/quick_actions_android/lib/src/messages.g.dart new file mode 100644 index 0000000000..2e4fa9a188 --- /dev/null +++ b/packages/quick_actions/quick_actions_android/lib/src/messages.g.dart @@ -0,0 +1,183 @@ +// 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. +// Autogenerated from Pigeon (v11.0.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +/// Home screen quick-action shortcut item. +class ShortcutItemMessage { + ShortcutItemMessage({ + required this.type, + required this.localizedTitle, + this.icon, + }); + + /// The identifier of this item; should be unique within the app. + String type; + + /// Localized title of the item. + String localizedTitle; + + /// Name of native resource to be displayed as the icon for this item. + String? icon; + + Object encode() { + return [ + type, + localizedTitle, + icon, + ]; + } + + static ShortcutItemMessage decode(Object result) { + result as List; + return ShortcutItemMessage( + type: result[0]! as String, + localizedTitle: result[1]! as String, + icon: result[2] as String?, + ); + } +} + +class _AndroidQuickActionsApiCodec extends StandardMessageCodec { + const _AndroidQuickActionsApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is ShortcutItemMessage) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return ShortcutItemMessage.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class AndroidQuickActionsApi { + /// Constructor for [AndroidQuickActionsApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + AndroidQuickActionsApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _AndroidQuickActionsApiCodec(); + + /// Checks for, and returns the action that launched the app. + Future getLaunchAction() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.quick_actions_android.AndroidQuickActionsApi.getLaunchAction', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return (replyList[0] as String?); + } + } + + /// Sets the dynamic shortcuts for the app. + Future setShortcutItems( + List arg_itemsList) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.quick_actions_android.AndroidQuickActionsApi.setShortcutItems', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_itemsList]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + /// Removes all dynamic shortcuts. + Future clearShortcutItems() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.quick_actions_android.AndroidQuickActionsApi.clearShortcutItems', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } +} + +abstract class AndroidQuickActionsFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + /// Sends a string representing a shortcut from the native platform to the app. + void launchAction(String action); + + static void setup(AndroidQuickActionsFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.quick_actions_android.AndroidQuickActionsFlutterApi.launchAction', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.quick_actions_android.AndroidQuickActionsFlutterApi.launchAction was null.'); + final List args = (message as List?)!; + final String? arg_action = (args[0] as String?); + assert(arg_action != null, + 'Argument for dev.flutter.pigeon.quick_actions_android.AndroidQuickActionsFlutterApi.launchAction was null, expected non-null String.'); + api.launchAction(arg_action!); + return; + }); + } + } + } +} diff --git a/packages/quick_actions/quick_actions_android/pigeons/copyright.txt b/packages/quick_actions/quick_actions_android/pigeons/copyright.txt new file mode 100644 index 0000000000..1236b63caf --- /dev/null +++ b/packages/quick_actions/quick_actions_android/pigeons/copyright.txt @@ -0,0 +1,3 @@ +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. diff --git a/packages/quick_actions/quick_actions_android/pigeons/messages.dart b/packages/quick_actions/quick_actions_android/pigeons/messages.dart new file mode 100644 index 0000000000..5bc3e1e5e3 --- /dev/null +++ b/packages/quick_actions/quick_actions_android/pigeons/messages.dart @@ -0,0 +1,52 @@ +// 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. + +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + javaOut: + 'android/src/main/java/io/flutter/plugins/quickactions/Messages.java', + javaOptions: JavaOptions( + package: 'io.flutter.plugins.quickactions', + ), + copyrightHeader: 'pigeons/copyright.txt', +)) + +/// Home screen quick-action shortcut item. +class ShortcutItemMessage { + ShortcutItemMessage( + this.type, + this.localizedTitle, + this.icon, + ); + + /// The identifier of this item; should be unique within the app. + String type; + + /// Localized title of the item. + String localizedTitle; + + /// Name of native resource to be displayed as the icon for this item. + String? icon; +} + +@HostApi() +abstract class AndroidQuickActionsApi { + /// Checks for, and returns the action that launched the app. + String? getLaunchAction(); + + /// Sets the dynamic shortcuts for the app. + @async + void setShortcutItems(List itemsList); + + /// Removes all dynamic shortcuts. + void clearShortcutItems(); +} + +@FlutterApi() +abstract class AndroidQuickActionsFlutterApi { + /// Sends a string representing a shortcut from the native platform to the app. + void launchAction(String action); +} diff --git a/packages/quick_actions/quick_actions_android/pubspec.yaml b/packages/quick_actions/quick_actions_android/pubspec.yaml index 72e82477fe..64341e63be 100644 --- a/packages/quick_actions/quick_actions_android/pubspec.yaml +++ b/packages/quick_actions/quick_actions_android/pubspec.yaml @@ -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/packages/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: 1.0.8 +version: 1.0.9 environment: sdk: ">=2.19.0 <4.0.0" @@ -27,6 +27,7 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter + pigeon: ^11.0.1 plugin_platform_interface: ^2.1.2 topics: diff --git a/packages/quick_actions/quick_actions_android/test/quick_actions_android_test.dart b/packages/quick_actions/quick_actions_android/test/quick_actions_android_test.dart index 0a98f5d4e5..f0adcfe3a6 100644 --- a/packages/quick_actions/quick_actions_android/test/quick_actions_android_test.dart +++ b/packages/quick_actions/quick_actions_android/test/quick_actions_android_test.dart @@ -4,169 +4,104 @@ import 'dart:async'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:quick_actions_android/quick_actions_android.dart'; +import 'package:quick_actions_android/src/messages.g.dart'; import 'package:quick_actions_platform_interface/quick_actions_platform_interface.dart'; +const String LAUNCH_ACTION_STRING = 'aString'; + +/// Conversion tool to change [ShortcutItemMessage] back to [ShortcutItem] +ShortcutItem shortcutItemMessageToShortcutItem(ShortcutItemMessage item) { + return ShortcutItem( + type: item.type, + localizedTitle: item.localizedTitle, + icon: item.icon, + ); +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('$QuickActionsAndroid', () { - late List log; + final _FakeQuickActionsApi api = _FakeQuickActionsApi(); + final QuickActionsAndroid quickActions = QuickActionsAndroid(api: api); - setUp(() { - log = []; + test('registerWith() registers correct instance', () { + QuickActionsAndroid.registerWith(); + expect(QuickActionsPlatform.instance, isA()); + }); + + group('#initialize', () { + test('passes getLaunchAction on launch method', () { + quickActions.initialize((String type) {}); + + expect(api.getLaunchActionCalled, true); }); - QuickActionsAndroid buildQuickActionsPlugin() { - final QuickActionsAndroid quickActions = QuickActionsAndroid(); - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler(quickActions.channel, - (MethodCall methodCall) async { - log.add(methodCall); - return ''; - }); + test('initialize', () async { + final Completer quickActionsHandler = Completer(); + await quickActions.initialize((_) => quickActionsHandler.complete(true)); - return quickActions; - } - - test('registerWith() registers correct instance', () { - QuickActionsAndroid.registerWith(); - expect(QuickActionsPlatform.instance, isA()); - }); - - group('#initialize', () { - test('passes getLaunchAction on launch method', () { - final QuickActionsAndroid quickActions = buildQuickActionsPlugin(); - quickActions.initialize((String type) {}); - - expect( - log, - [ - isMethodCall('getLaunchAction', arguments: null), - ], - ); - }); - - test('initialize', () async { - final QuickActionsAndroid quickActions = buildQuickActionsPlugin(); - final Completer quickActionsHandler = Completer(); - await quickActions - .initialize((_) => quickActionsHandler.complete(true)); - expect( - log, - [ - isMethodCall('getLaunchAction', arguments: null), - ], - ); - log.clear(); - - expect(quickActionsHandler.future, completion(isTrue)); - }); - }); - - group('#setShortCutItems', () { - test('passes shortcutItem through channel', () { - final QuickActionsAndroid quickActions = buildQuickActionsPlugin(); - quickActions.initialize((String type) {}); - quickActions.setShortcutItems([ - const ShortcutItem( - type: 'test', localizedTitle: 'title', icon: 'icon.svg') - ]); - - expect( - log, - [ - isMethodCall('getLaunchAction', arguments: null), - isMethodCall('setShortcutItems', arguments: >[ - { - 'type': 'test', - 'localizedTitle': 'title', - 'icon': 'icon.svg', - } - ]), - ], - ); - }); - - test('setShortcutItems with demo data', () async { - const String type = 'type'; - const String localizedTitle = 'localizedTitle'; - const String icon = 'icon'; - final QuickActionsAndroid quickActions = buildQuickActionsPlugin(); - await quickActions.setShortcutItems( - const [ - ShortcutItem(type: type, localizedTitle: localizedTitle, icon: icon) - ], - ); - expect( - log, - [ - isMethodCall( - 'setShortcutItems', - arguments: >[ - { - 'type': type, - 'localizedTitle': localizedTitle, - 'icon': icon, - } - ], - ), - ], - ); - log.clear(); - }); - }); - - group('#clearShortCutItems', () { - test('send clearShortcutItems through channel', () { - final QuickActionsAndroid quickActions = buildQuickActionsPlugin(); - quickActions.initialize((String type) {}); - quickActions.clearShortcutItems(); - - expect( - log, - [ - isMethodCall('getLaunchAction', arguments: null), - isMethodCall('clearShortcutItems', arguments: null), - ], - ); - }); - - test('clearShortcutItems', () { - final QuickActionsAndroid quickActions = buildQuickActionsPlugin(); - quickActions.clearShortcutItems(); - expect( - log, - [ - isMethodCall('clearShortcutItems', arguments: null), - ], - ); - log.clear(); - }); + expect(quickActionsHandler.future, completion(isTrue)); }); }); - group('$ShortcutItem', () { - test('Shortcut item can be constructed', () { - const String type = 'type'; - const String localizedTitle = 'title'; - const String icon = 'foo'; + test('setShortCutItems', () async { + await quickActions.initialize((String type) {}); + const ShortcutItem item = + ShortcutItem(type: 'test', localizedTitle: 'title', icon: 'icon.svg'); + await quickActions.setShortcutItems([item]); - const ShortcutItem item = - ShortcutItem(type: type, localizedTitle: localizedTitle, icon: icon); + expect(api.items.first.type, item.type); + expect(api.items.first.localizedTitle, item.localizedTitle); + expect(api.items.first.icon, item.icon); + }); - expect(item.type, type); - expect(item.localizedTitle, localizedTitle); - expect(item.icon, icon); - }); + test('clearShortCutItems', () { + quickActions.initialize((String type) {}); + const ShortcutItem item = + ShortcutItem(type: 'test', localizedTitle: 'title', icon: 'icon.svg'); + quickActions.setShortcutItems([item]); + quickActions.clearShortcutItems(); + + expect(api.items.isEmpty, true); + }); + + test('Shortcut item can be constructed', () { + const String type = 'type'; + const String localizedTitle = 'title'; + const String icon = 'foo'; + + const ShortcutItem item = + ShortcutItem(type: type, localizedTitle: localizedTitle, icon: icon); + + expect(item.type, type); + expect(item.localizedTitle, localizedTitle); + expect(item.icon, icon); }); } -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; +class _FakeQuickActionsApi implements AndroidQuickActionsApi { + List items = []; + bool getLaunchActionCalled = false; + + @override + Future clearShortcutItems() async { + items = []; + return; + } + + @override + Future getLaunchAction() async { + getLaunchActionCalled = true; + return LAUNCH_ACTION_STRING; + } + + @override + Future setShortcutItems(List itemsList) async { + await clearShortcutItems(); + for (final ShortcutItemMessage? element in itemsList) { + items.add(shortcutItemMessageToShortcutItem(element!)); + } + } +}