[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
This commit is contained in:
Tarrin Neal
2023-10-16 17:42:54 -07:00
committed by GitHub
parent b96a6dae0c
commit e8127e76d5
12 changed files with 912 additions and 403 deletions

View File

@ -1,3 +1,7 @@
## 1.0.9
* Changes method channels to pigeon.
## 1.0.8
* Adds pub topics to package metadata.

View File

@ -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<Object> wrapError(@NonNull Throwable exception) {
ArrayList<Object> errorList = new ArrayList<Object>(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.
*
* <p>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<Object> toList() {
ArrayList<Object> toListResult = new ArrayList<Object>(3);
toListResult.add(type);
toListResult.add(localizedTitle);
toListResult.add(icon);
return toListResult;
}
static @NonNull ShortcutItemMessage fromList(@NonNull ArrayList<Object> 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<T> {
@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<Object>) 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<ShortcutItemMessage> itemsList, @NonNull Result<Void> result);
/** Removes all dynamic shortcuts. */
void clearShortcutItems();
/** The codec used by AndroidQuickActionsApi. */
static @NonNull MessageCodec<Object> 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<Object> channel =
new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.quick_actions_android.AndroidQuickActionsApi.getLaunchAction",
getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
try {
String output = api.getLaunchAction();
wrapped.add(0, output);
} catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.quick_actions_android.AndroidQuickActionsApi.setShortcutItems",
getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
List<ShortcutItemMessage> itemsListArg = (List<ShortcutItemMessage>) args.get(0);
Result<Void> resultCallback =
new Result<Void>() {
public void success(Void result) {
wrapped.add(0, null);
reply.reply(wrapped);
}
public void error(Throwable error) {
ArrayList<Object> wrappedError = wrapError(error);
reply.reply(wrappedError);
}
};
api.setShortcutItems(itemsListArg, resultCallback);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.quick_actions_android.AndroidQuickActionsApi.clearShortcutItems",
getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
try {
api.clearShortcutItems();
wrapped.add(0, null);
} catch (Throwable exception) {
ArrayList<Object> 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<T> {
void reply(T reply);
}
/** The codec used by AndroidQuickActionsFlutterApi. */
static @NonNull MessageCodec<Object> 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<Void> callback) {
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.quick_actions_android.AndroidQuickActionsFlutterApi.launchAction",
getCodec());
channel.send(
new ArrayList<Object>(Collections.singletonList(actionArg)),
channelReply -> callback.reply(null));
}
}
}

View File

@ -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<Map<String, String>> serializedShortcuts = Objects.requireNonNull(call.arguments());
List<ShortcutInfo> 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<ShortcutInfo> deserializeShortcuts(List<Map<String, String>> shortcuts) {
final List<ShortcutInfo> shortcutInfos = new ArrayList<>();
for (Map<String, String> 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);
}
}
}

View File

@ -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<ShortcutItemMessage> itemsList, @NonNull Result<Void> result) {
if (!isVersionAllowed()) {
result.success(null);
return;
}
ShortcutManager shortcutManager =
(ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
List<ShortcutInfo> 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<ShortcutInfo> shortcutItemMessageToShortcutInfo(
@NonNull List<ShortcutItemMessage> shortcuts) {
final List<ShortcutInfo> 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);
}
}
}

View File

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

View File

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

View File

@ -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<void> initialize(QuickActionHandler handler) async {
channel.setMethodCallHandler((MethodCall call) async {
assert(call.method == 'launch');
handler(call.arguments as String);
});
final String? action =
await channel.invokeMethod<String?>('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<void> setShortcutItems(List<ShortcutItem> items) async {
final List<Map<String, String?>> itemsList =
items.map(_serializeItem).toList();
await channel.invokeMethod<void>('setShortcutItems', itemsList);
await _hostApi.setShortcutItems(
items.map(_shortcutItemToShortcutItemMessage).toList(),
);
}
@override
Future<void> clearShortcutItems() =>
channel.invokeMethod<void>('clearShortcutItems');
Future<void> clearShortcutItems() => _hostApi.clearShortcutItems();
Map<String, String?> _serializeItem(ShortcutItem item) {
return <String, String?>{
'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);
}
}

View File

@ -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 <Object?>[
type,
localizedTitle,
icon,
];
}
static ShortcutItemMessage decode(Object result) {
result as List<Object?>;
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<Object?> codec = _AndroidQuickActionsApiCodec();
/// Checks for, and returns the action that launched the app.
Future<String?> getLaunchAction() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.quick_actions_android.AndroidQuickActionsApi.getLaunchAction',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(null) as List<Object?>?;
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<void> setShortcutItems(
List<ShortcutItemMessage?> arg_itemsList) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.quick_actions_android.AndroidQuickActionsApi.setShortcutItems',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_itemsList]) as List<Object?>?;
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<void> clearShortcutItems() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.quick_actions_android.AndroidQuickActionsApi.clearShortcutItems',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(null) as List<Object?>?;
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<Object?> 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<Object?> channel = BasicMessageChannel<Object?>(
'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<Object?> args = (message as List<Object?>?)!;
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;
});
}
}
}
}

View File

@ -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.

View File

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

View File

@ -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:

View File

@ -4,33 +4,27 @@
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<MethodCall> log;
setUp(() {
log = <MethodCall>[];
});
QuickActionsAndroid buildQuickActionsPlugin() {
final QuickActionsAndroid quickActions = QuickActionsAndroid();
_ambiguate(TestDefaultBinaryMessengerBinding.instance)!
.defaultBinaryMessenger
.setMockMethodCallHandler(quickActions.channel,
(MethodCall methodCall) async {
log.add(methodCall);
return '';
});
return quickActions;
}
final _FakeQuickActionsApi api = _FakeQuickActionsApi();
final QuickActionsAndroid quickActions = QuickActionsAndroid(api: api);
test('registerWith() registers correct instance', () {
QuickActionsAndroid.registerWith();
@ -39,117 +33,40 @@ void main() {
group('#initialize', () {
test('passes getLaunchAction on launch method', () {
final QuickActionsAndroid quickActions = buildQuickActionsPlugin();
quickActions.initialize((String type) {});
expect(
log,
<Matcher>[
isMethodCall('getLaunchAction', arguments: null),
],
);
expect(api.getLaunchActionCalled, true);
});
test('initialize', () async {
final QuickActionsAndroid quickActions = buildQuickActionsPlugin();
final Completer<bool> quickActionsHandler = Completer<bool>();
await quickActions
.initialize((_) => quickActionsHandler.complete(true));
expect(
log,
<Matcher>[
isMethodCall('getLaunchAction', arguments: null),
],
);
log.clear();
await quickActions.initialize((_) => quickActionsHandler.complete(true));
expect(quickActionsHandler.future, completion(isTrue));
});
});
group('#setShortCutItems', () {
test('passes shortcutItem through channel', () {
final QuickActionsAndroid quickActions = buildQuickActionsPlugin();
quickActions.initialize((String type) {});
quickActions.setShortcutItems(<ShortcutItem>[
const ShortcutItem(
type: 'test', localizedTitle: 'title', icon: 'icon.svg')
]);
expect(
log,
<Matcher>[
isMethodCall('getLaunchAction', arguments: null),
isMethodCall('setShortcutItems', arguments: <Map<String, String>>[
<String, String>{
'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>[
ShortcutItem(type: type, localizedTitle: localizedTitle, icon: icon)
],
);
expect(
log,
<Matcher>[
isMethodCall(
'setShortcutItems',
arguments: <Map<String, String>>[
<String, String>{
'type': type,
'localizedTitle': localizedTitle,
'icon': icon,
}
],
),
],
);
log.clear();
});
});
group('#clearShortCutItems', () {
test('send clearShortcutItems through channel', () {
final QuickActionsAndroid quickActions = buildQuickActionsPlugin();
test('setShortCutItems', () async {
await quickActions.initialize((String type) {});
const ShortcutItem item =
ShortcutItem(type: 'test', localizedTitle: 'title', icon: 'icon.svg');
await quickActions.setShortcutItems(<ShortcutItem>[item]);
expect(api.items.first.type, item.type);
expect(api.items.first.localizedTitle, item.localizedTitle);
expect(api.items.first.icon, item.icon);
});
test('clearShortCutItems', () {
quickActions.initialize((String type) {});
const ShortcutItem item =
ShortcutItem(type: 'test', localizedTitle: 'title', icon: 'icon.svg');
quickActions.setShortcutItems(<ShortcutItem>[item]);
quickActions.clearShortcutItems();
expect(
log,
<Matcher>[
isMethodCall('getLaunchAction', arguments: null),
isMethodCall('clearShortcutItems', arguments: null),
],
);
expect(api.items.isEmpty, true);
});
test('clearShortcutItems', () {
final QuickActionsAndroid quickActions = buildQuickActionsPlugin();
quickActions.clearShortcutItems();
expect(
log,
<Matcher>[
isMethodCall('clearShortcutItems', arguments: null),
],
);
log.clear();
});
});
});
group('$ShortcutItem', () {
test('Shortcut item can be constructed', () {
const String type = 'type';
const String localizedTitle = 'title';
@ -162,11 +79,29 @@ void main() {
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>(T? value) => value;
class _FakeQuickActionsApi implements AndroidQuickActionsApi {
List<ShortcutItem> items = <ShortcutItem>[];
bool getLaunchActionCalled = false;
@override
Future<void> clearShortcutItems() async {
items = <ShortcutItem>[];
return;
}
@override
Future<String?> getLaunchAction() async {
getLaunchActionCalled = true;
return LAUNCH_ACTION_STRING;
}
@override
Future<void> setShortcutItems(List<ShortcutItemMessage?> itemsList) async {
await clearShortcutItems();
for (final ShortcutItemMessage? element in itemsList) {
items.add(shortcutItemMessageToShortcutItem(element!));
}
}
}