diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java
index 66c2acd8e886b..95e9b978892a5 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java
@@ -13,6 +13,7 @@
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_DESTROY_ENGINE_WITH_ACTIVITY;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_ENABLE_STATE_RESTORATION;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_INITIAL_ROUTE;
+import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.HANDLE_DEEPLINKING_META_DATA_KEY;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.INITIAL_ROUTE_META_DATA_KEY;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.NORMAL_THEME_META_DATA_KEY;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.SPLASH_SCREEN_META_DATA_KEY;
@@ -446,10 +447,9 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
*/
private void switchLaunchThemeForNormalTheme() {
try {
- ActivityInfo activityInfo =
- getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
- if (activityInfo.metaData != null) {
- int normalThemeRID = activityInfo.metaData.getInt(NORMAL_THEME_META_DATA_KEY, -1);
+ Bundle metaData = getMetaData();
+ if (metaData != null) {
+ int normalThemeRID = metaData.getInt(NORMAL_THEME_META_DATA_KEY, -1);
if (normalThemeRID != -1) {
setTheme(normalThemeRID);
}
@@ -485,10 +485,8 @@ public SplashScreen provideSplashScreen() {
@SuppressWarnings("deprecation")
private Drawable getSplashScreenFromManifest() {
try {
- ActivityInfo activityInfo =
- getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
- Bundle metadata = activityInfo.metaData;
- int splashScreenId = metadata != null ? metadata.getInt(SPLASH_SCREEN_META_DATA_KEY) : 0;
+ Bundle metaData = getMetaData();
+ int splashScreenId = metaData != null ? metaData.getInt(SPLASH_SCREEN_META_DATA_KEY) : 0;
return splashScreenId != 0
? Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP
? getResources().getDrawable(splashScreenId, getTheme())
@@ -748,11 +746,9 @@ public boolean shouldDestroyEngineWithHost() {
@NonNull
public String getDartEntrypointFunctionName() {
try {
- ActivityInfo activityInfo =
- getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
- Bundle metadata = activityInfo.metaData;
+ Bundle metaData = getMetaData();
String desiredDartEntrypoint =
- metadata != null ? metadata.getString(DART_ENTRYPOINT_META_DATA_KEY) : null;
+ metaData != null ? metaData.getString(DART_ENTRYPOINT_META_DATA_KEY) : null;
return desiredDartEntrypoint != null ? desiredDartEntrypoint : DEFAULT_DART_ENTRYPOINT;
} catch (PackageManager.NameNotFoundException e) {
return DEFAULT_DART_ENTRYPOINT;
@@ -779,22 +775,22 @@ public String getDartEntrypointFunctionName() {
* have control over the incoming {@code Intent}.
*
*
Subclasses may override this method to directly control the initial route.
+ *
+ *
If this method returns null and the {@code shouldHandleDeeplinking} returns true, the
+ * initial route is derived from the {@code Intent} through the Intent.getData() instead.
*/
- @NonNull
public String getInitialRoute() {
if (getIntent().hasExtra(EXTRA_INITIAL_ROUTE)) {
return getIntent().getStringExtra(EXTRA_INITIAL_ROUTE);
}
try {
- ActivityInfo activityInfo =
- getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
- Bundle metadata = activityInfo.metaData;
+ Bundle metaData = getMetaData();
String desiredInitialRoute =
- metadata != null ? metadata.getString(INITIAL_ROUTE_META_DATA_KEY) : null;
- return desiredInitialRoute != null ? desiredInitialRoute : DEFAULT_INITIAL_ROUTE;
+ metaData != null ? metaData.getString(INITIAL_ROUTE_META_DATA_KEY) : null;
+ return desiredInitialRoute;
} catch (PackageManager.NameNotFoundException e) {
- return DEFAULT_INITIAL_ROUTE;
+ return null;
}
}
@@ -894,6 +890,14 @@ protected FlutterEngine getFlutterEngine() {
return delegate.getFlutterEngine();
}
+ /** Retrieves the meta data specified in the AndroidManifest.xml. */
+ @Nullable
+ protected Bundle getMetaData() throws PackageManager.NameNotFoundException {
+ ActivityInfo activityInfo =
+ getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
+ return activityInfo.metaData;
+ }
+
@Nullable
@Override
public PlatformPlugin providePlatformPlugin(
@@ -970,6 +974,26 @@ public boolean shouldAttachEngineToActivity() {
return true;
}
+ /**
+ * Whether to handle the deeplinking from the {@code Intent} automatically if the {@code
+ * getInitialRoute} returns null.
+ *
+ *
The default implementation looks {@code } called {@link
+ * FlutterActivityLaunchConfigs#HANDLE_DEEPLINKING_META_DATA_KEY} within the Android manifest
+ * definition for this {@code FlutterActivity}.
+ */
+ @Override
+ public boolean shouldHandleDeeplinking() {
+ try {
+ Bundle metaData = getMetaData();
+ boolean shouldHandleDeeplinking =
+ metaData != null ? metaData.getBoolean(HANDLE_DEEPLINKING_META_DATA_KEY) : false;
+ return shouldHandleDeeplinking;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
@Override
public void onFlutterSurfaceViewCreated(@NonNull FlutterSurfaceView flutterSurfaceView) {
// Hook for subclasses.
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java
index 78585172dd683..f48b7e539f4b0 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java
@@ -5,10 +5,12 @@
package io.flutter.embedding.android;
import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
+import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_INITIAL_ROUTE;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
+import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -362,19 +364,23 @@ private void doInitialFlutterViewRun() {
// So this is expected behavior in many cases.
return;
}
-
+ String initialRoute = host.getInitialRoute();
+ if (initialRoute == null) {
+ initialRoute = maybeGetInitialRouteFromIntent(host.getActivity().getIntent());
+ if (initialRoute == null) {
+ initialRoute = DEFAULT_INITIAL_ROUTE;
+ }
+ }
Log.v(
TAG,
"Executing Dart entrypoint: "
+ host.getDartEntrypointFunctionName()
+ ", and sending initial route: "
- + host.getInitialRoute());
+ + initialRoute);
// The engine needs to receive the Flutter app's initial route before executing any
// Dart code to ensure that the initial route arrives in time to be applied.
- if (host.getInitialRoute() != null) {
- flutterEngine.getNavigationChannel().setInitialRoute(host.getInitialRoute());
- }
+ flutterEngine.getNavigationChannel().setInitialRoute(initialRoute);
String appBundlePathOverride = host.getAppBundlePath();
if (appBundlePathOverride == null || appBundlePathOverride.isEmpty()) {
@@ -388,6 +394,16 @@ private void doInitialFlutterViewRun() {
flutterEngine.getDartExecutor().executeDartEntrypoint(entrypoint);
}
+ private String maybeGetInitialRouteFromIntent(Intent intent) {
+ if (host.shouldHandleDeeplinking()) {
+ Uri data = intent.getData();
+ if (data != null && !data.toString().isEmpty()) {
+ return data.toString();
+ }
+ }
+ return null;
+ }
+
/**
* Invoke this from {@code Activity#onResume()} or {@code Fragment#onResume()}.
*
@@ -622,8 +638,12 @@ void onRequestPermissionsResult(
void onNewIntent(@NonNull Intent intent) {
ensureAlive();
if (flutterEngine != null) {
- Log.v(TAG, "Forwarding onNewIntent() to FlutterEngine.");
+ Log.v(TAG, "Forwarding onNewIntent() to FlutterEngine and sending pushRoute message.");
flutterEngine.getActivityControlSurface().onNewIntent(intent);
+ String initialRoute = maybeGetInitialRouteFromIntent(intent);
+ if (initialRoute != null && !initialRoute.isEmpty()) {
+ flutterEngine.getNavigationChannel().pushRoute(initialRoute);
+ }
} else {
Log.w(TAG, "onNewIntent() invoked before FlutterFragment was attached to an Activity.");
}
@@ -735,6 +755,10 @@ private void ensureAlive() {
@NonNull
Context getContext();
+ /** Returns true if the delegate should retrieve the initial route from the {@link Intent}. */
+ @Nullable
+ boolean shouldHandleDeeplinking();
+
/**
* Returns the host {@link Activity} or the {@code Activity} that is currently attached to the
* host {@code Fragment}.
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java
index ae7a4a48e67a8..a92e342333765 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java
@@ -16,7 +16,8 @@ public class FlutterActivityLaunchConfigs {
"io.flutter.embedding.android.SplashScreenDrawable";
/* package */ static final String NORMAL_THEME_META_DATA_KEY =
"io.flutter.embedding.android.NormalTheme";
-
+ /* package */ static final String HANDLE_DEEPLINKING_META_DATA_KEY =
+ "flutter_deeplinking_enabled";
// Intent extra arguments.
/* package */ static final String EXTRA_INITIAL_ROUTE = "route";
/* package */ static final String EXTRA_BACKGROUND_MODE = "background_mode";
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java
index 4d2438bd95935..f47dd8f0f280f 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java
@@ -88,6 +88,8 @@ public class FlutterFragment extends Fragment implements FlutterActivityAndFragm
protected static final String ARG_DART_ENTRYPOINT = "dart_entrypoint";
/** Initial Flutter route that is rendered in a Navigator widget. */
protected static final String ARG_INITIAL_ROUTE = "initial_route";
+ /** Whether the activity delegate should handle the deeplinking request. */
+ protected static final String ARG_HANDLE_DEEPLINKING = "handle_deeplinking";
/** Path to Flutter's Dart code. */
protected static final String ARG_APP_BUNDLE_PATH = "app_bundle_path";
/** Flutter shell arguments. */
@@ -185,6 +187,7 @@ public static class NewEngineFragmentBuilder {
private final Class extends FlutterFragment> fragmentClass;
private String dartEntrypoint = "main";
private String initialRoute = "/";
+ private boolean handleDeeplinking = false;
private String appBundlePath = null;
private FlutterShellArgs shellArgs = null;
private RenderMode renderMode = RenderMode.surface;
@@ -224,6 +227,16 @@ public NewEngineFragmentBuilder initialRoute(@NonNull String initialRoute) {
return this;
}
+ /**
+ * Whether to handle the deeplinking from the {@code Intent} automatically if the {@code
+ * getInitialRoute} returns null.
+ */
+ @NonNull
+ public NewEngineFragmentBuilder handleDeeplinking(@NonNull Boolean handleDeeplinking) {
+ this.handleDeeplinking = handleDeeplinking;
+ return this;
+ }
+
/**
* The path to the app bundle which contains the Dart app to execute. Null when unspecified,
* which defaults to {@link FlutterLoader#findAppBundlePath()}
@@ -316,6 +329,7 @@ public NewEngineFragmentBuilder shouldAttachEngineToActivity(
protected Bundle createArgs() {
Bundle args = new Bundle();
args.putString(ARG_INITIAL_ROUTE, initialRoute);
+ args.putBoolean(ARG_HANDLE_DEEPLINKING, handleDeeplinking);
args.putString(ARG_APP_BUNDLE_PATH, appBundlePath);
args.putString(ARG_DART_ENTRYPOINT, dartEntrypoint);
// TODO(mattcarroll): determine if we should have an explicit FlutterTestFragment instead of
@@ -409,6 +423,7 @@ public static class CachedEngineFragmentBuilder {
private final Class extends FlutterFragment> fragmentClass;
private final String engineId;
private boolean destroyEngineWithFragment = false;
+ private boolean handleDeeplinking = false;
private RenderMode renderMode = RenderMode.surface;
private TransparencyMode transparencyMode = TransparencyMode.transparent;
private boolean shouldAttachEngineToActivity = true;
@@ -460,6 +475,16 @@ public CachedEngineFragmentBuilder transparencyMode(
return this;
}
+ /**
+ * Whether to handle the deeplinking from the {@code Intent} automatically if the {@code
+ * getInitialRoute} returns null.
+ */
+ @NonNull
+ public CachedEngineFragmentBuilder handleDeeplinking(@NonNull Boolean handleDeeplinking) {
+ this.handleDeeplinking = handleDeeplinking;
+ return this;
+ }
+
/**
* Whether or not this {@code FlutterFragment} should automatically attach its {@code Activity}
* as a control surface for its {@link FlutterEngine}.
@@ -512,6 +537,7 @@ protected Bundle createArgs() {
Bundle args = new Bundle();
args.putString(ARG_CACHED_ENGINE_ID, engineId);
args.putBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, destroyEngineWithFragment);
+ args.putBoolean(ARG_HANDLE_DEEPLINKING, handleDeeplinking);
args.putString(
ARG_FLUTTERVIEW_RENDER_MODE,
renderMode != null ? renderMode.name() : RenderMode.surface.name());
@@ -1016,6 +1042,15 @@ public boolean shouldAttachEngineToActivity() {
return getArguments().getBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY);
}
+ /**
+ * Whether to handle the deeplinking from the {@code Intent} automatically if the {@code
+ * getInitialRoute} returns null.
+ */
+ @Override
+ public boolean shouldHandleDeeplinking() {
+ return getArguments().getBoolean(ARG_HANDLE_DEEPLINKING);
+ }
+
@Override
public void onFlutterSurfaceViewCreated(@NonNull FlutterSurfaceView flutterSurfaceView) {
// Hook for subclasses.
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java
index ff15581a57b22..f5ef1711e2592 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java
@@ -12,6 +12,7 @@
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_CACHED_ENGINE_ID;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_DESTROY_ENGINE_WITH_ACTIVITY;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_INITIAL_ROUTE;
+import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.HANDLE_DEEPLINKING_META_DATA_KEY;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.INITIAL_ROUTE_META_DATA_KEY;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.NORMAL_THEME_META_DATA_KEY;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.SPLASH_SCREEN_META_DATA_KEY;
@@ -33,6 +34,7 @@
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import io.flutter.Log;
@@ -279,10 +281,9 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
*/
private void switchLaunchThemeForNormalTheme() {
try {
- ActivityInfo activityInfo =
- getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
- if (activityInfo.metaData != null) {
- int normalThemeRID = activityInfo.metaData.getInt(NORMAL_THEME_META_DATA_KEY, -1);
+ Bundle metaData = getMetaData();
+ if (metaData != null) {
+ int normalThemeRID = metaData.getInt(NORMAL_THEME_META_DATA_KEY, -1);
if (normalThemeRID != -1) {
setTheme(normalThemeRID);
}
@@ -318,11 +319,9 @@ public SplashScreen provideSplashScreen() {
@SuppressWarnings("deprecation")
private Drawable getSplashScreenFromManifest() {
try {
- ActivityInfo activityInfo =
- getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
- Bundle metadata = activityInfo.metaData;
+ Bundle metaData = getMetaData();
Integer splashScreenId =
- metadata != null ? metadata.getInt(SPLASH_SCREEN_META_DATA_KEY) : null;
+ metaData != null ? metaData.getInt(SPLASH_SCREEN_META_DATA_KEY) : null;
return splashScreenId != null
? Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP
? getResources().getDrawable(splashScreenId, getTheme())
@@ -423,6 +422,7 @@ protected FlutterFragment createFlutterFragment() {
return FlutterFragment.withCachedEngine(getCachedEngineId())
.renderMode(renderMode)
.transparencyMode(transparencyMode)
+ .handleDeeplinking(shouldHandleDeeplinking())
.shouldAttachEngineToActivity(shouldAttachEngineToActivity())
.destroyEngineWithFragment(shouldDestroyEngineWithHost())
.build();
@@ -450,6 +450,7 @@ protected FlutterFragment createFlutterFragment() {
.initialRoute(getInitialRoute())
.appBundlePath(getAppBundlePath())
.flutterShellArgs(FlutterShellArgs.fromIntent(getIntent()))
+ .handleDeeplinking(shouldHandleDeeplinking())
.renderMode(renderMode)
.transparencyMode(transparencyMode)
.shouldAttachEngineToActivity(shouldAttachEngineToActivity())
@@ -545,6 +546,26 @@ protected boolean shouldAttachEngineToActivity() {
return true;
}
+ /**
+ * Whether to handle the deeplinking from the {@code Intent} automatically if the {@code
+ * getInitialRoute} returns null.
+ *
+ * The default implementation looks {@code } called {@link
+ * FlutterActivityLaunchConfigs#HANDLE_DEEPLINKING_META_DATA_KEY} within the Android manifest
+ * definition for this {@code FlutterFragmentActivity}.
+ */
+ @VisibleForTesting
+ protected boolean shouldHandleDeeplinking() {
+ try {
+ Bundle metaData = getMetaData();
+ boolean shouldHandleDeeplinking =
+ metaData != null ? metaData.getBoolean(HANDLE_DEEPLINKING_META_DATA_KEY) : false;
+ return shouldHandleDeeplinking;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
/** Hook for subclasses to easily provide a custom {@code FlutterEngine}. */
@Nullable
@Override
@@ -608,6 +629,14 @@ protected String getAppBundlePath() {
return null;
}
+ /** Retrieves the meta data specified in the AndroidManifest.xml. */
+ @Nullable
+ protected Bundle getMetaData() throws PackageManager.NameNotFoundException {
+ ActivityInfo activityInfo =
+ getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
+ return activityInfo.metaData;
+ }
+
/**
* The Dart entrypoint that will be executed as soon as the Dart snapshot is loaded.
*
@@ -620,11 +649,9 @@ protected String getAppBundlePath() {
@NonNull
public String getDartEntrypointFunctionName() {
try {
- ActivityInfo activityInfo =
- getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
- Bundle metadata = activityInfo.metaData;
+ Bundle metaData = getMetaData();
String desiredDartEntrypoint =
- metadata != null ? metadata.getString(DART_ENTRYPOINT_META_DATA_KEY) : null;
+ metaData != null ? metaData.getString(DART_ENTRYPOINT_META_DATA_KEY) : null;
return desiredDartEntrypoint != null ? desiredDartEntrypoint : DEFAULT_DART_ENTRYPOINT;
} catch (PackageManager.NameNotFoundException e) {
return DEFAULT_DART_ENTRYPOINT;
@@ -651,22 +678,22 @@ public String getDartEntrypointFunctionName() {
* have control over the incoming {@code Intent}.
*
* Subclasses may override this method to directly control the initial route.
+ *
+ *
If this method returns null and the {@code shouldHandleDeeplinking} returns true, the
+ * initial route is derived from the {@code Intent} through the Intent.getData() instead.
*/
- @NonNull
protected String getInitialRoute() {
if (getIntent().hasExtra(EXTRA_INITIAL_ROUTE)) {
return getIntent().getStringExtra(EXTRA_INITIAL_ROUTE);
}
try {
- ActivityInfo activityInfo =
- getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
- Bundle metadata = activityInfo.metaData;
+ Bundle metaData = getMetaData();
String desiredInitialRoute =
- metadata != null ? metadata.getString(INITIAL_ROUTE_META_DATA_KEY) : null;
- return desiredInitialRoute != null ? desiredInitialRoute : DEFAULT_INITIAL_ROUTE;
+ metaData != null ? metaData.getString(INITIAL_ROUTE_META_DATA_KEY) : null;
+ return desiredInitialRoute;
} catch (PackageManager.NameNotFoundException e) {
- return DEFAULT_INITIAL_ROUTE;
+ return null;
}
}
diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
index 4a621b3344107..bf49fb6871c94 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
@@ -15,6 +15,7 @@
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
+import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import io.flutter.FlutterInjector;
@@ -43,6 +44,7 @@
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.Config;
@Config(manifest = Config.NONE)
@@ -71,6 +73,7 @@ public void setup() {
when(mockHost.getTransparencyMode()).thenReturn(TransparencyMode.transparent);
when(mockHost.provideFlutterEngine(any(Context.class))).thenReturn(mockFlutterEngine);
when(mockHost.shouldAttachEngineToActivity()).thenReturn(true);
+ when(mockHost.shouldHandleDeeplinking()).thenReturn(false);
when(mockHost.shouldDestroyEngineWithHost()).thenReturn(true);
}
@@ -426,6 +429,78 @@ public void itForwardsOnRequestPermissionsResultToFlutterEngine() {
.onRequestPermissionsResult(any(Integer.class), any(String[].class), any(int[].class));
}
+ @Test
+ public void
+ itSendsInitialRouteFromIntentOnStartIfNoInitialRouteFromActivityAndShouldHandleDeeplinking() {
+ Intent intent = FlutterActivity.createDefaultIntent(RuntimeEnvironment.application);
+ intent.setData(Uri.parse("http://myApp/custom/route"));
+
+ ActivityController activityController =
+ Robolectric.buildActivity(FlutterActivity.class, intent);
+ FlutterActivity flutterActivity = activityController.get();
+
+ when(mockHost.getActivity()).thenReturn(flutterActivity);
+ when(mockHost.getInitialRoute()).thenReturn(null);
+ when(mockHost.shouldHandleDeeplinking()).thenReturn(true);
+ // Create the real object that we're testing.
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
+
+ // --- Execute the behavior under test ---
+ // The FlutterEngine is setup in onAttach().
+ delegate.onAttach(RuntimeEnvironment.application);
+ // Emulate app start.
+ delegate.onStart();
+
+ // Verify that the navigation channel was given the initial route message.
+ verify(mockFlutterEngine.getNavigationChannel(), times(1))
+ .setInitialRoute("http://myApp/custom/route");
+ }
+
+ @Test
+ public void itSendsdefaultInitialRouteOnStartIfNotDeepLinkingFromIntent() {
+ // Creates an empty intent without launch uri.
+ Intent intent = FlutterActivity.createDefaultIntent(RuntimeEnvironment.application);
+
+ ActivityController activityController =
+ Robolectric.buildActivity(FlutterActivity.class, intent);
+ FlutterActivity flutterActivity = activityController.get();
+
+ when(mockHost.getActivity()).thenReturn(flutterActivity);
+ when(mockHost.getInitialRoute()).thenReturn(null);
+ when(mockHost.shouldHandleDeeplinking()).thenReturn(true);
+ // Create the real object that we're testing.
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
+
+ // --- Execute the behavior under test ---
+ // The FlutterEngine is setup in onAttach().
+ delegate.onAttach(RuntimeEnvironment.application);
+ // Emulate app start.
+ delegate.onStart();
+
+ // Verify that the navigation channel was given the default initial route message.
+ verify(mockFlutterEngine.getNavigationChannel(), times(1)).setInitialRoute("/");
+ }
+
+ @Test
+ public void itSendsPushRouteMessageWhenOnNewIntent() {
+ when(mockHost.shouldHandleDeeplinking()).thenReturn(true);
+ // Create the real object that we're testing.
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
+
+ // --- Execute the behavior under test ---
+ // The FlutterEngine is setup in onAttach().
+ delegate.onAttach(RuntimeEnvironment.application);
+
+ Intent mockIntent = mock(Intent.class);
+ when(mockIntent.getData()).thenReturn(Uri.parse("http://myApp/custom/route"));
+ // Emulate the host and call the method that we expect to be forwarded.
+ delegate.onNewIntent(mockIntent);
+
+ // Verify that the navigation channel was given the push route message.
+ verify(mockFlutterEngine.getNavigationChannel(), times(1))
+ .pushRoute("http://myApp/custom/route");
+ }
+
@Test
public void itForwardsOnNewIntentToFlutterEngine() {
// Create the real object that we're testing.
diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java
index 2fe23de49b303..19720fa7e6e02 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java
@@ -1,5 +1,6 @@
package io.flutter.embedding.android;
+import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.HANDLE_DEEPLINKING_META_DATA_KEY;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -7,12 +8,14 @@
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -116,6 +119,58 @@ public void itCreatesNewEngineIntentWithRequestedSettings() {
assertEquals(TransparencyMode.transparent, flutterActivity.getTransparencyMode());
}
+ @Test
+ public void itReturnsValueFromMetaDataWhenCallsShouldHandleDeepLinkingCase1()
+ throws PackageManager.NameNotFoundException {
+ Intent intent =
+ FlutterActivity.withNewEngine()
+ .backgroundMode(BackgroundMode.transparent)
+ .build(RuntimeEnvironment.application);
+ ActivityController activityController =
+ Robolectric.buildActivity(FlutterActivity.class, intent);
+ FlutterActivity flutterActivity = activityController.get();
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(HANDLE_DEEPLINKING_META_DATA_KEY, true);
+ FlutterActivity spyFlutterActivity = spy(flutterActivity);
+ when(spyFlutterActivity.getMetaData()).thenReturn(bundle);
+ assertTrue(spyFlutterActivity.shouldHandleDeeplinking());
+ }
+
+ @Test
+ public void itReturnsValueFromMetaDataWhenCallsShouldHandleDeepLinkingCase2()
+ throws PackageManager.NameNotFoundException {
+ Intent intent =
+ FlutterActivity.withNewEngine()
+ .backgroundMode(BackgroundMode.transparent)
+ .build(RuntimeEnvironment.application);
+ ActivityController activityController =
+ Robolectric.buildActivity(FlutterActivity.class, intent);
+ FlutterActivity flutterActivity = activityController.get();
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(HANDLE_DEEPLINKING_META_DATA_KEY, false);
+ FlutterActivity spyFlutterActivity = spy(flutterActivity);
+ when(spyFlutterActivity.getMetaData()).thenReturn(bundle);
+ assertFalse(spyFlutterActivity.shouldHandleDeeplinking());
+ }
+
+ @Test
+ public void itReturnsValueFromMetaDataWhenCallsShouldHandleDeepLinkingCase3()
+ throws PackageManager.NameNotFoundException {
+ Intent intent =
+ FlutterActivity.withNewEngine()
+ .backgroundMode(BackgroundMode.transparent)
+ .build(RuntimeEnvironment.application);
+ ActivityController activityController =
+ Robolectric.buildActivity(FlutterActivity.class, intent);
+ FlutterActivity flutterActivity = activityController.get();
+ // Creates an empty bundle.
+ Bundle bundle = new Bundle();
+ FlutterActivity spyFlutterActivity = spy(flutterActivity);
+ when(spyFlutterActivity.getMetaData()).thenReturn(bundle);
+ // Empty bundle should return false.
+ assertFalse(spyFlutterActivity.shouldHandleDeeplinking());
+ }
+
@Test
public void itCreatesCachedEngineIntentThatDoesNotDestroyTheEngine() {
Intent intent =
diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java
index 5cede33a1b833..51532b9c96143 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java
@@ -358,6 +358,11 @@ public boolean shouldAttachEngineToActivity() {
return true;
}
+ @Override
+ public boolean shouldHandleDeeplinking() {
+ return false;
+ }
+
@Override
public boolean shouldRestoreAndSaveState() {
return true;
diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java
index cd311a835588e..448646db9c793 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java
@@ -1,12 +1,17 @@
package io.flutter.embedding.android;
+import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.HANDLE_DEEPLINKING_META_DATA_KEY;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.embedding.android.FlutterActivityLaunchConfigs.BackgroundMode;
@@ -82,6 +87,46 @@ public void itRegistersPluginsAtConfigurationTime() {
assertEquals(activity.getFlutterEngine(), registeredEngines.get(0));
}
+ @Test
+ public void itReturnsValueFromMetaDataWhenCallsShouldHandleDeepLinkingCase1()
+ throws PackageManager.NameNotFoundException {
+ FlutterFragmentActivity activity =
+ Robolectric.buildActivity(FlutterFragmentActivityWithProvidedEngine.class).get();
+ assertTrue(GeneratedPluginRegistrant.getRegisteredEngines().isEmpty());
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(HANDLE_DEEPLINKING_META_DATA_KEY, true);
+ FlutterFragmentActivity spyFlutterActivity = spy(activity);
+ when(spyFlutterActivity.getMetaData()).thenReturn(bundle);
+ assertTrue(spyFlutterActivity.shouldHandleDeeplinking());
+ }
+
+ @Test
+ public void itReturnsValueFromMetaDataWhenCallsShouldHandleDeepLinkingCase2()
+ throws PackageManager.NameNotFoundException {
+ FlutterFragmentActivity activity =
+ Robolectric.buildActivity(FlutterFragmentActivityWithProvidedEngine.class).get();
+ assertTrue(GeneratedPluginRegistrant.getRegisteredEngines().isEmpty());
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(HANDLE_DEEPLINKING_META_DATA_KEY, false);
+ FlutterFragmentActivity spyFlutterActivity = spy(activity);
+ when(spyFlutterActivity.getMetaData()).thenReturn(bundle);
+ assertFalse(spyFlutterActivity.shouldHandleDeeplinking());
+ }
+
+ @Test
+ public void itReturnsValueFromMetaDataWhenCallsShouldHandleDeepLinkingCase3()
+ throws PackageManager.NameNotFoundException {
+ FlutterFragmentActivity activity =
+ Robolectric.buildActivity(FlutterFragmentActivityWithProvidedEngine.class).get();
+ assertTrue(GeneratedPluginRegistrant.getRegisteredEngines().isEmpty());
+ // Creates an empty bundle.
+ Bundle bundle = new Bundle();
+ FlutterFragmentActivity spyFlutterActivity = spy(activity);
+ when(spyFlutterActivity.getMetaData()).thenReturn(bundle);
+ // Empty bundle should return false.
+ assertFalse(spyFlutterActivity.shouldHandleDeeplinking());
+ }
+
static class FlutterFragmentActivityWithProvidedEngine extends FlutterFragmentActivity {
@Override
protected FlutterFragment createFlutterFragment() {
@@ -119,6 +164,11 @@ protected String getInitialRoute() {
protected String getAppBundlePath() {
return "";
}
+
+ @Override
+ protected boolean shouldHandleDeeplinking() {
+ return false;
+ }
}
// This is just a compile time check to ensure that it's possible for FlutterFragmentActivity
diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java
index 9327994e8d9e6..ecaafe02f7e9a 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java
@@ -27,6 +27,7 @@ public void itCreatesDefaultFragmentWithExpectedDefaults() {
assertEquals("/", fragment.getInitialRoute());
assertArrayEquals(new String[] {}, fragment.getFlutterShellArgs().toArray());
assertTrue(fragment.shouldAttachEngineToActivity());
+ assertFalse(fragment.shouldHandleDeeplinking());
assertNull(fragment.getCachedEngineId());
assertTrue(fragment.shouldDestroyEngineWithHost());
assertEquals(RenderMode.surface, fragment.getRenderMode());
@@ -40,6 +41,7 @@ public void itCreatesNewEngineFragmentWithRequestedSettings() {
.dartEntrypoint("custom_entrypoint")
.initialRoute("/custom/route")
.shouldAttachEngineToActivity(false)
+ .handleDeeplinking(true)
.renderMode(RenderMode.texture)
.transparencyMode(TransparencyMode.opaque)
.build();
@@ -49,6 +51,7 @@ public void itCreatesNewEngineFragmentWithRequestedSettings() {
assertEquals("/custom/route", fragment.getInitialRoute());
assertArrayEquals(new String[] {}, fragment.getFlutterShellArgs().toArray());
assertFalse(fragment.shouldAttachEngineToActivity());
+ assertTrue(fragment.shouldHandleDeeplinking());
assertNull(fragment.getCachedEngineId());
assertTrue(fragment.shouldDestroyEngineWithHost());
assertEquals(RenderMode.texture, fragment.getRenderMode());