[webview_flutter] Added 'allowsInlineMediaPlayback' property (#3334)

diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md
index 3526139..a10c28d 100644
--- a/packages/webview_flutter/CHANGELOG.md
+++ b/packages/webview_flutter/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.0.0-nullsafety.1
+
+* Added `allowsInlineMediaPlayback` property.
+
 ## 2.0.0-nullsafety
 
 * Migration to null-safety.
diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java
index bfb79a3..ef9f006 100644
--- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java
+++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java
@@ -372,6 +372,9 @@
         case "userAgent":
           updateUserAgent((String) settings.get(key));
           break;
+        case "allowsInlineMediaPlayback":
+          // no-op inline media playback is always allowed on Android.
+          break;
         default:
           throw new IllegalArgumentException("Unknown WebView setting: " + key);
       }
diff --git a/packages/webview_flutter/example/assets/sample_video.mp4 b/packages/webview_flutter/example/assets/sample_video.mp4
new file mode 100644
index 0000000..a203d0c
--- /dev/null
+++ b/packages/webview_flutter/example/assets/sample_video.mp4
Binary files differ
diff --git a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart
index 2a17c53..86cf8ab 100644
--- a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart
+++ b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart
@@ -120,16 +120,14 @@
             controllerCompleter.complete(controller);
           },
           javascriptMode: JavascriptMode.unrestricted,
-          // TODO(iskakaushik): Remove this when collection literals makes it to stable.
-          // ignore: prefer_collection_literals
-          javascriptChannels: <JavascriptChannel>[
+          javascriptChannels: <JavascriptChannel>{
             JavascriptChannel(
               name: 'Echo',
               onMessageReceived: (JavascriptMessage message) {
                 messagesReceived.add(message.message);
               },
             ),
-          ].toSet(),
+          },
           onPageStarted: (String url) {
             pageStarted.complete(null);
           },
@@ -180,16 +178,14 @@
       onWebViewCreated: (WebViewController controller) {
         controllerCompleter.complete(controller);
       },
-      // TODO(iskakaushik): Remove this when collection literals makes it to stable.
-      // ignore: prefer_collection_literals
-      javascriptChannels: <JavascriptChannel>[
+      javascriptChannels: <JavascriptChannel>{
         JavascriptChannel(
           name: 'Resize',
           onMessageReceived: (JavascriptMessage message) {
             resizeCompleter.complete(true);
           },
         ),
-      ].toSet(),
+      },
       onPageStarted: (String url) {
         pageStarted.complete(null);
       },
@@ -327,7 +323,218 @@
     expect(customUserAgent2, defaultPlatformUserAgent);
   });
 
-  group('Media playback policy', () {
+  group('Video playback policy', () {
+    String videoTestBase64;
+    setUpAll(() async {
+      final ByteData videoData =
+          await rootBundle.load('assets/sample_video.mp4');
+      final String base64VideoData =
+          base64Encode(Uint8List.view(videoData.buffer));
+      final String videoTest = '''
+        <!DOCTYPE html><html>
+        <head><title>Video auto play</title>
+          <script type="text/javascript">
+            function play() {
+              var video = document.getElementById("video");
+              video.play();
+            }
+            function isPaused() {
+              var video = document.getElementById("video");
+              return video.paused;
+            }
+            function isFullScreen() {
+              var video = document.getElementById("video");
+              return video.webkitDisplayingFullscreen;
+            }
+          </script>
+        </head>
+        <body onload="play();">
+        <video controls playsinline autoplay id="video">
+          <source src="data:video/mp4;charset=utf-8;base64,$base64VideoData">
+        </video>
+        </body>
+        </html>
+      ''';
+      videoTestBase64 = base64Encode(const Utf8Encoder().convert(videoTest));
+    });
+
+    test('Auto media playback', () async {
+      Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      Completer<void> pageLoaded = Completer<void>();
+
+      await pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
+          ),
+        ),
+      );
+      WebViewController controller = await controllerCompleter.future;
+      await pageLoaded.future;
+
+      String isPaused = await controller.evaluateJavascript('isPaused();');
+      expect(isPaused, _webviewBool(false));
+
+      controllerCompleter = Completer<WebViewController>();
+      pageLoaded = Completer<void>();
+
+      // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy
+      await pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy:
+                AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
+          ),
+        ),
+      );
+
+      controller = await controllerCompleter.future;
+      await pageLoaded.future;
+
+      isPaused = await controller.evaluateJavascript('isPaused();');
+      expect(isPaused, _webviewBool(true));
+    });
+
+    test('Changes to initialMediaPlaybackPolicy are ignored', () async {
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      Completer<void> pageLoaded = Completer<void>();
+
+      final GlobalKey key = GlobalKey();
+      await pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: key,
+            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
+          ),
+        ),
+      );
+      final WebViewController controller = await controllerCompleter.future;
+      await pageLoaded.future;
+
+      String isPaused = await controller.evaluateJavascript('isPaused();');
+      expect(isPaused, _webviewBool(false));
+
+      pageLoaded = Completer<void>();
+
+      await pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: key,
+            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy:
+                AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
+          ),
+        ),
+      );
+
+      await controller.reload();
+
+      await pageLoaded.future;
+
+      isPaused = await controller.evaluateJavascript('isPaused();');
+      expect(isPaused, _webviewBool(false));
+    });
+
+    test('Video plays inline when allowsInlineMediaPlayback is true', () async {
+      Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      Completer<void> pageLoaded = Completer<void>();
+
+      await pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
+            allowsInlineMediaPlayback: true,
+          ),
+        ),
+      );
+      WebViewController controller = await controllerCompleter.future;
+      await pageLoaded.future;
+
+      String isFullScreen =
+          await controller.evaluateJavascript('isFullScreen();');
+      expect(isFullScreen, _webviewBool(false));
+
+      controllerCompleter = Completer<WebViewController>();
+      pageLoaded = Completer<void>();
+
+      await pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
+            allowsInlineMediaPlayback: false,
+          ),
+        ),
+      );
+
+      controller = await controllerCompleter.future;
+      await pageLoaded.future;
+
+      isFullScreen = await controller.evaluateJavascript('isFullScreen();');
+      expect(isFullScreen, _webviewBool(true));
+    });
+  });
+
+  group('Audio playback policy', () {
     String audioTestBase64;
     setUpAll(() async {
       final ByteData audioData =
diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart
index 7ec3008..2a4b652 100644
--- a/packages/webview_flutter/example/lib/main.dart
+++ b/packages/webview_flutter/example/lib/main.dart
@@ -62,11 +62,9 @@
           onWebViewCreated: (WebViewController webViewController) {
             _controller.complete(webViewController);
           },
-          // TODO(iskakaushik): Remove this when collection literals makes it to stable.
-          // ignore: prefer_collection_literals
-          javascriptChannels: <JavascriptChannel>[
+          javascriptChannels: <JavascriptChannel>{
             _toasterJavascriptChannel(context),
-          ].toSet(),
+          },
           navigationDelegate: (NavigationRequest request) {
             if (request.url.startsWith('https://www.youtube.com/')) {
               print('blocking navigation to $request}');
diff --git a/packages/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/example/pubspec.yaml
index 44c740a..543e7fd 100644
--- a/packages/webview_flutter/example/pubspec.yaml
+++ b/packages/webview_flutter/example/pubspec.yaml
@@ -23,3 +23,4 @@
   uses-material-design: true
   assets:
     - assets/sample_audio.ogg
+    - assets/sample_video.mp4
diff --git a/packages/webview_flutter/ios/Classes/FlutterWebView.m b/packages/webview_flutter/ios/Classes/FlutterWebView.m
index 969e010..ed3cf44 100644
--- a/packages/webview_flutter/ios/Classes/FlutterWebView.m
+++ b/packages/webview_flutter/ios/Classes/FlutterWebView.m
@@ -332,6 +332,9 @@
     } else if ([key isEqualToString:@"userAgent"]) {
       NSString* userAgent = settings[key];
       [self updateUserAgent:[userAgent isEqual:[NSNull null]] ? nil : userAgent];
+    } else if ([key isEqualToString:@"allowsInlineMediaPlayback"]) {
+      NSNumber* allowsInlineMediaPlayback = settings[key];
+      _webView.configuration.allowsInlineMediaPlayback = [allowsInlineMediaPlayback boolValue];
     } else {
       [unknownKeys addObject:key];
     }
diff --git a/packages/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/lib/platform_interface.dart
index f162e58..a840c00 100644
--- a/packages/webview_flutter/lib/platform_interface.dart
+++ b/packages/webview_flutter/lib/platform_interface.dart
@@ -390,6 +390,7 @@
     this.hasNavigationDelegate,
     this.debuggingEnabled,
     this.gestureNavigationEnabled,
+    this.allowsInlineMediaPlayback,
     required this.userAgent,
   }) : assert(userAgent != null);
 
@@ -404,6 +405,11 @@
   /// See also: [WebView.debuggingEnabled].
   final bool? debuggingEnabled;
 
+  /// Whether to play HTML5 videos inline or use the native full-screen controller on iOS.
+  ///
+  /// This will have no effect on Android.
+  final bool? allowsInlineMediaPlayback;
+
   /// The value used for the HTTP `User-Agent:` request header.
   ///
   /// If [userAgent.value] is null the platform's default user agent should be used.
@@ -421,7 +427,7 @@
 
   @override
   String toString() {
-    return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent)';
+    return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent, allowsInlineMediaPlayback: $allowsInlineMediaPlayback)';
   }
 }
 
diff --git a/packages/webview_flutter/lib/src/webview_method_channel.dart b/packages/webview_flutter/lib/src/webview_method_channel.dart
index 1c666d7..b38d65a 100644
--- a/packages/webview_flutter/lib/src/webview_method_channel.dart
+++ b/packages/webview_flutter/lib/src/webview_method_channel.dart
@@ -185,6 +185,8 @@
     _addIfNonNull('debuggingEnabled', settings.debuggingEnabled);
     _addIfNonNull(
         'gestureNavigationEnabled', settings.gestureNavigationEnabled);
+    _addIfNonNull(
+        'allowsInlineMediaPlayback', settings.allowsInlineMediaPlayback);
     _addSettingIfPresent('userAgent', settings.userAgent);
     return map;
   }
diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart
index 4327c78..6853d39 100644
--- a/packages/webview_flutter/lib/webview_flutter.dart
+++ b/packages/webview_flutter/lib/webview_flutter.dart
@@ -223,8 +223,10 @@
     this.userAgent,
     this.initialMediaPlaybackPolicy =
         AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
+    this.allowsInlineMediaPlayback = false,
   })  : assert(javascriptMode != null),
         assert(initialMediaPlaybackPolicy != null),
+        assert(allowsInlineMediaPlayback != null),
         super(key: key);
 
   static WebViewPlatform? _platform;
@@ -333,6 +335,13 @@
   ///     * When a navigationDelegate is set HTTP requests do not include the HTTP referer header.
   final NavigationDelegate? navigationDelegate;
 
+  /// Controls whether inline playback of HTML5 videos is allowed on iOS.
+  ///
+  /// This field is ignored on Android because Android allows it by default.
+  ///
+  /// By default `allowsInlineMediaPlayback` is false.
+  final bool allowsInlineMediaPlayback;
+
   /// Invoked when a page starts loading.
   final PageStartedCallback? onPageStarted;
 
@@ -469,6 +478,7 @@
     hasNavigationDelegate: widget.navigationDelegate != null,
     debuggingEnabled: widget.debuggingEnabled,
     gestureNavigationEnabled: widget.gestureNavigationEnabled,
+    allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback,
     userAgent: WebSetting<String?>.of(widget.userAgent),
   );
 }
diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml
index de62a50..75910df 100644
--- a/packages/webview_flutter/pubspec.yaml
+++ b/packages/webview_flutter/pubspec.yaml
@@ -1,6 +1,6 @@
 name: webview_flutter
 description: A Flutter plugin that provides a WebView widget on Android and iOS.
-version: 2.0.0-nullsafety
+version: 2.0.0-nullsafety.1
 homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter
 
 environment:
diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart
index 662a2f7..162b193 100644
--- a/packages/webview_flutter/test/webview_flutter_test.dart
+++ b/packages/webview_flutter/test/webview_flutter_test.dart
@@ -423,14 +423,12 @@
     await tester.pumpWidget(
       WebView(
         initialUrl: 'https://youtube.com',
-        // TODO(iskakaushik): Remove this when collection literals makes it to stable.
-        // ignore: prefer_collection_literals
-        javascriptChannels: <JavascriptChannel>[
+        javascriptChannels: <JavascriptChannel>{
           JavascriptChannel(
               name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
           JavascriptChannel(
               name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
-        ].toSet(),
+        },
       ),
     );
 
@@ -463,14 +461,12 @@
     await tester.pumpWidget(
       WebView(
         initialUrl: 'https://youtube.com',
-        // TODO(iskakaushik): Remove this when collection literals makes it to stable.
-        // ignore: prefer_collection_literals
-        javascriptChannels: <JavascriptChannel>[
+        javascriptChannels: <JavascriptChannel>{
           JavascriptChannel(
               name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
           JavascriptChannel(
               name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
-        ].toSet(),
+        },
       ),
     );
     expect(tester.takeException(), isNot(null));
@@ -480,30 +476,26 @@
     await tester.pumpWidget(
       WebView(
         initialUrl: 'https://youtube.com',
-        // TODO(iskakaushik): Remove this when collection literals makes it to stable.
-        // ignore: prefer_collection_literals
-        javascriptChannels: <JavascriptChannel>[
+        javascriptChannels: <JavascriptChannel>{
           JavascriptChannel(
               name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
           JavascriptChannel(
               name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
-        ].toSet(),
+        },
       ),
     );
 
     await tester.pumpWidget(
       WebView(
         initialUrl: 'https://youtube.com',
-        // TODO(iskakaushik): Remove this when collection literals makes it to stable.
-        // ignore: prefer_collection_literals
-        javascriptChannels: <JavascriptChannel>[
+        javascriptChannels: <JavascriptChannel>{
           JavascriptChannel(
               name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
           JavascriptChannel(
               name: 'Alarm2', onMessageReceived: (JavascriptMessage msg) {}),
           JavascriptChannel(
               name: 'Alarm3', onMessageReceived: (JavascriptMessage msg) {}),
-        ].toSet(),
+        },
       ),
     );
 
@@ -523,12 +515,10 @@
     await tester.pumpWidget(
       WebView(
         initialUrl: 'https://youtube.com',
-        // TODO(iskakaushik): Remove this when collection literals makes it to stable.
-        // ignore: prefer_collection_literals
-        javascriptChannels: <JavascriptChannel>[
+        javascriptChannels: <JavascriptChannel>{
           JavascriptChannel(
               name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
-        ].toSet(),
+        },
       ),
     );
 
@@ -541,12 +531,10 @@
     await tester.pumpWidget(
       WebView(
         initialUrl: 'https://youtube.com',
-        // TODO(iskakaushik): Remove this when collection literals makes it to stable.
-        // ignore: prefer_collection_literals
-        javascriptChannels: <JavascriptChannel>[
+        javascriptChannels: <JavascriptChannel>{
           JavascriptChannel(
               name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
-        ].toSet(),
+        },
       ),
     );
 
@@ -563,9 +551,7 @@
     await tester.pumpWidget(
       WebView(
         initialUrl: 'https://youtube.com',
-        // TODO(iskakaushik): Remove this when collection literals makes it to stable.
-        // ignore: prefer_collection_literals
-        javascriptChannels: <JavascriptChannel>[
+        javascriptChannels: <JavascriptChannel>{
           JavascriptChannel(
               name: 'Tts',
               onMessageReceived: (JavascriptMessage msg) {
@@ -576,7 +562,7 @@
               onMessageReceived: (JavascriptMessage msg) {
                 alarmMessagesReceived.add(msg.message);
               }),
-        ].toSet(),
+        },
       ),
     );