diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index 66c2acd8e886b..1a6d48f160dde 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -774,13 +774,18 @@ public String getDartEntrypointFunctionName() { * * If both preferences are set, the {@code Intent} preference takes priority. * + *

If none is set, the {@link FlutterActivityAndFragmentDelegate} retrieves the initial route + * from the {@code Intent} through the Intent.getData() instead. + * *

The reason that a {@code } preference is supported is because this {@code * Activity} might be the very first {@code Activity} launched, which means the developer won't * have control over the incoming {@code Intent}. * *

Subclasses may override this method to directly control the initial route. + * + *

If this method returns null, the {@link FlutterActivityAndFragmentDelegate} retrieves the + * initial route 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); @@ -792,9 +797,9 @@ public String getInitialRoute() { Bundle metadata = activityInfo.metaData; 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; } } diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index 78585172dd683..1ecbd0fafc908 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -9,6 +9,7 @@ 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,18 +363,21 @@ private void doInitialFlutterViewRun() { // So this is expected behavior in many cases. return; } - + String initialRoute = host.getInitialRoute(); + if (initialRoute == null) { + initialRoute = getInitialRouteFromIntent(host.getActivity().getIntent()); + } 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()); + if (initialRoute != null) { + flutterEngine.getNavigationChannel().setInitialRoute(initialRoute); } String appBundlePathOverride = host.getAppBundlePath(); @@ -388,6 +392,14 @@ private void doInitialFlutterViewRun() { flutterEngine.getDartExecutor().executeDartEntrypoint(entrypoint); } + private String getInitialRouteFromIntent(Intent intent) { + 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 +634,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 = getInitialRouteFromIntent(intent); + if (initialRoute != null && !initialRoute.isEmpty()) { + flutterEngine.getNavigationChannel().pushRoute(initialRoute); + } } else { Log.w(TAG, "onNewIntent() invoked before FlutterFragment was attached to an Activity."); } diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java index ff15581a57b22..486d36925d8b1 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java @@ -646,13 +646,18 @@ public String getDartEntrypointFunctionName() { * * If both preferences are set, the {@code Intent} preference takes priority. * + *

If none is set, the {@link FlutterActivityAndFragmentDelegate} retrieves the initial route + * from the {@code Intent} through the Intent.getData() instead. + * *

The reason that a {@code } preference is supported is because this {@code * Activity} might be the very first {@code Activity} launched, which means the developer won't * have control over the incoming {@code Intent}. * *

Subclasses may override this method to directly control the initial route. + * + *

If this method returns null, the {@link FlutterActivityAndFragmentDelegate} retrieves the + * initial route 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); @@ -664,9 +669,9 @@ protected String getInitialRoute() { Bundle metadata = activityInfo.metaData; 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; } } 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..329f730175627 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) @@ -426,6 +428,50 @@ public void itForwardsOnRequestPermissionsResultToFlutterEngine() { .onRequestPermissionsResult(any(Integer.class), any(String[].class), any(int[].class)); } + @Test + public void itSendsInitialRouteFromIntentOnStartIfnoInitialRouteFromActivity() { + 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); + // 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 itSendsPushRouteMessageWhenOnNewIntent() { + // 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.