Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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;
Expand All @@ -779,22 +775,22 @@ public String getDartEntrypointFunctionName() {
* have control over the incoming {@code Intent}.
*
* <p>Subclasses may override this method to directly control the initial route.
*
* <p>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;
}
}

Expand Down Expand Up @@ -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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored this bundle getter out so i can mock metadata during the test.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The throw here is a bit burdensome for the subclasser. I would just re-throw it in a RuntimeException since it should be very unusual.

ActivityInfo activityInfo =
getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
return activityInfo.metaData;
}

@Nullable
@Override
public PlatformPlugin providePlatformPlugin(
Expand Down Expand Up @@ -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.
*
* <p>The default implementation looks {@code <meta-data>} 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made it default to /, it matches the original behavior

}
}
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()) {
Expand All @@ -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()}.
*
Expand Down Expand Up @@ -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.");
}
Expand Down Expand Up @@ -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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note the classes on flutteractivity/fragment are the user facing docs. This interface is package private.


/**
* Returns the host {@link Activity} or the {@code Activity} that is currently attached to the
* host {@code Fragment}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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()}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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}.
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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.
Expand Down
Loading