From 263525b2dc89d7cea0ab2f64298dcbb910079b84 Mon Sep 17 00:00:00 2001 From: Nils Strelow Date: Sat, 10 Jul 2021 17:38:18 +0200 Subject: [PATCH 1/4] iOS deeplink sends fragment as well --- .../ios/framework/Source/FlutterAppDelegate.mm | 15 ++++++++++----- .../framework/Source/FlutterAppDelegateTest.mm | 5 +++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm index 472a6e8b1fd44..4ad72abfdb71d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm @@ -159,13 +159,18 @@ - (BOOL)application:(UIApplication*)application FML_LOG(ERROR) << "Timeout waiting for the first frame when launching an URL."; } else { - NSString* pathAndQuery = url.path; + NSString* pathAndQueryAndFragment = url.path; if ([url.query length] != 0) { - pathAndQuery = - [NSString stringWithFormat:@"%@?%@", pathAndQuery, url.query]; + pathAndQueryAndFragment = [NSString + stringWithFormat:@"%@?%@", pathAndQueryAndFragment, url.query]; } - [flutterViewController.engine.navigationChannel invokeMethod:@"pushRoute" - arguments:pathAndQuery]; + if ([url.fragment length] != 0) { + pathAndQueryAndFragment = [NSString + stringWithFormat:@"%@#%@", pathAndQueryAndFragment, url.fragment]; + } + [flutterViewController.engine.navigationChannel + invokeMethod:@"pushRoute" + arguments:pathAndQueryAndFragment]; } }]; return YES; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm index 6a62697dbfe3f..ef92842c160ec 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm @@ -30,7 +30,7 @@ - (void)skip_testLaunchUrl { appDelegate.rootFlutterViewControllerGetter = ^{ return viewController; }; - NSURL* url = [NSURL URLWithString:@"http://myApp/custom/route?query=test"]; + NSURL* url = [NSURL URLWithString:@"http://myApp/custom/route?query=test#fragment"]; NSDictionary* options = @{}; BOOL result = [appDelegate application:[UIApplication sharedApplication] openURL:url @@ -39,7 +39,8 @@ - (void)skip_testLaunchUrl { return @{@"FlutterDeepLinkingEnabled" : @(YES)}; }]; XCTAssertTrue(result); - OCMVerify([navigationChannel invokeMethod:@"pushRoute" arguments:@"/custom/route?query=test"]); + OCMVerify([navigationChannel invokeMethod:@"pushRoute" + arguments:@"/custom/route?query=test#fragment"]); } @end From 23cf9bed66a8af2e2cb1ae77a6772afb22fe7091 Mon Sep 17 00:00:00 2001 From: Nils Strelow Date: Sat, 10 Jul 2021 17:38:27 +0200 Subject: [PATCH 2/4] Android deeplink sends fragment as well --- .../android/FlutterActivityAndFragmentDelegate.java | 9 ++++++--- .../android/FlutterActivityAndFragmentDelegateTest.java | 9 +++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index a789a7a95a82b..c38701012bd8c 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 pathAndQueryAndFragment = data.getPath(); if (data.getQuery() != null && !data.getQuery().isEmpty()) { - pathAndQuery += "?" + data.getQuery(); + pathAndQueryAndFragment += "?" + data.getQuery(); } - return pathAndQuery; + if (data.getFragment() != null && !data.getFragment().isEmpty()) { + pathAndQueryAndFragment += "#" + data.getFragment(); + } + return pathAndQueryAndFragment; } } 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..3d4c505fa211e 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -433,7 +433,7 @@ public void itForwardsOnRequestPermissionsResultToFlutterEngine() { public void itSendsInitialRouteFromIntentOnStartIfNoInitialRouteFromActivityAndShouldHandleDeeplinking() { Intent intent = FlutterActivity.createDefaultIntent(RuntimeEnvironment.application); - intent.setData(Uri.parse("http://myApp/custom/route?query=test")); + intent.setData(Uri.parse("http://myApp/custom/route?query=test#fragment")); ActivityController activityController = Robolectric.buildActivity(FlutterActivity.class, intent); @@ -453,7 +453,7 @@ public void itForwardsOnRequestPermissionsResultToFlutterEngine() { // Verify that the navigation channel was given the initial route message. verify(mockFlutterEngine.getNavigationChannel(), times(1)) - .setInitialRoute("/custom/route?query=test"); + .setInitialRoute("/custom/route?query=test#fragment"); } @Test @@ -518,13 +518,14 @@ public void itSendsPushRouteMessageWhenOnNewIntent() { delegate.onAttach(RuntimeEnvironment.application); Intent mockIntent = mock(Intent.class); - when(mockIntent.getData()).thenReturn(Uri.parse("http://myApp/custom/route?query=test")); + 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"); + .pushRoute("/custom/route?query=test#fragment"); } @Test From 4af603ca0e4a48fb08b519712fa1c518e913f208 Mon Sep 17 00:00:00 2001 From: Nils Strelow Date: Thu, 15 Jul 2021 18:45:42 +0200 Subject: [PATCH 3/4] Rename pathAndQueryAndFragment -> fullRoute --- .../android/FlutterActivityAndFragmentDelegate.java | 8 ++++---- .../ios/framework/Source/FlutterAppDelegate.mm | 13 +++++-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index c38701012bd8c..5d504bb1317dd 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -405,14 +405,14 @@ private String maybeGetInitialRouteFromIntent(Intent intent) { if (host.shouldHandleDeeplinking()) { Uri data = intent.getData(); if (data != null && !data.getPath().isEmpty()) { - String pathAndQueryAndFragment = data.getPath(); + String fullRoute = data.getPath(); if (data.getQuery() != null && !data.getQuery().isEmpty()) { - pathAndQueryAndFragment += "?" + data.getQuery(); + fullRoute += "?" + data.getQuery(); } if (data.getFragment() != null && !data.getFragment().isEmpty()) { - pathAndQueryAndFragment += "#" + data.getFragment(); + fullRoute += "#" + data.getFragment(); } - return pathAndQueryAndFragment; + return fullRoute; } } return null; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm index 4ad72abfdb71d..32b3fe0eb1e06 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm @@ -159,18 +159,15 @@ - (BOOL)application:(UIApplication*)application FML_LOG(ERROR) << "Timeout waiting for the first frame when launching an URL."; } else { - NSString* pathAndQueryAndFragment = url.path; + NSString* fullRoute = url.path; if ([url.query length] != 0) { - pathAndQueryAndFragment = [NSString - stringWithFormat:@"%@?%@", pathAndQueryAndFragment, url.query]; + fullRoute = [NSString stringWithFormat:@"%@?%@", fullRoute, url.query]; } if ([url.fragment length] != 0) { - pathAndQueryAndFragment = [NSString - stringWithFormat:@"%@#%@", pathAndQueryAndFragment, url.fragment]; + fullRoute = [NSString stringWithFormat:@"%@#%@", fullRoute, url.fragment]; } - [flutterViewController.engine.navigationChannel - invokeMethod:@"pushRoute" - arguments:pathAndQueryAndFragment]; + [flutterViewController.engine.navigationChannel invokeMethod:@"pushRoute" + arguments:fullRoute]; } }]; return YES; From d0319b971e18acbdb36ddc4e0e177ee51b5d72c7 Mon Sep 17 00:00:00 2001 From: Nils Strelow Date: Thu, 15 Jul 2021 18:45:58 +0200 Subject: [PATCH 4/4] Add test cases for with and without fragment --- ...lutterActivityAndFragmentDelegateTest.java | 93 +++++++++++++++++++ .../Source/FlutterAppDelegateTest.mm | 46 +++++++++ 2 files changed, 139 insertions(+) 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 3d4c505fa211e..d2f6e42faa498 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -433,6 +433,33 @@ public void itForwardsOnRequestPermissionsResultToFlutterEngine() { public void itSendsInitialRouteFromIntentOnStartIfNoInitialRouteFromActivityAndShouldHandleDeeplinking() { Intent intent = FlutterActivity.createDefaultIntent(RuntimeEnvironment.application); + intent.setData(Uri.parse("http://myApp/custom/route?query=test")); + + 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"); + } + + @Test + public void + itSendsInitialRouteFromIntentOnStartIfNoInitialRouteFromActivityAndShouldHandleDeeplinkingWithQueryParameterAndFragment() { + Intent intent = FlutterActivity.createDefaultIntent(RuntimeEnvironment.application); intent.setData(Uri.parse("http://myApp/custom/route?query=test#fragment")); ActivityController activityController = @@ -456,6 +483,33 @@ public void itForwardsOnRequestPermissionsResultToFlutterEngine() { .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() { @@ -517,6 +571,26 @@ public void itSendsPushRouteMessageWhenOnNewIntent() { // 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")); + // 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"); + } + + @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")); @@ -528,6 +602,25 @@ public void itSendsPushRouteMessageWhenOnNewIntent() { .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/FlutterAppDelegateTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm index ef92842c160ec..9c275da9e955a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm @@ -20,6 +20,29 @@ @implementation FlutterAppDelegateTest // TODO(dnfield): https://github.com/flutter/flutter/issues/74267 - (void)skip_testLaunchUrl { + 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"]; + 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"]); +} + +- (void)skip_testLaunchUrlWithQueryParameterAndFragment { FlutterAppDelegate* appDelegate = [[FlutterAppDelegate alloc] init]; FlutterViewController* viewController = OCMClassMock([FlutterViewController class]); FlutterEngine* engine = OCMClassMock([FlutterEngine class]); @@ -43,4 +66,27 @@ - (void)skip_testLaunchUrl { 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