[video_player] Android: video_player_android parts of rotationCorrection fix (#5158)

diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md
index 28d0188..d5acda4 100644
--- a/packages/video_player/video_player_android/CHANGELOG.md
+++ b/packages/video_player/video_player_android/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.3.5
+
+* Sets rotationCorrection for videos recorded in landscapeRight (https://github.com/flutter/flutter/issues/60327).
+
 ## 2.3.4
 
 * Updates ExoPlayer to 2.17.1.
diff --git a/packages/video_player/video_player_android/android/build.gradle b/packages/video_player/video_player_android/android/build.gradle
index 32cc203..f9ad4e6 100644
--- a/packages/video_player/video_player_android/android/build.gradle
+++ b/packages/video_player/video_player_android/android/build.gradle
@@ -48,7 +48,9 @@
         implementation 'com.google.android.exoplayer:exoplayer-dash:2.17.1'
         implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.17.1'
         testImplementation 'junit:junit:4.12'
+        testImplementation 'androidx.test:core:1.3.0'
         testImplementation 'org.mockito:mockito-inline:3.9.0'
+        testImplementation 'org.robolectric:robolectric:4.5'
     }
 
 
@@ -63,4 +65,4 @@
             }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java
index dc7c881..f215354 100644
--- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java
+++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java
@@ -11,6 +11,7 @@
 import android.net.Uri;
 import android.view.Surface;
 import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
 import com.google.android.exoplayer2.C;
 import com.google.android.exoplayer2.ExoPlayer;
 import com.google.android.exoplayer2.Format;
@@ -51,11 +52,11 @@
 
   private final TextureRegistry.SurfaceTextureEntry textureEntry;
 
-  private QueuingEventSink eventSink = new QueuingEventSink();
+  private QueuingEventSink eventSink;
 
   private final EventChannel eventChannel;
 
-  private boolean isInitialized = false;
+  @VisibleForTesting boolean isInitialized = false;
 
   private final VideoPlayerOptions options;
 
@@ -71,10 +72,11 @@
     this.textureEntry = textureEntry;
     this.options = options;
 
-    exoPlayer = new ExoPlayer.Builder(context).build();
+    ExoPlayer exoPlayer = new ExoPlayer.Builder(context).build();
 
     Uri uri = Uri.parse(dataSource);
     DataSource.Factory dataSourceFactory;
+
     if (isHTTP(uri)) {
       DefaultHttpDataSource.Factory httpDataSourceFactory =
           new DefaultHttpDataSource.Factory()
@@ -90,10 +92,26 @@
     }
 
     MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context);
+
     exoPlayer.setMediaSource(mediaSource);
     exoPlayer.prepare();
 
-    setupVideoPlayer(eventChannel, textureEntry);
+    setUpVideoPlayer(exoPlayer, new QueuingEventSink());
+  }
+
+  // Constructor used to directly test members of this class.
+  @VisibleForTesting
+  VideoPlayer(
+      ExoPlayer exoPlayer,
+      EventChannel eventChannel,
+      TextureRegistry.SurfaceTextureEntry textureEntry,
+      VideoPlayerOptions options,
+      QueuingEventSink eventSink) {
+    this.eventChannel = eventChannel;
+    this.textureEntry = textureEntry;
+    this.options = options;
+
+    setUpVideoPlayer(exoPlayer, eventSink);
   }
 
   private static boolean isHTTP(Uri uri) {
@@ -106,7 +124,6 @@
 
   private MediaSource buildMediaSource(
       Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) {
-
     int type;
     if (formatHint == null) {
       type = Util.inferContentType(uri.getLastPathSegment());
@@ -153,8 +170,10 @@
     }
   }
 
-  private void setupVideoPlayer(
-      EventChannel eventChannel, TextureRegistry.SurfaceTextureEntry textureEntry) {
+  private void setUpVideoPlayer(ExoPlayer exoPlayer, QueuingEventSink eventSink) {
+    this.exoPlayer = exoPlayer;
+    this.eventSink = eventSink;
+
     eventChannel.setStreamHandler(
         new EventChannel.StreamHandler() {
           @Override
@@ -264,7 +283,8 @@
   }
 
   @SuppressWarnings("SuspiciousNameCombination")
-  private void sendInitialized() {
+  @VisibleForTesting
+  void sendInitialized() {
     if (isInitialized) {
       Map<String, Object> event = new HashMap<>();
       event.put("event", "initialized");
@@ -282,7 +302,16 @@
         }
         event.put("width", width);
         event.put("height", height);
+
+        // Rotating the video with ExoPlayer does not seem to be possible with a Surface,
+        // so inform the Flutter code that the widget needs to be rotated to prevent
+        // upside-down playback for videos with rotationDegrees of 180 (other orientations work
+        // correctly without correction).
+        if (rotationDegrees == 180) {
+          event.put("rotationCorrection", rotationDegrees);
+        }
       }
+
       eventSink.success(event);
     }
   }
diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java
new file mode 100644
index 0000000..2ed1165
--- /dev/null
+++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java
@@ -0,0 +1,15 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.videoplayer;
+
+import org.junit.Test;
+
+public class VideoPlayerPluginTest {
+  // This is only a placeholder test and doesn't actually initialize the plugin.
+  @Test
+  public void initPluginDoesNotThrow() {
+    final VideoPlayerPlugin plugin = new VideoPlayerPlugin();
+  }
+}
diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java
index ec960b7..194f790 100644
--- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java
+++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java
@@ -4,12 +4,154 @@
 
 package io.flutter.plugins.videoplayer;
 
-import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Format;
+import io.flutter.plugin.common.EventChannel;
+import io.flutter.view.TextureRegistry;
+import java.util.HashMap;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
 public class VideoPlayerTest {
-  // This is only a placeholder test and doesn't actually initialize the plugin.
+  private ExoPlayer fakeExoPlayer;
+  private EventChannel fakeEventChannel;
+  private TextureRegistry.SurfaceTextureEntry fakeSurfaceTextureEntry;
+  private VideoPlayerOptions fakeVideoPlayerOptions;
+  private QueuingEventSink fakeEventSink;
+
+  @Captor private ArgumentCaptor<HashMap<String, Object>> eventCaptor;
+
+  @Before
+  public void before() {
+    MockitoAnnotations.openMocks(this);
+
+    fakeExoPlayer = mock(ExoPlayer.class);
+    fakeEventChannel = mock(EventChannel.class);
+    fakeSurfaceTextureEntry = mock(TextureRegistry.SurfaceTextureEntry.class);
+    fakeVideoPlayerOptions = mock(VideoPlayerOptions.class);
+    fakeEventSink = mock(QueuingEventSink.class);
+  }
+
   @Test
-  public void initPluginDoesNotThrow() {
-    final VideoPlayerPlugin plugin = new VideoPlayerPlugin();
+  public void sendInitializedSendsExpectedEvent_90RotationDegrees() {
+    VideoPlayer videoPlayer =
+        new VideoPlayer(
+            fakeExoPlayer,
+            fakeEventChannel,
+            fakeSurfaceTextureEntry,
+            fakeVideoPlayerOptions,
+            fakeEventSink);
+    Format testFormat =
+        new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(90).build();
+
+    when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
+    when(fakeExoPlayer.getDuration()).thenReturn(10L);
+
+    videoPlayer.isInitialized = true;
+    videoPlayer.sendInitialized();
+
+    verify(fakeEventSink).success(eventCaptor.capture());
+    HashMap<String, Object> event = eventCaptor.getValue();
+
+    assertEquals(event.get("event"), "initialized");
+    assertEquals(event.get("duration"), 10L);
+    assertEquals(event.get("width"), 200);
+    assertEquals(event.get("height"), 100);
+    assertEquals(event.get("rotationCorrection"), null);
+  }
+
+  @Test
+  public void sendInitializedSendsExpectedEvent_270RotationDegrees() {
+    VideoPlayer videoPlayer =
+        new VideoPlayer(
+            fakeExoPlayer,
+            fakeEventChannel,
+            fakeSurfaceTextureEntry,
+            fakeVideoPlayerOptions,
+            fakeEventSink);
+    Format testFormat =
+        new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(270).build();
+
+    when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
+    when(fakeExoPlayer.getDuration()).thenReturn(10L);
+
+    videoPlayer.isInitialized = true;
+    videoPlayer.sendInitialized();
+
+    verify(fakeEventSink).success(eventCaptor.capture());
+    HashMap<String, Object> event = eventCaptor.getValue();
+
+    assertEquals(event.get("event"), "initialized");
+    assertEquals(event.get("duration"), 10L);
+    assertEquals(event.get("width"), 200);
+    assertEquals(event.get("height"), 100);
+    assertEquals(event.get("rotationCorrection"), null);
+  }
+
+  @Test
+  public void sendInitializedSendsExpectedEvent_0RotationDegrees() {
+    VideoPlayer videoPlayer =
+        new VideoPlayer(
+            fakeExoPlayer,
+            fakeEventChannel,
+            fakeSurfaceTextureEntry,
+            fakeVideoPlayerOptions,
+            fakeEventSink);
+    Format testFormat =
+        new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(0).build();
+
+    when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
+    when(fakeExoPlayer.getDuration()).thenReturn(10L);
+
+    videoPlayer.isInitialized = true;
+    videoPlayer.sendInitialized();
+
+    verify(fakeEventSink).success(eventCaptor.capture());
+    HashMap<String, Object> event = eventCaptor.getValue();
+
+    assertEquals(event.get("event"), "initialized");
+    assertEquals(event.get("duration"), 10L);
+    assertEquals(event.get("width"), 100);
+    assertEquals(event.get("height"), 200);
+    assertEquals(event.get("rotationCorrection"), null);
+  }
+
+  @Test
+  public void sendInitializedSendsExpectedEvent_180RotationDegrees() {
+    VideoPlayer videoPlayer =
+        new VideoPlayer(
+            fakeExoPlayer,
+            fakeEventChannel,
+            fakeSurfaceTextureEntry,
+            fakeVideoPlayerOptions,
+            fakeEventSink);
+    Format testFormat =
+        new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(180).build();
+
+    when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
+    when(fakeExoPlayer.getDuration()).thenReturn(10L);
+
+    videoPlayer.isInitialized = true;
+    videoPlayer.sendInitialized();
+
+    verify(fakeEventSink).success(eventCaptor.capture());
+    HashMap<String, Object> event = eventCaptor.getValue();
+
+    assertEquals(event.get("event"), "initialized");
+    assertEquals(event.get("duration"), 10L);
+    assertEquals(event.get("width"), 100);
+    assertEquals(event.get("height"), 200);
+    assertEquals(event.get("rotationCorrection"), 180);
   }
 }
diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart
index 5c5fd80..cee6d7d 100644
--- a/packages/video_player/video_player_android/lib/src/android_video_player.dart
+++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart
@@ -130,6 +130,7 @@
             duration: Duration(milliseconds: map['duration'] as int),
             size: Size((map['width'] as num?)?.toDouble() ?? 0.0,
                 (map['height'] as num?)?.toDouble() ?? 0.0),
+            rotationCorrection: map['rotationCorrection'] as int? ?? 0,
           );
         case 'completed':
           return VideoEvent(
diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml
index 7e23b32..367b95e 100644
--- a/packages/video_player/video_player_android/pubspec.yaml
+++ b/packages/video_player/video_player_android/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Android implementation of the video_player plugin.
 repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_android
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
-version: 2.3.4
+version: 2.3.5
 
 environment:
   sdk: ">=2.14.0 <3.0.0"
@@ -20,7 +20,7 @@
 dependencies:
   flutter:
     sdk: flutter
-  video_player_platform_interface: ">=4.2.0 <6.0.0"
+  video_player_platform_interface: ^5.1.1
 
 dev_dependencies:
   flutter_test:
diff --git a/packages/video_player/video_player_android/test/android_video_player_test.dart b/packages/video_player/video_player_android/test/android_video_player_test.dart
index b7cf763..aad31e4 100644
--- a/packages/video_player/video_player_android/test/android_video_player_test.dart
+++ b/packages/video_player/video_player_android/test/android_video_player_test.dart
@@ -259,6 +259,20 @@
                     'flutter.io/videoPlayer/videoEvents123',
                     const StandardMethodCodec()
                         .encodeSuccessEnvelope(<String, dynamic>{
+                      'event': 'initialized',
+                      'duration': 98765,
+                      'width': 1920,
+                      'height': 1080,
+                      'rotationCorrection': 180,
+                    }),
+                    (ByteData? data) {});
+
+            await _ambiguate(ServicesBinding.instance)
+                ?.defaultBinaryMessenger
+                .handlePlatformMessage(
+                    'flutter.io/videoPlayer/videoEvents123',
+                    const StandardMethodCodec()
+                        .encodeSuccessEnvelope(<String, dynamic>{
                       'event': 'completed',
                     }),
                     (ByteData? data) {});
@@ -312,6 +326,13 @@
               eventType: VideoEventType.initialized,
               duration: const Duration(milliseconds: 98765),
               size: const Size(1920, 1080),
+              rotationCorrection: 0,
+            ),
+            VideoEvent(
+              eventType: VideoEventType.initialized,
+              duration: const Duration(milliseconds: 98765),
+              size: const Size(1920, 1080),
+              rotationCorrection: 180,
             ),
             VideoEvent(eventType: VideoEventType.completed),
             VideoEvent(