From 5a82ef01511ecbaeac0f1d759e484924bac140d7 Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Thu, 3 Oct 2019 11:37:47 -0700 Subject: [PATCH 1/7] Refactor the existing logic Split out the business logic into a new IntentSender class. Split out the MethodCall parsing into MethodCallHandlerImpl. --- packages/android_intent/android/build.gradle | 4 + .../plugins/androidintent/IntentSender.java | 116 ++++++++++++++ .../androidintent/MethodCallHandlerImpl.java | 132 +++++++++++++++ .../androidintent/AndroidIntentPlugin.java | 150 +----------------- 4 files changed, 257 insertions(+), 145 deletions(-) create mode 100644 packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/IntentSender.java create mode 100644 packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/MethodCallHandlerImpl.java diff --git a/packages/android_intent/android/build.gradle b/packages/android_intent/android/build.gradle index 8b21464f4b19..19283348a48b 100644 --- a/packages/android_intent/android/build.gradle +++ b/packages/android_intent/android/build.gradle @@ -45,3 +45,7 @@ android { disable 'InvalidPackage' } } + +dependencies { + compileOnly 'androidx.annotation:annotation:1.0.0' +} diff --git a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/IntentSender.java b/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/IntentSender.java new file mode 100644 index 000000000000..25273146b48a --- /dev/null +++ b/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/IntentSender.java @@ -0,0 +1,116 @@ +package dev.flutter.plugins.androidintent; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import androidx.annotation.Nullable; + +/** Forms and launches intents. */ +public class IntentSender { + private static final String TAG = "IntentSender"; + + private @Nullable Activity activity; + private @Nullable Context applicationContext; + + /** + * Caches the given {@code activity} and {@code applicationContext} to use for sending intents + * later. + * + *

Either may be null initially, but at least {@code applicationContext} should be set before + * calling {@link #send}. + * + *

See also {@link #setActivity}, {@link #setApplicationContext}, and {@link #send}. + */ + public IntentSender(@Nullable Activity activity, @Nullable Context applicationContext) { + this.activity = activity; + this.applicationContext = applicationContext; + } + + /** + * Creates and launches an intent with the given params using the cached {@link Activity} and + * {@link Context}. + * + *

This will fail to create and send the intent if {@code applicationContext} hasn't been set + * at the time of calling. + * + *

This uses {@code activity} to start the intent whenever it's not null. Otherwise it falls + * back to {@code applicationContext} and adds {@link Intent#FLAG_ACTIVITY_NEW_TASK} to the intent + * before launching it. + * + * @param action the Intent action, such as {@code ACTION_VIEW}. + * @param flags forwarded to {@link Intent#addFlags(int)} if non-null. + * @param category forwarded to {@link Intent#addCategory(String)} if non-null. + * @param data forwarded to {@link Intent#setData(Uri)} if non-null. + * @param arguments forwarded to {@link Intent#putExtras(Bundle)} if non-null. + * @param packageName forwarded to {@link Intent#setPackage(String)} if non-null. This is forced + * to null if it can't be resolved. + * @param componentName forwarded to {@link Intent#setComponent(ComponentName)} if non-null. + */ + void send( + String action, + @Nullable Integer flags, + @Nullable String category, + @Nullable Uri data, + @Nullable Bundle arguments, + @Nullable String packageName, + @Nullable ComponentName componentName) { + if (applicationContext == null) { + Log.wtf(TAG, "Trying to send an intent before the applicationContext was initialized."); + return; + } + + Intent intent = new Intent(action); + + if (flags != null) { + intent.addFlags(flags); + } + if (!TextUtils.isEmpty(category)) { + intent.addCategory(category); + } + if (data != null) { + intent.setData(data); + } + if (arguments != null) { + intent.putExtras(arguments); + } + if (!TextUtils.isEmpty(packageName)) { + intent.setPackage(packageName); + if (intent.resolveActivity(applicationContext.getPackageManager()) == null) { + Log.i(TAG, "Cannot resolve explicit intent - ignoring package"); + intent.setPackage(null); + } + } + if (componentName != null) { + intent.setComponent(componentName); + } + + Log.v(TAG, "Sending intent " + intent); + if (activity != null) { + activity.startActivity(intent); + } else { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + applicationContext.startActivity(intent); + } + } + + /** Caches the given {@code activity} to use for {@link #send}. */ + void setActivity(@Nullable Activity activity) { + this.activity = activity; + } + + /** + * Caches the given {@code applicationContext} to use for {@link #send}. + * + *

Also resets the cached {@code activity} to null, since the given activity should no longer + * be valid after applicationContext changes. + */ + void setApplicationContext(@Nullable Context applicationContext) { + this.applicationContext = applicationContext; + setActivity(null); + } +} diff --git a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/MethodCallHandlerImpl.java b/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/MethodCallHandlerImpl.java new file mode 100644 index 000000000000..4b29bdb960db --- /dev/null +++ b/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/MethodCallHandlerImpl.java @@ -0,0 +1,132 @@ +package dev.flutter.plugins.androidintent; + +import android.content.ComponentName; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; +import android.text.TextUtils; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import java.util.ArrayList; +import java.util.Map; + +/** Forwards incoming {@link MethodCall}s to {@link IntentSender#send}. */ +public class MethodCallHandlerImpl implements MethodCallHandler { + private final IntentSender sender; + + /** + * Uses the given {@code sender} for all incoming calls. + * + *

This assumes that the sender's context and activity state are managed elsewhere and + * correctly initialized before being sent here. + */ + public MethodCallHandlerImpl(IntentSender sender) { + this.sender = sender; + } + + /** + * Parses the incoming call and forwards it to the cached {@link IntentSender}. + * + *

Always calls {@code result#success}. + */ + @Override + public void onMethodCall(MethodCall call, Result result) { + String action = convertAction((String) call.argument("action")); + Integer flags = call.argument("flags"); + String category = call.argument("category"); + Uri data = call.argument("data") != null ? Uri.parse((String) call.argument("data")) : null; + Bundle arguments = convertArguments((Map) call.argument("arguments")); + String packageName = call.argument("package"); + ComponentName componentName = + (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty((String) call.argument("component"))) + ? new ComponentName(packageName, (String) call.argument("component")) + : null; + + sender.send(action, flags, category, data, arguments, packageName, componentName); + + result.success(null); + } + + private static String convertAction(String action) { + switch (action) { + case "action_view": + return Intent.ACTION_VIEW; + case "action_voice": + return Intent.ACTION_VOICE_COMMAND; + case "settings": + return Settings.ACTION_SETTINGS; + case "action_location_source_settings": + return Settings.ACTION_LOCATION_SOURCE_SETTINGS; + case "action_application_details_settings": + return Settings.ACTION_APPLICATION_DETAILS_SETTINGS; + default: + return action; + } + } + + private static Bundle convertArguments(Map arguments) { + Bundle bundle = new Bundle(); + if (arguments == null) { + return bundle; + } + for (String key : arguments.keySet()) { + Object value = arguments.get(key); + if (value instanceof Integer) { + bundle.putInt(key, (Integer) value); + } else if (value instanceof String) { + bundle.putString(key, (String) value); + } else if (value instanceof Boolean) { + bundle.putBoolean(key, (Boolean) value); + } else if (value instanceof Double) { + bundle.putDouble(key, (Double) value); + } else if (value instanceof Long) { + bundle.putLong(key, (Long) value); + } else if (value instanceof byte[]) { + bundle.putByteArray(key, (byte[]) value); + } else if (value instanceof int[]) { + bundle.putIntArray(key, (int[]) value); + } else if (value instanceof long[]) { + bundle.putLongArray(key, (long[]) value); + } else if (value instanceof double[]) { + bundle.putDoubleArray(key, (double[]) value); + } else if (isTypedArrayList(value, Integer.class)) { + bundle.putIntegerArrayList(key, (ArrayList) value); + } else if (isTypedArrayList(value, String.class)) { + bundle.putStringArrayList(key, (ArrayList) value); + } else if (isStringKeyedMap(value)) { + bundle.putBundle(key, convertArguments((Map) value)); + } else { + throw new UnsupportedOperationException("Unsupported type " + value); + } + } + return bundle; + } + + private static boolean isTypedArrayList(Object value, Class type) { + if (!(value instanceof ArrayList)) { + return false; + } + ArrayList list = (ArrayList) value; + for (Object o : list) { + if (!(o == null || type.isInstance(o))) { + return false; + } + } + return true; + } + + private static boolean isStringKeyedMap(Object value) { + if (!(value instanceof Map)) { + return false; + } + Map map = (Map) value; + for (Object key : map.keySet()) { + if (!(key == null || key instanceof String)) { + return false; + } + } + return true; + } +} diff --git a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java index b6d3c81b1a8c..58a8806ce00b 100644 --- a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java +++ b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java @@ -4,158 +4,18 @@ package io.flutter.plugins.androidintent; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.provider.Settings; -import android.util.Log; -import io.flutter.plugin.common.MethodCall; +import dev.flutter.plugins.androidintent.IntentSender; +import dev.flutter.plugins.androidintent.MethodCallHandlerImpl; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.PluginRegistry.Registrar; -import java.util.ArrayList; -import java.util.Map; /** AndroidIntentPlugin */ -@SuppressWarnings("unchecked") -public class AndroidIntentPlugin implements MethodCallHandler { - private static final String TAG = AndroidIntentPlugin.class.getCanonicalName(); - private final Registrar mRegistrar; - +public class AndroidIntentPlugin { /** Plugin registration. */ public static void registerWith(Registrar registrar) { final MethodChannel channel = new MethodChannel(registrar.messenger(), "plugins.flutter.io/android_intent"); - channel.setMethodCallHandler(new AndroidIntentPlugin(registrar)); - } - - private AndroidIntentPlugin(Registrar registrar) { - this.mRegistrar = registrar; - } - - private String convertAction(String action) { - switch (action) { - case "action_view": - return Intent.ACTION_VIEW; - case "action_voice": - return Intent.ACTION_VOICE_COMMAND; - case "settings": - return Settings.ACTION_SETTINGS; - case "action_location_source_settings": - return Settings.ACTION_LOCATION_SOURCE_SETTINGS; - case "action_application_details_settings": - return Settings.ACTION_APPLICATION_DETAILS_SETTINGS; - default: - return action; - } - } - - private Bundle convertArguments(Map arguments) { - Bundle bundle = new Bundle(); - for (String key : arguments.keySet()) { - Object value = arguments.get(key); - if (value instanceof Integer) { - bundle.putInt(key, (Integer) value); - } else if (value instanceof String) { - bundle.putString(key, (String) value); - } else if (value instanceof Boolean) { - bundle.putBoolean(key, (Boolean) value); - } else if (value instanceof Double) { - bundle.putDouble(key, (Double) value); - } else if (value instanceof Long) { - bundle.putLong(key, (Long) value); - } else if (value instanceof byte[]) { - bundle.putByteArray(key, (byte[]) value); - } else if (value instanceof int[]) { - bundle.putIntArray(key, (int[]) value); - } else if (value instanceof long[]) { - bundle.putLongArray(key, (long[]) value); - } else if (value instanceof double[]) { - bundle.putDoubleArray(key, (double[]) value); - } else if (isTypedArrayList(value, Integer.class)) { - bundle.putIntegerArrayList(key, (ArrayList) value); - } else if (isTypedArrayList(value, String.class)) { - bundle.putStringArrayList(key, (ArrayList) value); - } else if (isStringKeyedMap(value)) { - bundle.putBundle(key, convertArguments((Map) value)); - } else { - throw new UnsupportedOperationException("Unsupported type " + value); - } - } - return bundle; - } - - private boolean isTypedArrayList(Object value, Class type) { - if (!(value instanceof ArrayList)) { - return false; - } - ArrayList list = (ArrayList) value; - for (Object o : list) { - if (!(o == null || type.isInstance(o))) { - return false; - } - } - return true; - } - - private boolean isStringKeyedMap(Object value) { - if (!(value instanceof Map)) { - return false; - } - Map map = (Map) value; - for (Object key : map.keySet()) { - if (!(key == null || key instanceof String)) { - return false; - } - } - return true; - } - - private Context getActiveContext() { - return (mRegistrar.activity() != null) ? mRegistrar.activity() : mRegistrar.context(); - } - - @Override - public void onMethodCall(MethodCall call, Result result) { - Context context = getActiveContext(); - String action = convertAction((String) call.argument("action")); - - // Build intent - Intent intent = new Intent(action); - if (mRegistrar.activity() == null) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } - if (call.argument("flag") != null) { - intent.addFlags((Integer) call.argument("flags")); - } - if (call.argument("category") != null) { - intent.addCategory((String) call.argument("category")); - } - if (call.argument("data") != null) { - intent.setData(Uri.parse((String) call.argument("data"))); - } - if (call.argument("arguments") != null) { - intent.putExtras(convertArguments((Map) call.argument("arguments"))); - } - if (call.argument("package") != null) { - String packageName = (String) call.argument("package"); - intent.setPackage(packageName); - if (call.argument("componentName") != null) { - intent.setComponent( - new ComponentName(packageName, (String) call.argument("componentName"))); - } - if (intent.resolveActivity(context.getPackageManager()) == null) { - Log.i(TAG, "Cannot resolve explicit intent - ignoring package"); - intent.setPackage(null); - } - } - - Log.i(TAG, "Sending intent " + intent); - context.startActivity(intent); - - result.success(null); + IntentSender sender = new IntentSender(registrar.activity(), registrar.context()); + channel.setMethodCallHandler(new MethodCallHandlerImpl(sender)); } } From c26003e7aa03ada9fb7f22cd84e37790b33f9ae9 Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Thu, 3 Oct 2019 11:39:16 -0700 Subject: [PATCH 2/7] Migrate the plugin to the new embedding Create a new dev.Plugin class with the new embedding. Add it to the manifest and use it in a new dev.MainActivity. --- .../androidintent/AndroidIntentPlugin.java | 66 +++++++++++++++++++ .../android/app/src/main/AndroidManifest.xml | 62 ++++++++++------- .../androidintentexample/MainActivity.java | 12 ++++ .../example/android/gradle.properties | 1 + 4 files changed, 116 insertions(+), 25 deletions(-) create mode 100644 packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPlugin.java create mode 100644 packages/android_intent/example/android/app/src/main/java/dev/flutter/plugins/androidintentexample/MainActivity.java diff --git a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPlugin.java b/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPlugin.java new file mode 100644 index 000000000000..b8e90ee8fa3e --- /dev/null +++ b/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPlugin.java @@ -0,0 +1,66 @@ +package dev.flutter.plugins.androidintent; + +import androidx.annotation.NonNull; +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.MethodChannel; +import io.flutter.plugin.common.PluginRegistry.Registrar; + +/** + * The new embedding implementation of the plugin. + * + *

This can be included in an add to app scenario to gracefully handle activity and context + * changes, unlike the previous {@link io.flutter.plugins.androidintent.AndroidIntentPlugin}. + */ +public class AndroidIntentPlugin implements FlutterPlugin, ActivityAware { + private final IntentSender sender; + + /** + * Initialize this within the {@code #configureFlutterEngine} of a Flutter activity or fragment. + * + *

See {@code dev.flutter.plugins.androidintentexample.MainActivity} for an example. + */ + public AndroidIntentPlugin() { + sender = new IntentSender(/*activity=*/ null, /*context=*/ null); + } + + /** This exists for legacy compatibility purposes. */ + public static void registerWith(Registrar registrar) { + io.flutter.plugins.androidintent.AndroidIntentPlugin.registerWith(registrar); + } + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + sender.setApplicationContext(binding.getApplicationContext()); + MethodChannel channel = + new MethodChannel( + binding.getFlutterEngine().getDartExecutor(), "plugins.flutter.io/android_intent"); + channel.setMethodCallHandler(new MethodCallHandlerImpl(sender)); + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + sender.setApplicationContext(null); + } + + @Override + public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { + sender.setActivity(binding.getActivity()); + } + + @Override + public void onDetachedFromActivity() { + sender.setActivity(null); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity(); + } + + @Override + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { + onAttachedToActivity(binding); + } +} diff --git a/packages/android_intent/example/android/app/src/main/AndroidManifest.xml b/packages/android_intent/example/android/app/src/main/AndroidManifest.xml index ce2fbe4a64a8..156f648df40b 100644 --- a/packages/android_intent/example/android/app/src/main/AndroidManifest.xml +++ b/packages/android_intent/example/android/app/src/main/AndroidManifest.xml @@ -1,29 +1,41 @@ + package="io.flutter.plugins.androidintentexample"> - - - + + + + + + + + + + + - - - - - - - - - + + diff --git a/packages/android_intent/example/android/app/src/main/java/dev/flutter/plugins/androidintentexample/MainActivity.java b/packages/android_intent/example/android/app/src/main/java/dev/flutter/plugins/androidintentexample/MainActivity.java new file mode 100644 index 000000000000..10a3fda27270 --- /dev/null +++ b/packages/android_intent/example/android/app/src/main/java/dev/flutter/plugins/androidintentexample/MainActivity.java @@ -0,0 +1,12 @@ +package dev.flutter.plugins.androidintentexample; + +import dev.flutter.plugins.androidintent.AndroidIntentPlugin; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +public class MainActivity extends FlutterActivity { + @Override + public void configureFlutterEngine(FlutterEngine flutterEngine) { + flutterEngine.getPlugins().add(new AndroidIntentPlugin()); + } +} diff --git a/packages/android_intent/example/android/gradle.properties b/packages/android_intent/example/android/gradle.properties index 8bd86f680510..7be3d8b46841 100644 --- a/packages/android_intent/example/android/gradle.properties +++ b/packages/android_intent/example/android/gradle.properties @@ -1 +1,2 @@ org.gradle.jvmargs=-Xmx1536M +android.enableR8=true From 7cca6f93ca12817e9eb12da2ef33d29a56dd7372 Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Thu, 3 Oct 2019 12:30:33 -0700 Subject: [PATCH 3/7] Add a unit test. --- packages/android_intent/android/build.gradle | 7 + .../MethodCallHandlerImplTest.java | 137 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 packages/android_intent/android/src/test/java/dev/flutter/plugins/androidintent/MethodCallHandlerImplTest.java diff --git a/packages/android_intent/android/build.gradle b/packages/android_intent/android/build.gradle index 19283348a48b..44c249b3b344 100644 --- a/packages/android_intent/android/build.gradle +++ b/packages/android_intent/android/build.gradle @@ -44,8 +44,15 @@ android { lintOptions { disable 'InvalidPackage' } + testOptions { + unitTests.includeAndroidResources = true + } } dependencies { compileOnly 'androidx.annotation:annotation:1.0.0' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:1.10.19' + testImplementation 'androidx.test:core:1.0.0' + testImplementation 'org.robolectric:robolectric:4.3' } diff --git a/packages/android_intent/android/src/test/java/dev/flutter/plugins/androidintent/MethodCallHandlerImplTest.java b/packages/android_intent/android/src/test/java/dev/flutter/plugins/androidintent/MethodCallHandlerImplTest.java new file mode 100644 index 000000000000..9069da9cbc8b --- /dev/null +++ b/packages/android_intent/android/src/test/java/dev/flutter/plugins/androidintent/MethodCallHandlerImplTest.java @@ -0,0 +1,137 @@ +package dev.flutter.plugins.androidintent; + +import static org.junit.Assert.assertEquals; +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.times; +import static org.mockito.Mockito.verify; +import static org.robolectric.Shadows.shadowOf; + +import android.app.Application; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel.Result; +import java.util.HashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class MethodCallHandlerImplTest { + private Context context; + private IntentSender sender; + private MethodCallHandlerImpl methodCallHandler; + + public MethodCallHandlerImplTest() {} + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + sender = new IntentSender(null, null); + methodCallHandler = new MethodCallHandlerImpl(sender); + } + + @Test + public void onMethodCall_doesNothingWhenContextIsNull() { + sendLaunchMethodCall("foo", null, null, null, null); + + assertNull(shadowOf((Application) context).getNextStartedActivity()); + } + + @Test + public void onMethodCall_setsAction() { + sender.setApplicationContext(context); + + sendLaunchMethodCall("foo", null, null, null, null); + + Intent intent = shadowOf((Application) context).getNextStartedActivity(); + assertNotNull(intent); + assertEquals("foo", intent.getAction()); + } + + @Test + public void onMethodCall_setsNewTaskFlagWithApplicationContext() { + sender.setApplicationContext(context); + + sendLaunchMethodCall("foo", null, null, null, null); + + Intent intent = shadowOf((Application) context).getNextStartedActivity(); + assertNotNull(intent); + assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK, intent.getFlags()); + } + + @Test + public void onMethodCall_addsFlags() { + sender.setApplicationContext(context); + + Integer requestFlags = Intent.FLAG_FROM_BACKGROUND; + sendLaunchMethodCall("foo", requestFlags, null, null, null); + + Intent intent = shadowOf((Application) context).getNextStartedActivity(); + assertNotNull(intent); + assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK | requestFlags, intent.getFlags()); + } + + @Test + public void onMethodCall_addsCategory() { + sender.setApplicationContext(context); + + String category = "bar"; + sendLaunchMethodCall("foo", null, category, null, null); + + Intent intent = shadowOf((Application) context).getNextStartedActivity(); + assertNotNull(intent); + assertTrue(intent.getCategories().contains(category)); + } + + @Test + public void onMethodCall_setsData() { + sender.setApplicationContext(context); + + Uri data = Uri.parse("http://flutter.dev"); + sendLaunchMethodCall("foo", null, null, data, null); + + Intent intent = shadowOf((Application) context).getNextStartedActivity(); + assertNotNull(intent); + assertEquals(data, intent.getData()); + } + + @Test + public void onMethodCall_clearsInvalidPackageNames() { + sender.setApplicationContext(context); + + sendLaunchMethodCall("foo", null, null, null, "foo"); + + Intent intent = shadowOf((Application) context).getNextStartedActivity(); + assertNotNull(intent); + assertNull(intent.getPackage()); + } + + private void sendLaunchMethodCall( + String action, + @Nullable Integer flags, + @Nullable String category, + @Nullable Uri data, + @Nullable String packageName) { + Map args = new HashMap<>(); + args.put("action", action); + args.put("flags", flags); + args.put("category", category); + args.put("data", String.valueOf(data)); + args.put("packageName", packageName); + MethodCall call = new MethodCall("launch", args); + Result result = mock(Result.class); + methodCallHandler.onMethodCall(call, result); + + // No matter what, should always succeed. + verify(result, times(1)).success(null); + } +} From dd6079c8f8b4404c23cea13f496962fc83257a45 Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Thu, 3 Oct 2019 14:40:55 -0700 Subject: [PATCH 4/7] Review feedback. --- .../androidintent/AndroidIntentPlugin.java | 12 +-- .../plugins/androidintent/IntentSender.java | 10 +-- .../androidintent/MethodCallHandlerImpl.java | 5 +- .../androidintent/AndroidIntentPlugin.java | 9 ++- .../MethodCallHandlerImplTest.java | 77 +++++++++++-------- 5 files changed, 60 insertions(+), 53 deletions(-) diff --git a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPlugin.java b/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPlugin.java index b8e90ee8fa3e..947dec5a16d7 100644 --- a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPlugin.java +++ b/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPlugin.java @@ -5,15 +5,14 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry.Registrar; /** - * The new embedding implementation of the plugin. + * Plugin implementation that uses the new {@code io.flutter.embedding} package. * *

This can be included in an add to app scenario to gracefully handle activity and context * changes, unlike the previous {@link io.flutter.plugins.androidintent.AndroidIntentPlugin}. */ -public class AndroidIntentPlugin implements FlutterPlugin, ActivityAware { +public final class AndroidIntentPlugin implements FlutterPlugin, ActivityAware { private final IntentSender sender; /** @@ -25,14 +24,10 @@ public AndroidIntentPlugin() { sender = new IntentSender(/*activity=*/ null, /*context=*/ null); } - /** This exists for legacy compatibility purposes. */ - public static void registerWith(Registrar registrar) { - io.flutter.plugins.androidintent.AndroidIntentPlugin.registerWith(registrar); - } - @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { sender.setApplicationContext(binding.getApplicationContext()); + sender.setActivity(null); MethodChannel channel = new MethodChannel( binding.getFlutterEngine().getDartExecutor(), "plugins.flutter.io/android_intent"); @@ -42,6 +37,7 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { sender.setApplicationContext(null); + sender.setActivity(null); } @Override diff --git a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/IntentSender.java b/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/IntentSender.java index 25273146b48a..d3c1aaa888db 100644 --- a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/IntentSender.java +++ b/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/IntentSender.java @@ -11,7 +11,7 @@ import androidx.annotation.Nullable; /** Forms and launches intents. */ -public class IntentSender { +public final class IntentSender { private static final String TAG = "IntentSender"; private @Nullable Activity activity; @@ -103,14 +103,8 @@ void setActivity(@Nullable Activity activity) { this.activity = activity; } - /** - * Caches the given {@code applicationContext} to use for {@link #send}. - * - *

Also resets the cached {@code activity} to null, since the given activity should no longer - * be valid after applicationContext changes. - */ + /** Caches the given {@code applicationContext} to use for {@link #send}. */ void setApplicationContext(@Nullable Context applicationContext) { this.applicationContext = applicationContext; - setActivity(null); } } diff --git a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/MethodCallHandlerImpl.java b/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/MethodCallHandlerImpl.java index 4b29bdb960db..1d53591224e1 100644 --- a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/MethodCallHandlerImpl.java +++ b/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/MethodCallHandlerImpl.java @@ -6,6 +6,7 @@ import android.os.Bundle; import android.provider.Settings; import android.text.TextUtils; +import androidx.annotation.NonNull; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; @@ -13,7 +14,7 @@ import java.util.Map; /** Forwards incoming {@link MethodCall}s to {@link IntentSender#send}. */ -public class MethodCallHandlerImpl implements MethodCallHandler { +public final class MethodCallHandlerImpl implements MethodCallHandler { private final IntentSender sender; /** @@ -32,7 +33,7 @@ public MethodCallHandlerImpl(IntentSender sender) { *

Always calls {@code result#success}. */ @Override - public void onMethodCall(MethodCall call, Result result) { + public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { String action = convertAction((String) call.argument("action")); Integer flags = call.argument("flags"); String category = call.argument("category"); diff --git a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java index 58a8806ce00b..c1acd619af37 100644 --- a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java +++ b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java @@ -9,8 +9,13 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.PluginRegistry.Registrar; -/** AndroidIntentPlugin */ -public class AndroidIntentPlugin { +/** + * Plugin implementation that uses the legacy {@code io.flutter.plugin.common} package. + * + *

This does not respond to changes in activity or context, unlike {@link + * dev.flutter.plugins.androidintent.AndroidIntentPlugin}. + */ +public final class AndroidIntentPlugin { /** Plugin registration. */ public static void registerWith(Registrar registrar) { final MethodChannel channel = diff --git a/packages/android_intent/android/src/test/java/dev/flutter/plugins/androidintent/MethodCallHandlerImplTest.java b/packages/android_intent/android/src/test/java/dev/flutter/plugins/androidintent/MethodCallHandlerImplTest.java index 9069da9cbc8b..02155a0c15f4 100644 --- a/packages/android_intent/android/src/test/java/dev/flutter/plugins/androidintent/MethodCallHandlerImplTest.java +++ b/packages/android_intent/android/src/test/java/dev/flutter/plugins/androidintent/MethodCallHandlerImplTest.java @@ -13,7 +13,6 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; -import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel.Result; @@ -30,8 +29,6 @@ public class MethodCallHandlerImplTest { private IntentSender sender; private MethodCallHandlerImpl methodCallHandler; - public MethodCallHandlerImplTest() {} - @Before public void setUp() { context = ApplicationProvider.getApplicationContext(); @@ -41,17 +38,27 @@ public void setUp() { @Test public void onMethodCall_doesNothingWhenContextIsNull() { - sendLaunchMethodCall("foo", null, null, null, null); + Result result = mock(Result.class); + Map args = new HashMap<>(); + args.put("action", "foo"); + methodCallHandler.onMethodCall(new MethodCall("launch", args), result); + + // No matter what, should always succeed. + verify(result, times(1)).success(null); assertNull(shadowOf((Application) context).getNextStartedActivity()); } @Test public void onMethodCall_setsAction() { sender.setApplicationContext(context); + Map args = new HashMap<>(); + args.put("action", "foo"); + Result result = mock(Result.class); - sendLaunchMethodCall("foo", null, null, null, null); + methodCallHandler.onMethodCall(new MethodCall("launch", args), result); + verify(result, times(1)).success(null); Intent intent = shadowOf((Application) context).getNextStartedActivity(); assertNotNull(intent); assertEquals("foo", intent.getAction()); @@ -60,9 +67,13 @@ public void onMethodCall_setsAction() { @Test public void onMethodCall_setsNewTaskFlagWithApplicationContext() { sender.setApplicationContext(context); + Map args = new HashMap<>(); + args.put("action", "foo"); + Result result = mock(Result.class); - sendLaunchMethodCall("foo", null, null, null, null); + methodCallHandler.onMethodCall(new MethodCall("launch", args), result); + verify(result, times(1)).success(null); Intent intent = shadowOf((Application) context).getNextStartedActivity(); assertNotNull(intent); assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK, intent.getFlags()); @@ -71,10 +82,15 @@ public void onMethodCall_setsNewTaskFlagWithApplicationContext() { @Test public void onMethodCall_addsFlags() { sender.setApplicationContext(context); - + Map args = new HashMap<>(); + args.put("action", "foo"); Integer requestFlags = Intent.FLAG_FROM_BACKGROUND; - sendLaunchMethodCall("foo", requestFlags, null, null, null); + args.put("flags", requestFlags); + Result result = mock(Result.class); + methodCallHandler.onMethodCall(new MethodCall("launch", args), result); + + verify(result, times(1)).success(null); Intent intent = shadowOf((Application) context).getNextStartedActivity(); assertNotNull(intent); assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK | requestFlags, intent.getFlags()); @@ -83,10 +99,15 @@ public void onMethodCall_addsFlags() { @Test public void onMethodCall_addsCategory() { sender.setApplicationContext(context); - + Map args = new HashMap<>(); + args.put("action", "foo"); String category = "bar"; - sendLaunchMethodCall("foo", null, category, null, null); + args.put("category", category); + Result result = mock(Result.class); + + methodCallHandler.onMethodCall(new MethodCall("launch", args), result); + verify(result, times(1)).success(null); Intent intent = shadowOf((Application) context).getNextStartedActivity(); assertNotNull(intent); assertTrue(intent.getCategories().contains(category)); @@ -95,10 +116,15 @@ public void onMethodCall_addsCategory() { @Test public void onMethodCall_setsData() { sender.setApplicationContext(context); - + Map args = new HashMap<>(); + args.put("action", "foo"); Uri data = Uri.parse("http://flutter.dev"); - sendLaunchMethodCall("foo", null, null, data, null); + args.put("data", data.toString()); + Result result = mock(Result.class); + + methodCallHandler.onMethodCall(new MethodCall("launch", args), result); + verify(result, times(1)).success(null); Intent intent = shadowOf((Application) context).getNextStartedActivity(); assertNotNull(intent); assertEquals(data, intent.getData()); @@ -107,31 +133,16 @@ public void onMethodCall_setsData() { @Test public void onMethodCall_clearsInvalidPackageNames() { sender.setApplicationContext(context); + Map args = new HashMap<>(); + args.put("action", "foo"); + args.put("packageName", "invalid"); + Result result = mock(Result.class); - sendLaunchMethodCall("foo", null, null, null, "foo"); + methodCallHandler.onMethodCall(new MethodCall("launch", args), result); + verify(result, times(1)).success(null); Intent intent = shadowOf((Application) context).getNextStartedActivity(); assertNotNull(intent); assertNull(intent.getPackage()); } - - private void sendLaunchMethodCall( - String action, - @Nullable Integer flags, - @Nullable String category, - @Nullable Uri data, - @Nullable String packageName) { - Map args = new HashMap<>(); - args.put("action", action); - args.put("flags", flags); - args.put("category", category); - args.put("data", String.valueOf(data)); - args.put("packageName", packageName); - MethodCall call = new MethodCall("launch", args); - Result result = mock(Result.class); - methodCallHandler.onMethodCall(call, result); - - // No matter what, should always succeed. - verify(result, times(1)).success(null); - } } From a5caa9f68f9cea46a36c3b25b5725b6b0a0aff2d Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Thu, 3 Oct 2019 16:55:52 -0700 Subject: [PATCH 5/7] Changes from chat discussion 1. Move all classes into `dev`. 2. Clean up the MethodChannel based on the engine lifecycle handlers. --- .../androidintent/AndroidIntentPlugin.java | 12 ++--- .../AndroidIntentPluginRegistrar.java | 28 +++++++++++ .../androidintent/MethodCallHandlerImpl.java | 40 ++++++++++++++- .../androidintent/AndroidIntentPlugin.java | 26 ---------- .../MethodCallHandlerImplTest.java | 49 +++++++++++++++++++ packages/android_intent/pubspec.yaml | 4 +- 6 files changed, 123 insertions(+), 36 deletions(-) create mode 100644 packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPluginRegistrar.java delete mode 100644 packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java diff --git a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPlugin.java b/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPlugin.java index 947dec5a16d7..96f75d427b17 100644 --- a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPlugin.java +++ b/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPlugin.java @@ -4,16 +4,15 @@ 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.MethodChannel; /** * Plugin implementation that uses the new {@code io.flutter.embedding} package. * - *

This can be included in an add to app scenario to gracefully handle activity and context - * changes, unlike the previous {@link io.flutter.plugins.androidintent.AndroidIntentPlugin}. + *

Instantiate this in an add to app scenario to gracefully handle activity and context changes. */ public final class AndroidIntentPlugin implements FlutterPlugin, ActivityAware { private final IntentSender sender; + private final MethodCallHandlerImpl impl; /** * Initialize this within the {@code #configureFlutterEngine} of a Flutter activity or fragment. @@ -22,22 +21,21 @@ public final class AndroidIntentPlugin implements FlutterPlugin, ActivityAware { */ public AndroidIntentPlugin() { sender = new IntentSender(/*activity=*/ null, /*context=*/ null); + impl = new MethodCallHandlerImpl(sender); } @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { sender.setApplicationContext(binding.getApplicationContext()); sender.setActivity(null); - MethodChannel channel = - new MethodChannel( - binding.getFlutterEngine().getDartExecutor(), "plugins.flutter.io/android_intent"); - channel.setMethodCallHandler(new MethodCallHandlerImpl(sender)); + impl.startListening(binding.getFlutterEngine().getDartExecutor()); } @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { sender.setApplicationContext(null); sender.setActivity(null); + impl.stopListening(); } @Override diff --git a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPluginRegistrar.java b/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPluginRegistrar.java new file mode 100644 index 000000000000..e747b6c0ce48 --- /dev/null +++ b/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPluginRegistrar.java @@ -0,0 +1,28 @@ +// Copyright 2017 The Chromium 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 dev.flutter.plugins.androidintent; + +import io.flutter.plugin.common.PluginRegistry.Registrar; + +/** + * Automatically registers a plugin implementation that relies on the stable {@code + * io.flutter.plugin.common} package. + */ +public final class AndroidIntentPluginRegistrar { + private AndroidIntentPluginRegistrar() {} + + /** + * Registers a plugin implementation that uses the stable {@code io.flutter.plugin.common} + * package. + * + *

Calling this automatically initializes the plugin. However plugins initialized this way + * won't react to changes in activity or context, unlike {@link AndroidIntentPlugin}. + */ + public static void registerWith(Registrar registrar) { + IntentSender sender = new IntentSender(registrar.activity(), registrar.context()); + MethodCallHandlerImpl impl = new MethodCallHandlerImpl(sender); + impl.startListening(registrar.messenger()); + } +} diff --git a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/MethodCallHandlerImpl.java b/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/MethodCallHandlerImpl.java index 1d53591224e1..1ba461aca34e 100644 --- a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/MethodCallHandlerImpl.java +++ b/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/MethodCallHandlerImpl.java @@ -6,8 +6,12 @@ import android.os.Bundle; import android.provider.Settings; import android.text.TextUtils; +import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; import java.util.ArrayList; @@ -15,7 +19,9 @@ /** Forwards incoming {@link MethodCall}s to {@link IntentSender#send}. */ public final class MethodCallHandlerImpl implements MethodCallHandler { + private static final String TAG = "MethodCallHandlerImpl"; private final IntentSender sender; + private @Nullable MethodChannel methodChannel; /** * Uses the given {@code sender} for all incoming calls. @@ -23,10 +29,42 @@ public final class MethodCallHandlerImpl implements MethodCallHandler { *

This assumes that the sender's context and activity state are managed elsewhere and * correctly initialized before being sent here. */ - public MethodCallHandlerImpl(IntentSender sender) { + MethodCallHandlerImpl(IntentSender sender) { this.sender = sender; } + /** + * Registers this instance as a method call handler on the given {@code messenger}. + * + *

Stops any previously started and unstopped calls. + * + *

This should be cleaned with {@link #stopListening} once the messenger is disposed of. + */ + void startListening(BinaryMessenger messenger) { + if (methodChannel != null) { + Log.wtf(TAG, "Setting a method call handler before the last was disposed."); + stopListening(); + } + + methodChannel = new MethodChannel(messenger, "plugins.flutter.io/android_intent"); + methodChannel.setMethodCallHandler(this); + } + + /** + * Clears this instance from listening to method calls. + * + *

Does nothing is {@link #startListening} hasn't been called, or if we're already stopped. + */ + void stopListening() { + if (methodChannel == null) { + Log.d(TAG, "Tried to stop listening when no methodChannel had been initialized."); + return; + } + + methodChannel.setMethodCallHandler(null); + methodChannel = null; + } + /** * Parses the incoming call and forwards it to the cached {@link IntentSender}. * diff --git a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java deleted file mode 100644 index c1acd619af37..000000000000 --- a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2017 The Chromium 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.androidintent; - -import dev.flutter.plugins.androidintent.IntentSender; -import dev.flutter.plugins.androidintent.MethodCallHandlerImpl; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry.Registrar; - -/** - * Plugin implementation that uses the legacy {@code io.flutter.plugin.common} package. - * - *

This does not respond to changes in activity or context, unlike {@link - * dev.flutter.plugins.androidintent.AndroidIntentPlugin}. - */ -public final class AndroidIntentPlugin { - /** Plugin registration. */ - public static void registerWith(Registrar registrar) { - final MethodChannel channel = - new MethodChannel(registrar.messenger(), "plugins.flutter.io/android_intent"); - IntentSender sender = new IntentSender(registrar.activity(), registrar.context()); - channel.setMethodCallHandler(new MethodCallHandlerImpl(sender)); - } -} diff --git a/packages/android_intent/android/src/test/java/dev/flutter/plugins/androidintent/MethodCallHandlerImplTest.java b/packages/android_intent/android/src/test/java/dev/flutter/plugins/androidintent/MethodCallHandlerImplTest.java index 02155a0c15f4..ad55fc3298d1 100644 --- a/packages/android_intent/android/src/test/java/dev/flutter/plugins/androidintent/MethodCallHandlerImplTest.java +++ b/packages/android_intent/android/src/test/java/dev/flutter/plugins/androidintent/MethodCallHandlerImplTest.java @@ -4,7 +4,10 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.robolectric.Shadows.shadowOf; @@ -14,6 +17,8 @@ import android.content.Intent; import android.net.Uri; import androidx.test.core.app.ApplicationProvider; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel.Result; import java.util.HashMap; @@ -25,6 +30,7 @@ @RunWith(RobolectricTestRunner.class) public class MethodCallHandlerImplTest { + private static final String CHANNEL_NAME = "plugins.flutter.io/android_intent"; private Context context; private IntentSender sender; private MethodCallHandlerImpl methodCallHandler; @@ -36,6 +42,49 @@ public void setUp() { methodCallHandler = new MethodCallHandlerImpl(sender); } + @Test + public void startListening_registersChannel() { + BinaryMessenger messenger = mock(BinaryMessenger.class); + + methodCallHandler.startListening(messenger); + + verify(messenger, times(1)) + .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); + } + + @Test + public void startListening_unregistersExistingChannel() { + BinaryMessenger firstMessenger = mock(BinaryMessenger.class); + BinaryMessenger secondMessenger = mock(BinaryMessenger.class); + methodCallHandler.startListening(firstMessenger); + + methodCallHandler.startListening(secondMessenger); + + // Unregisters the first and then registers the second. + verify(firstMessenger, times(1)).setMessageHandler(CHANNEL_NAME, null); + verify(secondMessenger, times(1)) + .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); + } + + @Test + public void stopListening_unregistersExistingChannel() { + BinaryMessenger messenger = mock(BinaryMessenger.class); + methodCallHandler.startListening(messenger); + + methodCallHandler.stopListening(); + + verify(messenger, times(1)).setMessageHandler(CHANNEL_NAME, null); + } + + @Test + public void stopListening_doesNothingWhenUnset() { + BinaryMessenger messenger = mock(BinaryMessenger.class); + + methodCallHandler.stopListening(); + + verify(messenger, never()).setMessageHandler(CHANNEL_NAME, null); + } + @Test public void onMethodCall_doesNothingWhenContextIsNull() { Result result = mock(Result.class); diff --git a/packages/android_intent/pubspec.yaml b/packages/android_intent/pubspec.yaml index 2bdf002f3ea8..a8a31fe747c3 100644 --- a/packages/android_intent/pubspec.yaml +++ b/packages/android_intent/pubspec.yaml @@ -6,9 +6,9 @@ version: 0.3.3+3 flutter: plugin: - androidPackage: io.flutter.plugins.androidintent + androidPackage: dev.flutter.plugins.androidintent iosPrefix: FLT - pluginClass: AndroidIntentPlugin + pluginClass: AndroidIntentPluginRegistrar dependencies: flutter: From 0a0e3e3ac54d7946203db6a1a3c59df9417c923b Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Tue, 8 Oct 2019 12:57:02 -0700 Subject: [PATCH 6/7] Naming changes based on offline discussion. --- .../androidintent/AndroidIntentPlugin.java | 4 +-- .../AndroidIntentPluginRegistrar.java | 2 +- .../plugins/androidintent/IntentSender.java | 2 +- .../androidintent/MethodCallHandlerImpl.java | 2 +- .../MethodCallHandlerImplTest.java | 2 +- .../android/app/src/main/AndroidManifest.xml | 34 +++++++++---------- .../androidintentexample/MainActivity.java | 12 ------- .../EmbeddingV1Activity.java | 17 ++++++++++ .../androidintentexample/MainActivity.java | 15 +++----- packages/android_intent/pubspec.yaml | 2 +- 10 files changed, 46 insertions(+), 46 deletions(-) rename packages/android_intent/android/src/main/java/{dev => io}/flutter/plugins/androidintent/AndroidIntentPlugin.java (93%) rename packages/android_intent/android/src/main/java/{dev => io}/flutter/plugins/androidintent/AndroidIntentPluginRegistrar.java (95%) rename packages/android_intent/android/src/main/java/{dev => io}/flutter/plugins/androidintent/IntentSender.java (98%) rename packages/android_intent/android/src/main/java/{dev => io}/flutter/plugins/androidintent/MethodCallHandlerImpl.java (99%) rename packages/android_intent/android/src/test/java/{dev => io}/flutter/plugins/androidintent/MethodCallHandlerImplTest.java (99%) delete mode 100644 packages/android_intent/example/android/app/src/main/java/dev/flutter/plugins/androidintentexample/MainActivity.java create mode 100644 packages/android_intent/example/android/app/src/main/java/io/flutter/plugins/androidintentexample/EmbeddingV1Activity.java diff --git a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPlugin.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java similarity index 93% rename from packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPlugin.java rename to packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java index 96f75d427b17..b670cb749da1 100644 --- a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPlugin.java +++ b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java @@ -1,4 +1,4 @@ -package dev.flutter.plugins.androidintent; +package io.flutter.plugins.androidintent; import androidx.annotation.NonNull; import io.flutter.embedding.engine.plugins.FlutterPlugin; @@ -17,7 +17,7 @@ public final class AndroidIntentPlugin implements FlutterPlugin, ActivityAware { /** * Initialize this within the {@code #configureFlutterEngine} of a Flutter activity or fragment. * - *

See {@code dev.flutter.plugins.androidintentexample.MainActivity} for an example. + *

See {@code io.flutter.plugins.androidintentexample.MainActivity} for an example. */ public AndroidIntentPlugin() { sender = new IntentSender(/*activity=*/ null, /*context=*/ null); diff --git a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPluginRegistrar.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPluginRegistrar.java similarity index 95% rename from packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPluginRegistrar.java rename to packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPluginRegistrar.java index e747b6c0ce48..3cfc1a843412 100644 --- a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/AndroidIntentPluginRegistrar.java +++ b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPluginRegistrar.java @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package dev.flutter.plugins.androidintent; +package io.flutter.plugins.androidintent; import io.flutter.plugin.common.PluginRegistry.Registrar; diff --git a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/IntentSender.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/IntentSender.java similarity index 98% rename from packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/IntentSender.java rename to packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/IntentSender.java index d3c1aaa888db..13e56ed487e5 100644 --- a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/IntentSender.java +++ b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/IntentSender.java @@ -1,4 +1,4 @@ -package dev.flutter.plugins.androidintent; +package io.flutter.plugins.androidintent; import android.app.Activity; import android.content.ComponentName; diff --git a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/MethodCallHandlerImpl.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/MethodCallHandlerImpl.java similarity index 99% rename from packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/MethodCallHandlerImpl.java rename to packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/MethodCallHandlerImpl.java index 1ba461aca34e..abbefc89f930 100644 --- a/packages/android_intent/android/src/main/java/dev/flutter/plugins/androidintent/MethodCallHandlerImpl.java +++ b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/MethodCallHandlerImpl.java @@ -1,4 +1,4 @@ -package dev.flutter.plugins.androidintent; +package io.flutter.plugins.androidintent; import android.content.ComponentName; import android.content.Intent; diff --git a/packages/android_intent/android/src/test/java/dev/flutter/plugins/androidintent/MethodCallHandlerImplTest.java b/packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java similarity index 99% rename from packages/android_intent/android/src/test/java/dev/flutter/plugins/androidintent/MethodCallHandlerImplTest.java rename to packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java index ad55fc3298d1..19392b04084f 100644 --- a/packages/android_intent/android/src/test/java/dev/flutter/plugins/androidintent/MethodCallHandlerImplTest.java +++ b/packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java @@ -1,4 +1,4 @@ -package dev.flutter.plugins.androidintent; +package io.flutter.plugins.androidintent; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; diff --git a/packages/android_intent/example/android/app/src/main/AndroidManifest.xml b/packages/android_intent/example/android/app/src/main/AndroidManifest.xml index 156f648df40b..7d6fcd44834f 100644 --- a/packages/android_intent/example/android/app/src/main/AndroidManifest.xml +++ b/packages/android_intent/example/android/app/src/main/AndroidManifest.xml @@ -1,14 +1,23 @@ - + + - - + diff --git a/packages/android_intent/example/android/app/src/main/java/dev/flutter/plugins/androidintentexample/MainActivity.java b/packages/android_intent/example/android/app/src/main/java/dev/flutter/plugins/androidintentexample/MainActivity.java deleted file mode 100644 index 10a3fda27270..000000000000 --- a/packages/android_intent/example/android/app/src/main/java/dev/flutter/plugins/androidintentexample/MainActivity.java +++ /dev/null @@ -1,12 +0,0 @@ -package dev.flutter.plugins.androidintentexample; - -import dev.flutter.plugins.androidintent.AndroidIntentPlugin; -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; - -public class MainActivity extends FlutterActivity { - @Override - public void configureFlutterEngine(FlutterEngine flutterEngine) { - flutterEngine.getPlugins().add(new AndroidIntentPlugin()); - } -} diff --git a/packages/android_intent/example/android/app/src/main/java/io/flutter/plugins/androidintentexample/EmbeddingV1Activity.java b/packages/android_intent/example/android/app/src/main/java/io/flutter/plugins/androidintentexample/EmbeddingV1Activity.java new file mode 100644 index 000000000000..95dc41a02ef7 --- /dev/null +++ b/packages/android_intent/example/android/app/src/main/java/io/flutter/plugins/androidintentexample/EmbeddingV1Activity.java @@ -0,0 +1,17 @@ +// Copyright 2017 The Chromium 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.androidintentexample; + +import android.os.Bundle; +import io.flutter.app.FlutterActivity; +import io.flutter.plugins.GeneratedPluginRegistrant; + +public class EmbeddingV1Activity extends FlutterActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + GeneratedPluginRegistrant.registerWith(this); + } +} diff --git a/packages/android_intent/example/android/app/src/main/java/io/flutter/plugins/androidintentexample/MainActivity.java b/packages/android_intent/example/android/app/src/main/java/io/flutter/plugins/androidintentexample/MainActivity.java index 4af83acdf1cb..56e0bab207d4 100644 --- a/packages/android_intent/example/android/app/src/main/java/io/flutter/plugins/androidintentexample/MainActivity.java +++ b/packages/android_intent/example/android/app/src/main/java/io/flutter/plugins/androidintentexample/MainActivity.java @@ -1,17 +1,12 @@ -// Copyright 2017 The Chromium 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.androidintentexample; -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugins.androidintent.AndroidIntentPlugin; public class MainActivity extends FlutterActivity { @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); + public void configureFlutterEngine(FlutterEngine flutterEngine) { + flutterEngine.getPlugins().add(new AndroidIntentPlugin()); } } diff --git a/packages/android_intent/pubspec.yaml b/packages/android_intent/pubspec.yaml index a8a31fe747c3..24da8f1645eb 100644 --- a/packages/android_intent/pubspec.yaml +++ b/packages/android_intent/pubspec.yaml @@ -6,7 +6,7 @@ version: 0.3.3+3 flutter: plugin: - androidPackage: dev.flutter.plugins.androidintent + androidPackage: io.flutter.plugins.androidintent iosPrefix: FLT pluginClass: AndroidIntentPluginRegistrar From 5434ffb3b50c592aa6287b6d8798a349c6f0c845 Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Fri, 11 Oct 2019 16:50:43 -0700 Subject: [PATCH 7/7] Last migration fixes --- packages/android_intent/CHANGELOG.md | 11 ++++++-- packages/android_intent/android/build.gradle | 25 +++++++++++++++++ .../androidintent/AndroidIntentPlugin.java | 14 ++++++++++ .../AndroidIntentPluginRegistrar.java | 28 ------------------- packages/android_intent/pubspec.yaml | 4 +-- 5 files changed, 50 insertions(+), 32 deletions(-) delete mode 100644 packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPluginRegistrar.java diff --git a/packages/android_intent/CHANGELOG.md b/packages/android_intent/CHANGELOG.md index 26090115d466..9b614940823b 100644 --- a/packages/android_intent/CHANGELOG.md +++ b/packages/android_intent/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.3.4 + +* Migrate the plugin to use the V2 Android engine embedding. This shouldn't + affect existing functionality. Plugin authors who use the V2 embedding can now + instantiate the plugin and expect that it correctly responds to app lifecycle + changes. + ## 0.3.3+3 * Define clang module for iOS. @@ -8,11 +15,11 @@ ## 0.3.3+1 -* Added "action_application_details_settings" action to open application info settings . +* Added "action_application_details_settings" action to open application info settings . ## 0.3.3 -* Added "flags" option to call intent.addFlags(int) in native. +* Added "flags" option to call intent.addFlags(int) in native. ## 0.3.2 diff --git a/packages/android_intent/android/build.gradle b/packages/android_intent/android/build.gradle index 44c249b3b344..ceece7ff6a00 100644 --- a/packages/android_intent/android/build.gradle +++ b/packages/android_intent/android/build.gradle @@ -56,3 +56,28 @@ dependencies { testImplementation 'androidx.test:core:1.0.0' testImplementation 'org.robolectric:robolectric:4.3' } + +// TODO(mklim): Remove this hack once androidx.lifecycle is included on stable. https://github.com/flutter/flutter/issues/42348 +afterEvaluate { + def containsEmbeddingDependencies = false + for (def configuration : configurations.all) { + for (def dependency : configuration.dependencies) { + if (dependency.group == 'io.flutter' && + dependency.name.startsWith('flutter_embedding') && + dependency.isTransitive()) + { + containsEmbeddingDependencies = true + break + } + } + } + if (!containsEmbeddingDependencies) { + android { + dependencies { + def lifecycle_version = "2.1.0" + api "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" + api "androidx.lifecycle:lifecycle-runtime:$lifecycle_version" + } + } + } +} diff --git a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java index b670cb749da1..d2b58814dcf7 100644 --- a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java +++ b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java @@ -4,6 +4,7 @@ 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.PluginRegistry.Registrar; /** * Plugin implementation that uses the new {@code io.flutter.embedding} package. @@ -24,6 +25,19 @@ public AndroidIntentPlugin() { impl = new MethodCallHandlerImpl(sender); } + /** + * Registers a plugin implementation that uses the stable {@code io.flutter.plugin.common} + * package. + * + *

Calling this automatically initializes the plugin. However plugins initialized this way + * won't react to changes in activity or context, unlike {@link AndroidIntentPlugin}. + */ + public static void registerWith(Registrar registrar) { + IntentSender sender = new IntentSender(registrar.activity(), registrar.context()); + MethodCallHandlerImpl impl = new MethodCallHandlerImpl(sender); + impl.startListening(registrar.messenger()); + } + @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { sender.setApplicationContext(binding.getApplicationContext()); diff --git a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPluginRegistrar.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPluginRegistrar.java deleted file mode 100644 index 3cfc1a843412..000000000000 --- a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPluginRegistrar.java +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2017 The Chromium 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.androidintent; - -import io.flutter.plugin.common.PluginRegistry.Registrar; - -/** - * Automatically registers a plugin implementation that relies on the stable {@code - * io.flutter.plugin.common} package. - */ -public final class AndroidIntentPluginRegistrar { - private AndroidIntentPluginRegistrar() {} - - /** - * Registers a plugin implementation that uses the stable {@code io.flutter.plugin.common} - * package. - * - *

Calling this automatically initializes the plugin. However plugins initialized this way - * won't react to changes in activity or context, unlike {@link AndroidIntentPlugin}. - */ - public static void registerWith(Registrar registrar) { - IntentSender sender = new IntentSender(registrar.activity(), registrar.context()); - MethodCallHandlerImpl impl = new MethodCallHandlerImpl(sender); - impl.startListening(registrar.messenger()); - } -} diff --git a/packages/android_intent/pubspec.yaml b/packages/android_intent/pubspec.yaml index 24da8f1645eb..bcb73cf76636 100644 --- a/packages/android_intent/pubspec.yaml +++ b/packages/android_intent/pubspec.yaml @@ -2,13 +2,13 @@ name: android_intent description: Flutter plugin for launching Android Intents. Not supported on iOS. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/android_intent -version: 0.3.3+3 +version: 0.3.4 flutter: plugin: androidPackage: io.flutter.plugins.androidintent iosPrefix: FLT - pluginClass: AndroidIntentPluginRegistrar + pluginClass: AndroidIntentPlugin dependencies: flutter: