[video_player] Add v2 embedding support (#2226)

- Adds support in the plugin
- Adds a v2 embedding to the example app
- Fixes a broken remote example in the example app
- Increments the Flutter SDK dependency
- Increments the version
- Adds e2e tests for some simple use cases of the plugin
diff --git a/packages/video_player/CHANGELOG.md b/packages/video_player/CHANGELOG.md
index d488249..759c955 100644
--- a/packages/video_player/CHANGELOG.md
+++ b/packages/video_player/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.10.3
+
+* Add support for the v2 Android embedding. This shouldn't impact existing
+  functionality.
+
 ## 0.10.2+6
 
 * Remove AndroidX warnings.
diff --git a/packages/video_player/android/build.gradle b/packages/video_player/android/build.gradle
index edbb4c7..4a73ec5 100644
--- a/packages/video_player/android/build.gradle
+++ b/packages/video_player/android/build.gradle
@@ -45,3 +45,29 @@
         implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.9.6'
     }
 }
+
+// TODO(mklim): Remove this hack once androidx.lifecycle is included on stable. https://github.com/flutter/flutter/issues/42348
+afterEvaluate {
+    def containsEmbeddingDependencies = false
+    for (def configuration : configurations.all) {
+        for (def dependency : configuration.dependencies) {
+            if (dependency.group == 'io.flutter' &&
+                dependency.name.startsWith('flutter_embedding') &&
+                dependency.isTransitive())
+            {
+                containsEmbeddingDependencies = true
+                break
+            }
+        }
+    }
+    if (!containsEmbeddingDependencies) {
+        android {
+            dependencies {
+                def lifecycle_version = "1.1.1"
+                compileOnly "android.arch.lifecycle:runtime:$lifecycle_version"
+                compileOnly "android.arch.lifecycle:common:$lifecycle_version"
+                compileOnly "android.arch.lifecycle:common-java8:$lifecycle_version"
+            }
+        }
+    }
+}
diff --git a/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java
new file mode 100644
index 0000000..43123ef
--- /dev/null
+++ b/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java
@@ -0,0 +1,283 @@
+package io.flutter.plugins.videoplayer;
+
+import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
+import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.view.Surface;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayerFactory;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Player.EventListener;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.audio.AudioAttributes;
+import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
+import com.google.android.exoplayer2.source.ExtractorMediaSource;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.dash.DashMediaSource;
+import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
+import com.google.android.exoplayer2.source.hls.HlsMediaSource;
+import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
+import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
+import com.google.android.exoplayer2.util.Util;
+import io.flutter.plugin.common.EventChannel;
+import io.flutter.plugin.common.MethodChannel.Result;
+import io.flutter.view.TextureRegistry;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+final class VideoPlayer {
+  private static final String FORMAT_SS = "ss";
+  private static final String FORMAT_DASH = "dash";
+  private static final String FORMAT_HLS = "hls";
+  private static final String FORMAT_OTHER = "other";
+
+  private SimpleExoPlayer exoPlayer;
+
+  private Surface surface;
+
+  private final TextureRegistry.SurfaceTextureEntry textureEntry;
+
+  private QueuingEventSink eventSink = new QueuingEventSink();
+
+  private final EventChannel eventChannel;
+
+  private boolean isInitialized = false;
+
+  VideoPlayer(
+      Context context,
+      EventChannel eventChannel,
+      TextureRegistry.SurfaceTextureEntry textureEntry,
+      String dataSource,
+      Result result,
+      String formatHint) {
+    this.eventChannel = eventChannel;
+    this.textureEntry = textureEntry;
+
+    TrackSelector trackSelector = new DefaultTrackSelector();
+    exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
+
+    Uri uri = Uri.parse(dataSource);
+
+    DataSource.Factory dataSourceFactory;
+    if (isHTTP(uri)) {
+      dataSourceFactory =
+          new DefaultHttpDataSourceFactory(
+              "ExoPlayer",
+              null,
+              DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
+              DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
+              true);
+    } else {
+      dataSourceFactory = new DefaultDataSourceFactory(context, "ExoPlayer");
+    }
+
+    MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context);
+    exoPlayer.prepare(mediaSource);
+
+    setupVideoPlayer(eventChannel, textureEntry, result);
+  }
+
+  private static boolean isHTTP(Uri uri) {
+    if (uri == null || uri.getScheme() == null) {
+      return false;
+    }
+    String scheme = uri.getScheme();
+    return scheme.equals("http") || scheme.equals("https");
+  }
+
+  private MediaSource buildMediaSource(
+      Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) {
+    int type;
+    if (formatHint == null) {
+      type = Util.inferContentType(uri.getLastPathSegment());
+    } else {
+      switch (formatHint) {
+        case FORMAT_SS:
+          type = C.TYPE_SS;
+          break;
+        case FORMAT_DASH:
+          type = C.TYPE_DASH;
+          break;
+        case FORMAT_HLS:
+          type = C.TYPE_HLS;
+          break;
+        case FORMAT_OTHER:
+          type = C.TYPE_OTHER;
+          break;
+        default:
+          type = -1;
+          break;
+      }
+    }
+    switch (type) {
+      case C.TYPE_SS:
+        return new SsMediaSource.Factory(
+                new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
+                new DefaultDataSourceFactory(context, null, mediaDataSourceFactory))
+            .createMediaSource(uri);
+      case C.TYPE_DASH:
+        return new DashMediaSource.Factory(
+                new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
+                new DefaultDataSourceFactory(context, null, mediaDataSourceFactory))
+            .createMediaSource(uri);
+      case C.TYPE_HLS:
+        return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
+      case C.TYPE_OTHER:
+        return new ExtractorMediaSource.Factory(mediaDataSourceFactory)
+            .setExtractorsFactory(new DefaultExtractorsFactory())
+            .createMediaSource(uri);
+      default:
+        {
+          throw new IllegalStateException("Unsupported type: " + type);
+        }
+    }
+  }
+
+  private void setupVideoPlayer(
+      EventChannel eventChannel, TextureRegistry.SurfaceTextureEntry textureEntry, Result result) {
+
+    eventChannel.setStreamHandler(
+        new EventChannel.StreamHandler() {
+          @Override
+          public void onListen(Object o, EventChannel.EventSink sink) {
+            eventSink.setDelegate(sink);
+          }
+
+          @Override
+          public void onCancel(Object o) {
+            eventSink.setDelegate(null);
+          }
+        });
+
+    surface = new Surface(textureEntry.surfaceTexture());
+    exoPlayer.setVideoSurface(surface);
+    setAudioAttributes(exoPlayer);
+
+    exoPlayer.addListener(
+        new EventListener() {
+
+          @Override
+          public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
+            if (playbackState == Player.STATE_BUFFERING) {
+              sendBufferingUpdate();
+            } else if (playbackState == Player.STATE_READY) {
+              if (!isInitialized) {
+                isInitialized = true;
+                sendInitialized();
+              }
+            } else if (playbackState == Player.STATE_ENDED) {
+              Map<String, Object> event = new HashMap<>();
+              event.put("event", "completed");
+              eventSink.success(event);
+            }
+          }
+
+          @Override
+          public void onPlayerError(final ExoPlaybackException error) {
+            if (eventSink != null) {
+              eventSink.error("VideoError", "Video player had error " + error, null);
+            }
+          }
+        });
+
+    Map<String, Object> reply = new HashMap<>();
+    reply.put("textureId", textureEntry.id());
+    result.success(reply);
+  }
+
+  void sendBufferingUpdate() {
+    Map<String, Object> event = new HashMap<>();
+    event.put("event", "bufferingUpdate");
+    List<? extends Number> range = Arrays.asList(0, exoPlayer.getBufferedPosition());
+    // iOS supports a list of buffered ranges, so here is a list with a single range.
+    event.put("values", Collections.singletonList(range));
+    eventSink.success(event);
+  }
+
+  @SuppressWarnings("deprecation")
+  private static void setAudioAttributes(SimpleExoPlayer exoPlayer) {
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+      exoPlayer.setAudioAttributes(
+          new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build());
+    } else {
+      exoPlayer.setAudioStreamType(C.STREAM_TYPE_MUSIC);
+    }
+  }
+
+  void play() {
+    exoPlayer.setPlayWhenReady(true);
+  }
+
+  void pause() {
+    exoPlayer.setPlayWhenReady(false);
+  }
+
+  void setLooping(boolean value) {
+    exoPlayer.setRepeatMode(value ? REPEAT_MODE_ALL : REPEAT_MODE_OFF);
+  }
+
+  void setVolume(double value) {
+    float bracketedValue = (float) Math.max(0.0, Math.min(1.0, value));
+    exoPlayer.setVolume(bracketedValue);
+  }
+
+  void seekTo(int location) {
+    exoPlayer.seekTo(location);
+  }
+
+  long getPosition() {
+    return exoPlayer.getCurrentPosition();
+  }
+
+  @SuppressWarnings("SuspiciousNameCombination")
+  private void sendInitialized() {
+    if (isInitialized) {
+      Map<String, Object> event = new HashMap<>();
+      event.put("event", "initialized");
+      event.put("duration", exoPlayer.getDuration());
+
+      if (exoPlayer.getVideoFormat() != null) {
+        Format videoFormat = exoPlayer.getVideoFormat();
+        int width = videoFormat.width;
+        int height = videoFormat.height;
+        int rotationDegrees = videoFormat.rotationDegrees;
+        // Switch the width/height if video was taken in portrait mode
+        if (rotationDegrees == 90 || rotationDegrees == 270) {
+          width = exoPlayer.getVideoFormat().height;
+          height = exoPlayer.getVideoFormat().width;
+        }
+        event.put("width", width);
+        event.put("height", height);
+      }
+      eventSink.success(event);
+    }
+  }
+
+  void dispose() {
+    if (isInitialized) {
+      exoPlayer.stop();
+    }
+    textureEntry.release();
+    eventChannel.setStreamHandler(null);
+    if (surface != null) {
+      surface.release();
+    }
+    if (exoPlayer != null) {
+      exoPlayer.release();
+    }
+  }
+}
diff --git a/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java
index 5b1f55f..fdc0511 100644
--- a/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java
+++ b/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java
@@ -4,322 +4,70 @@
 
 package io.flutter.plugins.videoplayer;
 
-import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
-import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
-
 import android.content.Context;
-import android.net.Uri;
-import android.os.Build;
+import android.util.Log;
 import android.util.LongSparseArray;
-import android.view.Surface;
-import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.ExoPlaybackException;
-import com.google.android.exoplayer2.ExoPlayerFactory;
-import com.google.android.exoplayer2.Format;
-import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.Player.EventListener;
-import com.google.android.exoplayer2.SimpleExoPlayer;
-import com.google.android.exoplayer2.audio.AudioAttributes;
-import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
-import com.google.android.exoplayer2.source.ExtractorMediaSource;
-import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.source.dash.DashMediaSource;
-import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
-import com.google.android.exoplayer2.source.hls.HlsMediaSource;
-import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
-import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
-import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
-import com.google.android.exoplayer2.trackselection.TrackSelector;
-import com.google.android.exoplayer2.upstream.DataSource;
-import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
-import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
-import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
-import com.google.android.exoplayer2.util.Util;
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.plugin.common.BinaryMessenger;
 import io.flutter.plugin.common.EventChannel;
 import io.flutter.plugin.common.MethodCall;
 import io.flutter.plugin.common.MethodChannel;
 import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
 import io.flutter.plugin.common.MethodChannel.Result;
-import io.flutter.plugin.common.PluginRegistry;
 import io.flutter.plugin.common.PluginRegistry.Registrar;
-import io.flutter.view.FlutterNativeView;
+import io.flutter.view.FlutterMain;
 import io.flutter.view.TextureRegistry;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
 
-public class VideoPlayerPlugin implements MethodCallHandler {
+/** Android platform implementation of the VideoPlayerPlugin. */
+public class VideoPlayerPlugin implements MethodCallHandler, FlutterPlugin {
+  private static final String TAG = "VideoPlayerPlugin";
+  private final LongSparseArray<VideoPlayer> videoPlayers = new LongSparseArray<>();
+  private FlutterState flutterState;
 
-  private static class VideoPlayer {
-    private static final String FORMAT_SS = "ss";
-    private static final String FORMAT_DASH = "dash";
-    private static final String FORMAT_HLS = "hls";
-    private static final String FORMAT_OTHER = "other";
+  /** Register this with the v2 embedding for the plugin to respond to lifecycle callbacks. */
+  public VideoPlayerPlugin() {}
 
-    private SimpleExoPlayer exoPlayer;
-
-    private Surface surface;
-
-    private final TextureRegistry.SurfaceTextureEntry textureEntry;
-
-    private QueuingEventSink eventSink = new QueuingEventSink();
-
-    private final EventChannel eventChannel;
-
-    private boolean isInitialized = false;
-
-    VideoPlayer(
-        Context context,
-        EventChannel eventChannel,
-        TextureRegistry.SurfaceTextureEntry textureEntry,
-        String dataSource,
-        Result result,
-        String formatHint) {
-      this.eventChannel = eventChannel;
-      this.textureEntry = textureEntry;
-
-      TrackSelector trackSelector = new DefaultTrackSelector();
-      exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
-
-      Uri uri = Uri.parse(dataSource);
-
-      DataSource.Factory dataSourceFactory;
-      if (isHTTP(uri)) {
-        dataSourceFactory =
-            new DefaultHttpDataSourceFactory(
-                "ExoPlayer",
-                null,
-                DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
-                DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
-                true);
-      } else {
-        dataSourceFactory = new DefaultDataSourceFactory(context, "ExoPlayer");
-      }
-
-      MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context);
-      exoPlayer.prepare(mediaSource);
-
-      setupVideoPlayer(eventChannel, textureEntry, result);
-    }
-
-    private static boolean isHTTP(Uri uri) {
-      if (uri == null || uri.getScheme() == null) {
-        return false;
-      }
-      String scheme = uri.getScheme();
-      return scheme.equals("http") || scheme.equals("https");
-    }
-
-    private MediaSource buildMediaSource(
-        Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) {
-      int type;
-      if (formatHint == null) {
-        type = Util.inferContentType(uri.getLastPathSegment());
-      } else {
-        switch (formatHint) {
-          case FORMAT_SS:
-            type = C.TYPE_SS;
-            break;
-          case FORMAT_DASH:
-            type = C.TYPE_DASH;
-            break;
-          case FORMAT_HLS:
-            type = C.TYPE_HLS;
-            break;
-          case FORMAT_OTHER:
-            type = C.TYPE_OTHER;
-            break;
-          default:
-            type = -1;
-            break;
-        }
-      }
-      switch (type) {
-        case C.TYPE_SS:
-          return new SsMediaSource.Factory(
-                  new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
-                  new DefaultDataSourceFactory(context, null, mediaDataSourceFactory))
-              .createMediaSource(uri);
-        case C.TYPE_DASH:
-          return new DashMediaSource.Factory(
-                  new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
-                  new DefaultDataSourceFactory(context, null, mediaDataSourceFactory))
-              .createMediaSource(uri);
-        case C.TYPE_HLS:
-          return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
-        case C.TYPE_OTHER:
-          return new ExtractorMediaSource.Factory(mediaDataSourceFactory)
-              .setExtractorsFactory(new DefaultExtractorsFactory())
-              .createMediaSource(uri);
-        default:
-          {
-            throw new IllegalStateException("Unsupported type: " + type);
-          }
-      }
-    }
-
-    private void setupVideoPlayer(
-        EventChannel eventChannel,
-        TextureRegistry.SurfaceTextureEntry textureEntry,
-        Result result) {
-
-      eventChannel.setStreamHandler(
-          new EventChannel.StreamHandler() {
-            @Override
-            public void onListen(Object o, EventChannel.EventSink sink) {
-              eventSink.setDelegate(sink);
-            }
-
-            @Override
-            public void onCancel(Object o) {
-              eventSink.setDelegate(null);
-            }
-          });
-
-      surface = new Surface(textureEntry.surfaceTexture());
-      exoPlayer.setVideoSurface(surface);
-      setAudioAttributes(exoPlayer);
-
-      exoPlayer.addListener(
-          new EventListener() {
-
-            @Override
-            public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
-              if (playbackState == Player.STATE_BUFFERING) {
-                sendBufferingUpdate();
-              } else if (playbackState == Player.STATE_READY) {
-                if (!isInitialized) {
-                  isInitialized = true;
-                  sendInitialized();
-                }
-              } else if (playbackState == Player.STATE_ENDED) {
-                Map<String, Object> event = new HashMap<>();
-                event.put("event", "completed");
-                eventSink.success(event);
-              }
-            }
-
-            @Override
-            public void onPlayerError(final ExoPlaybackException error) {
-              if (eventSink != null) {
-                eventSink.error("VideoError", "Video player had error " + error, null);
-              }
-            }
-          });
-
-      Map<String, Object> reply = new HashMap<>();
-      reply.put("textureId", textureEntry.id());
-      result.success(reply);
-    }
-
-    private void sendBufferingUpdate() {
-      Map<String, Object> event = new HashMap<>();
-      event.put("event", "bufferingUpdate");
-      List<? extends Number> range = Arrays.asList(0, exoPlayer.getBufferedPosition());
-      // iOS supports a list of buffered ranges, so here is a list with a single range.
-      event.put("values", Collections.singletonList(range));
-      eventSink.success(event);
-    }
-
-    @SuppressWarnings("deprecation")
-    private static void setAudioAttributes(SimpleExoPlayer exoPlayer) {
-      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-        exoPlayer.setAudioAttributes(
-            new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build());
-      } else {
-        exoPlayer.setAudioStreamType(C.STREAM_TYPE_MUSIC);
-      }
-    }
-
-    void play() {
-      exoPlayer.setPlayWhenReady(true);
-    }
-
-    void pause() {
-      exoPlayer.setPlayWhenReady(false);
-    }
-
-    void setLooping(boolean value) {
-      exoPlayer.setRepeatMode(value ? REPEAT_MODE_ALL : REPEAT_MODE_OFF);
-    }
-
-    void setVolume(double value) {
-      float bracketedValue = (float) Math.max(0.0, Math.min(1.0, value));
-      exoPlayer.setVolume(bracketedValue);
-    }
-
-    void seekTo(int location) {
-      exoPlayer.seekTo(location);
-    }
-
-    long getPosition() {
-      return exoPlayer.getCurrentPosition();
-    }
-
-    @SuppressWarnings("SuspiciousNameCombination")
-    private void sendInitialized() {
-      if (isInitialized) {
-        Map<String, Object> event = new HashMap<>();
-        event.put("event", "initialized");
-        event.put("duration", exoPlayer.getDuration());
-
-        if (exoPlayer.getVideoFormat() != null) {
-          Format videoFormat = exoPlayer.getVideoFormat();
-          int width = videoFormat.width;
-          int height = videoFormat.height;
-          int rotationDegrees = videoFormat.rotationDegrees;
-          // Switch the width/height if video was taken in portrait mode
-          if (rotationDegrees == 90 || rotationDegrees == 270) {
-            width = exoPlayer.getVideoFormat().height;
-            height = exoPlayer.getVideoFormat().width;
-          }
-          event.put("width", width);
-          event.put("height", height);
-        }
-        eventSink.success(event);
-      }
-    }
-
-    void dispose() {
-      if (isInitialized) {
-        exoPlayer.stop();
-      }
-      textureEntry.release();
-      eventChannel.setStreamHandler(null);
-      if (surface != null) {
-        surface.release();
-      }
-      if (exoPlayer != null) {
-        exoPlayer.release();
-      }
-    }
+  private VideoPlayerPlugin(Registrar registrar) {
+    this.flutterState =
+        new FlutterState(
+            registrar.context(),
+            registrar.messenger(),
+            registrar::lookupKeyForAsset,
+            registrar::lookupKeyForAsset,
+            registrar.textures());
+    flutterState.startListening(this);
   }
 
+  /** Registers this with the stable v1 embedding. Will not respond to lifecycle events. */
   public static void registerWith(Registrar registrar) {
     final VideoPlayerPlugin plugin = new VideoPlayerPlugin(registrar);
-    final MethodChannel channel =
-        new MethodChannel(registrar.messenger(), "flutter.io/videoPlayer");
-    channel.setMethodCallHandler(plugin);
     registrar.addViewDestroyListener(
-        new PluginRegistry.ViewDestroyListener() {
-          @Override
-          public boolean onViewDestroy(FlutterNativeView view) {
-            plugin.onDestroy();
-            return false; // We are not interested in assuming ownership of the NativeView.
-          }
+        view -> {
+          plugin.onDestroy();
+          return false; // We are not interested in assuming ownership of the NativeView.
         });
   }
 
-  private VideoPlayerPlugin(Registrar registrar) {
-    this.registrar = registrar;
-    this.videoPlayers = new LongSparseArray<>();
+  @Override
+  public void onAttachedToEngine(FlutterPluginBinding binding) {
+    this.flutterState =
+        new FlutterState(
+            binding.getApplicationContext(),
+            binding.getFlutterEngine().getDartExecutor(),
+            FlutterMain::getLookupKeyForAsset,
+            FlutterMain::getLookupKeyForAsset,
+            binding.getFlutterEngine().getRenderer());
+    flutterState.startListening(this);
   }
 
-  private final LongSparseArray<VideoPlayer> videoPlayers;
-
-  private final Registrar registrar;
+  @Override
+  public void onDetachedFromEngine(FlutterPluginBinding binding) {
+    if (flutterState == null) {
+      Log.wtf(TAG, "Detached from the engine before registering to it.");
+    }
+    flutterState.stopListening();
+    flutterState = null;
+  }
 
   private void disposeAllPlayers() {
     for (int i = 0; i < videoPlayers.size(); i++) {
@@ -339,8 +87,7 @@
 
   @Override
   public void onMethodCall(MethodCall call, Result result) {
-    TextureRegistry textures = registrar.textures();
-    if (textures == null) {
+    if (flutterState == null || flutterState.textureRegistry == null) {
       result.error("no_activity", "video_player plugin requires a foreground activity", null);
       return;
     }
@@ -350,23 +97,25 @@
         break;
       case "create":
         {
-          TextureRegistry.SurfaceTextureEntry handle = textures.createSurfaceTexture();
+          TextureRegistry.SurfaceTextureEntry handle =
+              flutterState.textureRegistry.createSurfaceTexture();
           EventChannel eventChannel =
               new EventChannel(
-                  registrar.messenger(), "flutter.io/videoPlayer/videoEvents" + handle.id());
+                  flutterState.binaryMessenger, "flutter.io/videoPlayer/videoEvents" + handle.id());
 
           VideoPlayer player;
           if (call.argument("asset") != null) {
             String assetLookupKey;
             if (call.argument("package") != null) {
               assetLookupKey =
-                  registrar.lookupKeyForAsset(call.argument("asset"), call.argument("package"));
+                  flutterState.keyForAssetAndPackageName.get(
+                      call.argument("asset"), call.argument("package"));
             } else {
-              assetLookupKey = registrar.lookupKeyForAsset(call.argument("asset"));
+              assetLookupKey = flutterState.keyForAsset.get(call.argument("asset"));
             }
             player =
                 new VideoPlayer(
-                    registrar.context(),
+                    flutterState.applicationContext,
                     eventChannel,
                     handle,
                     "asset:///" + assetLookupKey,
@@ -376,7 +125,7 @@
           } else {
             player =
                 new VideoPlayer(
-                    registrar.context(),
+                    flutterState.applicationContext,
                     eventChannel,
                     handle,
                     call.argument("uri"),
@@ -440,4 +189,43 @@
         break;
     }
   }
+
+  private interface KeyForAssetFn {
+    String get(String asset);
+  }
+
+  private interface KeyForAssetAndPackageName {
+    String get(String asset, String packageName);
+  }
+
+  private static final class FlutterState {
+    private final Context applicationContext;
+    private final BinaryMessenger binaryMessenger;
+    private final KeyForAssetFn keyForAsset;
+    private final KeyForAssetAndPackageName keyForAssetAndPackageName;
+    private final TextureRegistry textureRegistry;
+    private final MethodChannel methodChannel;
+
+    FlutterState(
+        Context applicationContext,
+        BinaryMessenger messenger,
+        KeyForAssetFn keyForAsset,
+        KeyForAssetAndPackageName keyForAssetAndPackageName,
+        TextureRegistry textureRegistry) {
+      this.applicationContext = applicationContext;
+      this.binaryMessenger = messenger;
+      this.keyForAsset = keyForAsset;
+      this.keyForAssetAndPackageName = keyForAssetAndPackageName;
+      this.textureRegistry = textureRegistry;
+      methodChannel = new MethodChannel(messenger, "flutter.io/videoPlayer");
+    }
+
+    void startListening(VideoPlayerPlugin methodCallHandler) {
+      methodChannel.setMethodCallHandler(methodCallHandler);
+    }
+
+    void stopListening() {
+      methodChannel.setMethodCallHandler(null);
+    }
+  }
 }
diff --git a/packages/video_player/example/android/app/src/main/AndroidManifest.xml b/packages/video_player/example/android/app/src/main/AndroidManifest.xml
index 914e82b..deec4b6 100644
--- a/packages/video_player/example/android/app/src/main/AndroidManifest.xml
+++ b/packages/video_player/example/android/app/src/main/AndroidManifest.xml
@@ -1,27 +1,35 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="io.flutter.plugins.videoplayerexample">
+  package="io.flutter.plugins.videoplayerexample">
 
-    <uses-permission android:name="android.permission.INTERNET"/>
+  <application
+    android:icon="@mipmap/ic_launcher"
+    android:label="video_player_example"
+    android:name="io.flutter.app.FlutterApplication"
+    android:networkSecurityConfig="@xml/network_security_config">
+    <activity
+      android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
+      android:exported="true"
+      android:hardwareAccelerated="true"
+      android:launchMode="singleTop"
+      android:name=".EmbeddingV1Activity"
+      android:theme="@style/LaunchTheme"
+      android:windowSoftInputMode="adjustResize">
+      <meta-data
+        android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
+        android:value="true"/>
+    </activity>
+    <activity
+      android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
+      android:hardwareAccelerated="true"
+      android:name=".MainActivity"
+      android:theme="@style/LaunchTheme"
+      android:windowSoftInputMode="adjustResize">
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN"/>
+        <category android:name="android.intent.category.LAUNCHER"/>
+      </intent-filter>
+    </activity>
+  </application>
 
-    <application
-        android:name="io.flutter.app.FlutterApplication"
-        android:label="video_player_example"
-        android:icon="@mipmap/ic_launcher"
-        android:networkSecurityConfig="@xml/network_security_config">
-        <activity
-            android:name=".MainActivity"
-            android:launchMode="singleTop"
-            android:theme="@style/LaunchTheme"
-            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
-            android:hardwareAccelerated="true"
-            android:windowSoftInputMode="adjustResize">
-            <meta-data
-                android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
-                android:value="true" />
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.LAUNCHER"/>
-            </intent-filter>
-        </activity>
-    </application>
+  <uses-permission android:name="android.permission.INTERNET"/>
 </manifest>
diff --git a/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java b/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java
new file mode 100644
index 0000000..f1af8ec
--- /dev/null
+++ b/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java
@@ -0,0 +1,17 @@
+// Copyright 2019 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.
+
+package io.flutter.plugins.videoplayerexample;
+
+import android.os.Bundle;
+import io.flutter.app.FlutterActivity;
+import io.flutter.plugins.GeneratedPluginRegistrant;
+
+public class EmbeddingV1Activity extends FlutterActivity {
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    GeneratedPluginRegistrant.registerWith(this);
+  }
+}
diff --git a/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java b/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java
index 133c3fa..2a0ae15 100644
--- a/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java
+++ b/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java
@@ -4,14 +4,13 @@
 
 package io.flutter.plugins.videoplayerexample;
 
-import android.os.Bundle;
-import io.flutter.app.FlutterActivity;
-import io.flutter.plugins.GeneratedPluginRegistrant;
+import io.flutter.embedding.android.FlutterActivity;
+import io.flutter.embedding.engine.FlutterEngine;
+import io.flutter.plugins.videoplayer.VideoPlayerPlugin;
 
 public class MainActivity extends FlutterActivity {
   @Override
-  protected void onCreate(Bundle savedInstanceState) {
-    super.onCreate(savedInstanceState);
-    GeneratedPluginRegistrant.registerWith(this);
+  public void configureFlutterEngine(FlutterEngine flutterEngine) {
+    flutterEngine.getPlugins().add(new VideoPlayerPlugin());
   }
 }
diff --git a/packages/video_player/example/android/gradle.properties b/packages/video_player/example/android/gradle.properties
index 8bd86f6..a673820 100644
--- a/packages/video_player/example/android/gradle.properties
+++ b/packages/video_player/example/android/gradle.properties
@@ -1 +1,4 @@
 org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
+android.enableR8=true
diff --git a/packages/video_player/example/lib/main.dart b/packages/video_player/example/lib/main.dart
index dea1086..196b908 100644
--- a/packages/video_player/example/lib/main.dart
+++ b/packages/video_player/example/lib/main.dart
@@ -388,11 +388,11 @@
                     Container(
                       padding: const EdgeInsets.only(top: 20.0),
                     ),
-                    const Text('With remote m3u8'),
+                    const Text('With remote mp4'),
                     Container(
                       padding: const EdgeInsets.all(20),
                       child: NetworkPlayerLifeCycle(
-                        'http://184.72.239.149/vod/smil:BigBuckBunny.smil/playlist.m3u8',
+                        'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
                         (BuildContext context,
                                 VideoPlayerController controller) =>
                             AspectRatioVideo(controller),
diff --git a/packages/video_player/example/pubspec.yaml b/packages/video_player/example/pubspec.yaml
index da48f06..bcb559b 100644
--- a/packages/video_player/example/pubspec.yaml
+++ b/packages/video_player/example/pubspec.yaml
@@ -8,7 +8,9 @@
 dev_dependencies:
   flutter_test:
     sdk: flutter
-
+  flutter_driver:
+    sdk: flutter
+  e2e: "^0.2.0"
   video_player:
     path: ../
 
diff --git a/packages/video_player/example/test_driver/video_player_e2e.dart b/packages/video_player/example/test_driver/video_player_e2e.dart
new file mode 100644
index 0000000..e726ab2
--- /dev/null
+++ b/packages/video_player/example/test_driver/video_player_e2e.dart
@@ -0,0 +1,67 @@
+// Copyright 2019, the Chromium project authors.  Please see the AUTHORS file
+// for details. 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:io';
+import 'package:e2e/e2e.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:video_player/video_player.dart';
+
+const Duration _playDuration = Duration(seconds: 1);
+
+void main() {
+  E2EWidgetsFlutterBinding.ensureInitialized();
+  VideoPlayerController _controller;
+
+  tearDown(() async => _controller.dispose());
+
+  group('asset videos', () {
+    setUp(() {
+      _controller = VideoPlayerController.asset('assets/Butterfly-209.mp4');
+    });
+
+    testWidgets('can be initialized', (WidgetTester tester) async {
+      await _controller.initialize();
+
+      expect(_controller.value.initialized, true);
+      expect(_controller.value.position, const Duration(seconds: 0));
+      expect(_controller.value.isPlaying, false);
+      expect(_controller.value.duration,
+          const Duration(seconds: 7, milliseconds: 540));
+    });
+
+    testWidgets('can be played', (WidgetTester tester) async {
+      await _controller.initialize();
+
+      await _controller.play();
+      await tester.pumpAndSettle(_playDuration);
+
+      expect(_controller.value.isPlaying, true);
+      expect(_controller.value.position,
+          (Duration position) => position > const Duration(seconds: 0));
+    }, skip: Platform.isIOS);
+
+    testWidgets('can seek', (WidgetTester tester) async {
+      await _controller.initialize();
+
+      await _controller.seekTo(const Duration(seconds: 3));
+
+      expect(_controller.value.position, const Duration(seconds: 3));
+    }, skip: Platform.isIOS);
+
+    testWidgets('can be paused', (WidgetTester tester) async {
+      await _controller.initialize();
+
+      // Play for a second, then pause, and then wait a second.
+      await _controller.play();
+      await tester.pumpAndSettle(_playDuration);
+      await _controller.pause();
+      final Duration pausedPosition = _controller.value.position;
+      await tester.pumpAndSettle(_playDuration);
+
+      // Verify that we stopped playing after the pause.
+      expect(_controller.value.isPlaying, false);
+      expect(_controller.value.position, pausedPosition);
+    }, skip: Platform.isIOS);
+  });
+}
diff --git a/packages/video_player/example/test_driver/video_player_e2e_test.dart b/packages/video_player/example/test_driver/video_player_e2e_test.dart
new file mode 100644
index 0000000..ccd7166
--- /dev/null
+++ b/packages/video_player/example/test_driver/video_player_e2e_test.dart
@@ -0,0 +1,16 @@
+// Copyright 2019, the Chromium project authors.  Please see the AUTHORS file
+// for details. 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:io';
+
+import 'package:flutter_driver/flutter_driver.dart';
+
+Future<void> main() async {
+  final FlutterDriver driver = await FlutterDriver.connect();
+  final String result =
+      await driver.requestData(null, timeout: const Duration(minutes: 1));
+  await driver.close();
+  exit(result == 'pass' ? 0 : 1);
+}
diff --git a/packages/video_player/pubspec.yaml b/packages/video_player/pubspec.yaml
index 1b0805b..656a81f 100644
--- a/packages/video_player/pubspec.yaml
+++ b/packages/video_player/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Flutter plugin for displaying inline video with other Flutter
   widgets on Android and iOS.
 author: Flutter Team <flutter-dev@googlegroups.com>
-version: 0.10.2+6
+version: 0.10.3
 homepage: https://github.com/flutter/plugins/tree/master/packages/video_player
 
 flutter:
@@ -22,4 +22,4 @@
 
 environment:
   sdk: ">=2.0.0-dev.28.0 <3.0.0"
-  flutter: ">=1.5.0 <2.0.0"
+  flutter: ">=1.9.1+hotfix.5 <2.0.0"