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 8b21464f4b19..ceece7ff6a00 100644 --- a/packages/android_intent/android/build.gradle +++ b/packages/android_intent/android/build.gradle @@ -44,4 +44,40 @@ 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' +} + +// 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 b6d3c81b1a8c..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 @@ -1,161 +1,74 @@ -// 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 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 io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; +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.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; +/** + * Plugin implementation that uses the new {@code io.flutter.embedding} package. + * + *

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; - /** 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)); + /** + * Initialize this within the {@code #configureFlutterEngine} of a Flutter activity or fragment. + * + *

See {@code io.flutter.plugins.androidintentexample.MainActivity} for an example. + */ + public AndroidIntentPlugin() { + sender = new IntentSender(/*activity=*/ null, /*context=*/ null); + impl = new MethodCallHandlerImpl(sender); } - private AndroidIntentPlugin(Registrar registrar) { - this.mRegistrar = registrar; + /** + * 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()); } - 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; - } + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + sender.setApplicationContext(binding.getApplicationContext()); + sender.setActivity(null); + impl.startListening(binding.getFlutterEngine().getDartExecutor()); } - 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; + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + sender.setApplicationContext(null); + sender.setActivity(null); + impl.stopListening(); } - 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; + @Override + public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { + sender.setActivity(binding.getActivity()); } - 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; + @Override + public void onDetachedFromActivity() { + sender.setActivity(null); } - private Context getActiveContext() { - return (mRegistrar.activity() != null) ? mRegistrar.activity() : mRegistrar.context(); + @Override + public void onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity(); } @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); + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { + onAttachedToActivity(binding); } } diff --git a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/IntentSender.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/IntentSender.java new file mode 100644 index 000000000000..13e56ed487e5 --- /dev/null +++ b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/IntentSender.java @@ -0,0 +1,110 @@ +package io.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 final 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}. */ + void setApplicationContext(@Nullable Context applicationContext) { + this.applicationContext = applicationContext; + } +} diff --git a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/MethodCallHandlerImpl.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/MethodCallHandlerImpl.java new file mode 100644 index 000000000000..abbefc89f930 --- /dev/null +++ b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/MethodCallHandlerImpl.java @@ -0,0 +1,171 @@ +package io.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 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; +import java.util.Map; + +/** 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. + * + *

This assumes that the sender's context and activity state are managed elsewhere and + * correctly initialized before being sent here. + */ + 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}. + * + *

Always calls {@code result#success}. + */ + @Override + 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"); + 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/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java b/packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java new file mode 100644 index 000000000000..19392b04084f --- /dev/null +++ b/packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java @@ -0,0 +1,197 @@ +package io.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.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; + +import android.app.Application; +import android.content.Context; +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; +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 static final String CHANNEL_NAME = "plugins.flutter.io/android_intent"; + private Context context; + private IntentSender sender; + private MethodCallHandlerImpl methodCallHandler; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + sender = new IntentSender(null, null); + 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); + 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); + + 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()); + } + + @Test + public void onMethodCall_setsNewTaskFlagWithApplicationContext() { + sender.setApplicationContext(context); + Map args = new HashMap<>(); + args.put("action", "foo"); + 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, intent.getFlags()); + } + + @Test + public void onMethodCall_addsFlags() { + sender.setApplicationContext(context); + Map args = new HashMap<>(); + args.put("action", "foo"); + Integer requestFlags = Intent.FLAG_FROM_BACKGROUND; + 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()); + } + + @Test + public void onMethodCall_addsCategory() { + sender.setApplicationContext(context); + Map args = new HashMap<>(); + args.put("action", "foo"); + String category = "bar"; + 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)); + } + + @Test + public void onMethodCall_setsData() { + sender.setApplicationContext(context); + Map args = new HashMap<>(); + args.put("action", "foo"); + Uri data = Uri.parse("http://flutter.dev"); + 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()); + } + + @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); + + methodCallHandler.onMethodCall(new MethodCall("launch", args), result); + + verify(result, times(1)).success(null); + Intent intent = shadowOf((Application) context).getNextStartedActivity(); + assertNotNull(intent); + assertNull(intent.getPackage()); + } +} 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..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,29 +1,41 @@ + package="io.flutter.plugins.androidintentexample"> - - - + + + + + + + + + + + - - - - - - - - - + + 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/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 diff --git a/packages/android_intent/pubspec.yaml b/packages/android_intent/pubspec.yaml index 2bdf002f3ea8..bcb73cf76636 100644 --- a/packages/android_intent/pubspec.yaml +++ b/packages/android_intent/pubspec.yaml @@ -2,7 +2,7 @@ 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: