Prepare url_launcher for the Link widget (#3154)

diff --git a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md
index 768042b..10057e1 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 @@
+## 1.0.9
+
+* Laid the groundwork for introducing a Link widget.
+
 ## 1.0.8
 
 * Added webOnlyWindowName parameter
diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/link.dart b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart
new file mode 100644
index 0000000..425dc88
--- /dev/null
+++ b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart
@@ -0,0 +1,113 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+/// Signature for a function provided by the [Link] widget that instructs it to
+/// follow the link.
+typedef FollowLink = Future<void> Function();
+
+/// Signature for a builder function passed to the [Link] widget to construct
+/// the widget tree under it.
+typedef LinkWidgetBuilder = Widget Function(
+  BuildContext context,
+  FollowLink followLink,
+);
+
+/// Signature for a delegate function to build the [Link] widget.
+typedef LinkDelegate = Widget Function(LinkInfo linkWidget);
+
+final MethodCodec _codec = const JSONMethodCodec();
+
+/// Defines where a Link URL should be open.
+///
+/// This is a class instead of an enum to allow future customizability e.g.
+/// opening a link in a specific iframe.
+class LinkTarget {
+  /// Const private constructor with a [debugLabel] to allow the creation of
+  /// multiple distinct const instances.
+  const LinkTarget._({this.debugLabel});
+
+  /// Used to distinguish multiple const instances of [LinkTarget].
+  final String debugLabel;
+
+  /// Use the default target for each platform.
+  ///
+  /// On Android, the default is [blank]. On the web, the default is [self].
+  ///
+  /// iOS, on the other hand, defaults to [self] for web URLs, and [blank] for
+  /// non-web URLs.
+  static const defaultTarget = LinkTarget._(debugLabel: 'defaultTarget');
+
+  /// On the web, this opens the link in the same tab where the flutter app is
+  /// running.
+  ///
+  /// On Android and iOS, this opens the link in a webview within the app.
+  static const self = LinkTarget._(debugLabel: 'self');
+
+  /// On the web, this opens the link in a new tab or window (depending on the
+  /// browser and user configuration).
+  ///
+  /// On Android and iOS, this opens the link in the browser or the relevant
+  /// app.
+  static const blank = LinkTarget._(debugLabel: 'blank');
+}
+
+/// Encapsulates all the information necessary to build a Link widget.
+abstract class LinkInfo {
+  /// Called at build time to construct the widget tree under the link.
+  LinkWidgetBuilder get builder;
+
+  /// The destination that this link leads to.
+  Uri get uri;
+
+  /// The target indicating where to open the link.
+  LinkTarget get target;
+
+  /// Whether the link is disabled or not.
+  bool get isDisabled;
+}
+
+/// Pushes the [routeName] into Flutter's navigation system via a platform
+/// message.
+Future<ByteData> pushRouteNameToFramework(
+  BuildContext context,
+  String routeName, {
+  @visibleForTesting bool debugForceRouter = false,
+}) {
+  final Completer<ByteData> completer = Completer<ByteData>();
+  if (debugForceRouter || _hasRouter(context)) {
+    SystemNavigator.routeInformationUpdated(location: routeName);
+    window.onPlatformMessage(
+      'flutter/navigation',
+      _codec.encodeMethodCall(
+        MethodCall('pushRouteInformation', <dynamic, dynamic>{
+          'location': routeName,
+          'state': null,
+        }),
+      ),
+      completer.complete,
+    );
+  } else {
+    window.onPlatformMessage(
+      'flutter/navigation',
+      _codec.encodeMethodCall(MethodCall('pushRoute', routeName)),
+      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/lib/method_channel_url_launcher.dart b/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart
index f87630e..ac5bfa2 100644
--- a/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart
+++ b/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart
@@ -7,6 +7,7 @@
 import 'package:flutter/services.dart';
 import 'package:meta/meta.dart' show required;
 
+import 'link.dart';
 import 'url_launcher_platform_interface.dart';
 
 const MethodChannel _channel = MethodChannel('plugins.flutter.io/url_launcher');
@@ -14,6 +15,9 @@
 /// An implementation of [UrlLauncherPlatform] that uses method channels.
 class MethodChannelUrlLauncher extends UrlLauncherPlatform {
   @override
+  final LinkDelegate linkDelegate = null;
+
+  @override
   Future<bool> canLaunch(String url) {
     return _channel.invokeMethod<bool>(
       'canLaunch',
diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart
index 1de5742..75002ff 100644
--- a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart
+++ b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart
@@ -6,6 +6,7 @@
 
 import 'package:meta/meta.dart' show required;
 import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+import 'package:url_launcher_platform_interface/link.dart';
 
 import 'method_channel_url_launcher.dart';
 
@@ -38,6 +39,9 @@
     _instance = instance;
   }
 
+  /// The delegate used by the Link widget to build itself.
+  LinkDelegate get linkDelegate;
+
   /// Returns `true` if this platform is able to launch [url].
   Future<bool> canLaunch(String url) {
     throw UnimplementedError('canLaunch() has not been implemented.');
diff --git a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml
index 0c40962..ce0fdd9 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: 1.0.8
+version: 1.0.9
 
 dependencies:
   flutter:
@@ -19,4 +19,4 @@
 
 environment:
   sdk: ">=2.1.0 <3.0.0"
-  flutter: ">=1.9.1+hotfix.4 <2.0.0"
+  flutter: ">=1.22.0 <2.0.0"
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
new file mode 100644
index 0000000..99a885c
--- /dev/null
+++ b/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart
@@ -0,0 +1,71 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:ui';
+
+import 'package:mockito/mockito.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.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);
+      callback(_codec.encodeSuccessEnvelope(true));
+    };
+  });
+
+  tearDown(() {
+    window.onPlatformMessage = oldHandler;
+  });
+
+  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 {}
diff --git a/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart b/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart
index 628ab48..d88f53a 100644
--- a/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart
+++ b/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart
@@ -7,6 +7,7 @@
 import 'package:flutter_test/flutter_test.dart';
 import 'package:plugin_platform_interface/plugin_platform_interface.dart';
 
+import 'package:url_launcher_platform_interface/link.dart';
 import 'package:url_launcher_platform_interface/method_channel_url_launcher.dart';
 import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
 
@@ -286,4 +287,7 @@
 class ImplementsUrlLauncherPlatform extends Mock
     implements UrlLauncherPlatform {}
 
-class ExtendsUrlLauncherPlatform extends UrlLauncherPlatform {}
+class ExtendsUrlLauncherPlatform extends UrlLauncherPlatform {
+  @override
+  final LinkDelegate linkDelegate = null;
+}