-
Notifications
You must be signed in to change notification settings - Fork 6k
reland support uri launch in android #22363
Changes from 6 commits
cda2c3b
5fe12e9
bce3134
6b09ef5
fe25a89
bc1059c
33428d1
9822d25
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,9 +485,7 @@ 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(); | ||
| int splashScreenId = metadata != null ? metadata.getInt(SPLASH_SCREEN_META_DATA_KEY) : 0; | ||
| return splashScreenId != 0 | ||
| ? Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP | ||
|
|
@@ -748,9 +746,7 @@ 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; | ||
| return desiredDartEntrypoint != null ? desiredDartEntrypoint : DEFAULT_DART_ENTRYPOINT; | ||
|
|
@@ -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; | ||
| return desiredInitialRoute; | ||
| } catch (PackageManager.NameNotFoundException e) { | ||
| return DEFAULT_INITIAL_ROUTE; | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -894,6 +890,25 @@ protected FlutterEngine getFlutterEngine() { | |
| return delegate.getFlutterEngine(); | ||
| } | ||
|
|
||
| private Bundle cachedMetaData; | ||
|
|
||
| /** Mocks the meta data for testing purposes. */ | ||
| @VisibleForTesting | ||
| public void setMetaData(Bundle metaData) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If your test is in the same package, you don't need public. Just But also, don't add this method :) Just use https://javadoc.io/static/org.mockito/mockito-core/3.2.4/org/mockito/Mockito.html#spy-T- on getMetaData in tests (or just override it in a subclass for the class-under-test). |
||
| cachedMetaData = metaData; | ||
| }; | ||
|
|
||
| /** Retrieves the meta data specified in the AndroidManifest.xml. */ | ||
| @Nullable | ||
| protected Bundle getMetaData() throws PackageManager.NameNotFoundException { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| if (cachedMetaData == null) { | ||
| ActivityInfo activityInfo = | ||
| getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA); | ||
| cachedMetaData = activityInfo.metaData; | ||
| } | ||
| return cachedMetaData; | ||
| } | ||
|
|
||
| @Nullable | ||
| @Override | ||
| public PlatformPlugin providePlatformPlugin( | ||
|
|
@@ -970,6 +985,25 @@ 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 for the value of the key `flutter_handle_deeplinking` in | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Be explicit about the metadata tag. Just saying key value in an AndroidManifest will be very hard to decipher. |
||
| * the AndroidManifest.xml. | ||
| */ | ||
| @Override | ||
| public boolean shouldHandleDeeplinking() { | ||
|
chunhtai marked this conversation as resolved.
|
||
| 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. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made it default to |
||
| } | ||
| } | ||
| 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(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry, I don't love this. Don't do any of this and just use https://javadoc.io/static/org.mockito/mockito-core/3.2.4/org/mockito/Mockito.html#spy-T-