[url_launcher] Migrate pushRouteNameToFramework to ChannelBuffers (#3893)

diff --git a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md
index 72fa831..06a2efe 100644
--- a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md
+++ b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.0.3
+
+* Migrate `pushRouteNameToFramework` to use ChannelBuffers API.
+
 ## 2.0.2
 
 * Update platform_plugin_interface version requirement.
diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/link.dart b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart
index 9784a3d..4a414ae 100644
--- a/packages/url_launcher/url_launcher_platform_interface/lib/link.dart
+++ b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart
@@ -3,10 +3,10 @@
 // found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:ui';
+import 'dart:ui' as ui;
 
-import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
 
 /// Signature for a function provided by the [Link] widget that instructs it to
 /// follow the link.
@@ -73,45 +73,32 @@
   bool get isDisabled;
 }
 
+typedef _SendMessage = Function(String, ByteData?, void Function(ByteData?));
+
 /// Pushes the [routeName] into Flutter's navigation system via a platform
 /// message.
-Future<ByteData> pushRouteNameToFramework(
-  BuildContext context,
-  String routeName, {
-  @visibleForTesting bool debugForceRouter = false,
-}) {
-  final PlatformMessageCallback? onPlatformMessage = window.onPlatformMessage;
-  if (onPlatformMessage == null) {
-    return Future<ByteData>.value(null);
-  }
+///
+/// The platform is notified using [SystemNavigator.routeInformationUpdated]. On
+/// older versions of Flutter, this means it will not work unless the
+/// application uses a [Router] (e.g. using [MaterialApp.router]).
+///
+/// Returns the raw data returned by the framework.
+// TODO(ianh): Remove the first argument.
+Future<ByteData> pushRouteNameToFramework(Object? _, String routeName) {
   final Completer<ByteData> completer = Completer<ByteData>();
-  if (debugForceRouter || _hasRouter(context)) {
-    SystemNavigator.routeInformationUpdated(location: routeName);
-    onPlatformMessage(
-      'flutter/navigation',
-      _codec.encodeMethodCall(
-        MethodCall('pushRouteInformation', <dynamic, dynamic>{
-          'location': routeName,
-          'state': null,
-        }),
-      ),
-      completer.complete,
-    );
-  } else {
-    onPlatformMessage(
-      'flutter/navigation',
-      _codec.encodeMethodCall(MethodCall('pushRoute', routeName)),
-      completer.complete,
-    );
-  }
+  SystemNavigator.routeInformationUpdated(location: routeName);
+  final _SendMessage sendMessage =
+      WidgetsBinding.instance?.platformDispatcher.onPlatformMessage ??
+          ui.channelBuffers.push;
+  sendMessage(
+    'flutter/navigation',
+    _codec.encodeMethodCall(
+      MethodCall('pushRouteInformation', <dynamic, dynamic>{
+        'location': routeName,
+        'state': null,
+      }),
+    ),
+    completer.complete,
+  );
   return completer.future;
 }
-
-bool _hasRouter(BuildContext context) {
-  try {
-    return Router.of(context) != null;
-  } on AssertionError {
-    // When a `Router` can't be found, an assertion error is thrown.
-    return false;
-  }
-}
diff --git a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml
index 4411566..80002d1 100644
--- a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml
@@ -3,7 +3,7 @@
 homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_platform_interface
 # NOTE: We strongly prefer non-breaking changes, even at the expense of a
 # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
-version: 2.0.2
+version: 2.0.3
 
 dependencies:
   flutter:
diff --git a/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart b/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart
index 638a655..1f45b59 100644
--- a/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart
+++ b/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart
@@ -2,72 +2,87 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:ui';
+import 'dart:collection';
 
-import 'package:mockito/mockito.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter/widgets.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
 
 import 'package:url_launcher_platform_interface/link.dart';
 
-final MethodCodec _codec = const JSONMethodCodec();
-
 void main() {
-  TestWidgetsFlutterBinding.ensureInitialized();
-
-  PlatformMessageCallback? oldHandler;
-  MethodCall? lastCall;
-
-  setUp(() {
-    oldHandler = window.onPlatformMessage;
-    window.onPlatformMessage = (
-      String name,
-      ByteData? data,
-      PlatformMessageResponseCallback? callback,
-    ) {
-      lastCall = _codec.decodeMethodCall(data);
-      if (callback != null) {
-        callback(_codec.encodeSuccessEnvelope(true));
-      }
-    };
+  testWidgets('Link with Navigator', (WidgetTester tester) async {
+    await tester.pumpWidget(MaterialApp(
+      home: Placeholder(key: Key('home')),
+      routes: <String, WidgetBuilder>{
+        '/a': (BuildContext context) => Placeholder(key: Key('a')),
+      },
+    ));
+    expect(find.byKey(Key('home')), findsOneWidget);
+    expect(find.byKey(Key('a')), findsNothing);
+    await pushRouteNameToFramework(null, '/a');
+    // start animation
+    await tester.pump();
+    // skip past animation (5s is arbitrary, just needs to be long enough)
+    await tester.pump(const Duration(seconds: 5));
+    expect(find.byKey(Key('a')), findsOneWidget);
+    expect(find.byKey(Key('home')), findsNothing);
   });
 
-  tearDown(() {
-    window.onPlatformMessage = oldHandler;
+  testWidgets('Link with Navigator', (WidgetTester tester) async {
+    await tester.pumpWidget(MaterialApp.router(
+      routeInformationParser: _RouteInformationParser(),
+      routerDelegate: _RouteDelegate(),
+    ));
+    expect(find.byKey(Key('/')), findsOneWidget);
+    expect(find.byKey(Key('/a')), findsNothing);
+    await pushRouteNameToFramework(null, '/a');
+    // start animation
+    await tester.pump();
+    // skip past animation (5s is arbitrary, just needs to be long enough)
+    await tester.pump(const Duration(seconds: 5));
+    expect(find.byKey(Key('/a')), findsOneWidget);
+    expect(find.byKey(Key('/')), findsNothing);
   });
-
-  test('pushRouteNameToFramework() calls pushRoute when no Router', () async {
-    await pushRouteNameToFramework(CustomBuildContext(), '/foo/bar');
-    expect(
-      lastCall,
-      isMethodCall(
-        'pushRoute',
-        arguments: '/foo/bar',
-      ),
-    );
-  });
-
-  test(
-    'pushRouteNameToFramework() calls pushRouteInformation when Router exists',
-    () async {
-      await pushRouteNameToFramework(
-        CustomBuildContext(),
-        '/foo/bar',
-        debugForceRouter: true,
-      );
-      expect(
-        lastCall,
-        isMethodCall(
-          'pushRouteInformation',
-          arguments: <dynamic, dynamic>{
-            'location': '/foo/bar',
-            'state': null,
-          },
-        ),
-      );
-    },
-  );
 }
 
-class CustomBuildContext<T> extends Mock implements BuildContext {}
+class _RouteInformationParser extends RouteInformationParser<RouteInformation> {
+  @override
+  Future<RouteInformation> parseRouteInformation(
+      RouteInformation routeInformation) {
+    return SynchronousFuture(routeInformation);
+  }
+
+  @override
+  RouteInformation? restoreRouteInformation(RouteInformation configuration) {
+    return configuration;
+  }
+}
+
+class _RouteDelegate extends RouterDelegate<RouteInformation>
+    with ChangeNotifier {
+  final Queue<RouteInformation> _history = Queue<RouteInformation>();
+
+  @override
+  Future<void> setNewRoutePath(RouteInformation configuration) {
+    _history.add(configuration);
+    return SynchronousFuture(null);
+  }
+
+  @override
+  Future<bool> popRoute() {
+    if (_history.isEmpty) {
+      return SynchronousFuture(false);
+    }
+    _history.removeLast();
+    return SynchronousFuture(true);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (_history.isEmpty) {
+      return Placeholder(key: Key('empty'));
+    }
+    return Placeholder(key: Key('${_history.last.location}'));
+  }
+}