diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index a789a7a95a82b..5d504bb1317dd 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -405,11 +405,14 @@ private String maybeGetInitialRouteFromIntent(Intent intent) { if (host.shouldHandleDeeplinking()) { Uri data = intent.getData(); if (data != null && !data.getPath().isEmpty()) { - String pathAndQuery = data.getPath(); + String fullRoute = data.getPath(); if (data.getQuery() != null && !data.getQuery().isEmpty()) { - pathAndQuery += "?" + data.getQuery(); + fullRoute += "?" + data.getQuery(); } - return pathAndQuery; + if (data.getFragment() != null && !data.getFragment().isEmpty()) { + fullRoute += "#" + data.getFragment(); + } + return fullRoute; } } 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 8dbe0ba8dfb58..d2f6e42faa498 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -456,6 +456,60 @@ public void itForwardsOnRequestPermissionsResultToFlutterEngine() { .setInitialRoute("/custom/route?query=test"); } + @Test + public void + itSendsInitialRouteFromIntentOnStartIfNoInitialRouteFromActivityAndShouldHandleDeeplinkingWithQueryParameterAndFragment() { + Intent intent = FlutterActivity.createDefaultIntent(RuntimeEnvironment.application); + intent.setData(Uri.parse("http://myApp/custom/route?query=test#fragment")); + + 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 set up 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("/custom/route?query=test#fragment"); + } + + @Test + public void + itSendsInitialRouteFromIntentOnStartIfNoInitialRouteFromActivityAndShouldHandleDeeplinkingWithFragmentNoQueryParameter() { + Intent intent = FlutterActivity.createDefaultIntent(RuntimeEnvironment.application); + intent.setData(Uri.parse("http://myApp/custom/route#fragment")); + + 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 set up 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("/custom/route#fragment"); + } + @Test public void itSendsInitialRouteFromIntentOnStartIfNoInitialRouteFromActivityAndShouldHandleDeeplinkingNoQueryParameter() { @@ -527,6 +581,46 @@ public void itSendsPushRouteMessageWhenOnNewIntent() { .pushRoute("/custom/route?query=test"); } + @Test + public void itSendsPushRouteMessageWhenOnNewIntentWithQueryParameterAndFragment() { + 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 set up in onAttach(). + delegate.onAttach(RuntimeEnvironment.application); + + Intent mockIntent = mock(Intent.class); + when(mockIntent.getData()) + .thenReturn(Uri.parse("http://myApp/custom/route?query=test#fragment")); + // 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("/custom/route?query=test#fragment"); + } + + @Test + public void itSendsPushRouteMessageWhenOnNewIntentWithFragmentNoQueryParameter() { + 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 set up in onAttach(). + delegate.onAttach(RuntimeEnvironment.application); + + Intent mockIntent = mock(Intent.class); + when(mockIntent.getData()).thenReturn(Uri.parse("http://myApp/custom/route#fragment")); + // 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("/custom/route#fragment"); + } + @Test public void itSendsPushRouteMessageWhenOnNewIntentNoQueryParameter() { when(mockHost.shouldHandleDeeplinking()).thenReturn(true); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm index 472a6e8b1fd44..32b3fe0eb1e06 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm @@ -159,13 +159,15 @@ - (BOOL)application:(UIApplication*)application FML_LOG(ERROR) << "Timeout waiting for the first frame when launching an URL."; } else { - NSString* pathAndQuery = url.path; + NSString* fullRoute = url.path; if ([url.query length] != 0) { - pathAndQuery = - [NSString stringWithFormat:@"%@?%@", pathAndQuery, url.query]; + fullRoute = [NSString stringWithFormat:@"%@?%@", fullRoute, url.query]; + } + if ([url.fragment length] != 0) { + fullRoute = [NSString stringWithFormat:@"%@#%@", fullRoute, url.fragment]; } [flutterViewController.engine.navigationChannel invokeMethod:@"pushRoute" - arguments:pathAndQuery]; + arguments:fullRoute]; } }]; return YES; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm index 6a62697dbfe3f..9c275da9e955a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm @@ -42,4 +42,51 @@ - (void)skip_testLaunchUrl { OCMVerify([navigationChannel invokeMethod:@"pushRoute" arguments:@"/custom/route?query=test"]); } +- (void)skip_testLaunchUrlWithQueryParameterAndFragment { + FlutterAppDelegate* appDelegate = [[FlutterAppDelegate alloc] init]; + FlutterViewController* viewController = OCMClassMock([FlutterViewController class]); + FlutterEngine* engine = OCMClassMock([FlutterEngine class]); + FlutterMethodChannel* navigationChannel = OCMClassMock([FlutterMethodChannel class]); + OCMStub([engine navigationChannel]).andReturn(navigationChannel); + OCMStub([viewController engine]).andReturn(engine); + OCMStub([engine waitForFirstFrame:3.0 callback:([OCMArg invokeBlockWithArgs:@(NO), nil])]); + appDelegate.rootFlutterViewControllerGetter = ^{ + return viewController; + }; + NSURL* url = [NSURL URLWithString:@"http://myApp/custom/route?query=test#fragment"]; + NSDictionary* options = @{}; + BOOL result = [appDelegate application:[UIApplication sharedApplication] + openURL:url + options:options + infoPlistGetter:^NSDictionary*() { + return @{@"FlutterDeepLinkingEnabled" : @(YES)}; + }]; + XCTAssertTrue(result); + OCMVerify([navigationChannel invokeMethod:@"pushRoute" + arguments:@"/custom/route?query=test#fragment"]); +} + +- (void)skip_testLaunchUrlWithFragmentNoQueryParameter { + FlutterAppDelegate* appDelegate = [[FlutterAppDelegate alloc] init]; + FlutterViewController* viewController = OCMClassMock([FlutterViewController class]); + FlutterEngine* engine = OCMClassMock([FlutterEngine class]); + FlutterMethodChannel* navigationChannel = OCMClassMock([FlutterMethodChannel class]); + OCMStub([engine navigationChannel]).andReturn(navigationChannel); + OCMStub([viewController engine]).andReturn(engine); + OCMStub([engine waitForFirstFrame:3.0 callback:([OCMArg invokeBlockWithArgs:@(NO), nil])]); + appDelegate.rootFlutterViewControllerGetter = ^{ + return viewController; + }; + NSURL* url = [NSURL URLWithString:@"http://myApp/custom/route#fragment"]; + NSDictionary* options = @{}; + BOOL result = [appDelegate application:[UIApplication sharedApplication] + openURL:url + options:options + infoPlistGetter:^NSDictionary*() { + return @{@"FlutterDeepLinkingEnabled" : @(YES)}; + }]; + XCTAssertTrue(result); + OCMVerify([navigationChannel invokeMethod:@"pushRoute" arguments:@"/custom/route#fragment"]); +} + @end