Merge "[WebView Support Library] Implement onSafeBrowsingHit" into pi-androidx-dev
diff --git a/car/res/values/integers.xml b/car/res/values/integers.xml
index 1acb572..dcae680 100644
--- a/car/res/values/integers.xml
+++ b/car/res/values/integers.xml
@@ -31,10 +31,6 @@
     <!-- Slide Down Menu -->
     <integer name="car_slide_down_menu_column_number">4</integer>
 
-    <!-- The length limit of body text in a paged list item. String longer than this limit should be
-         truncated. -->
-    <integer name="car_list_item_text_length_limit">120</integer>
-
     <!-- The length limit of text in a borderless button. String longer than this limit should be
          truncated. -->
     <integer name="car_borderless_button_text_length_limit">20</integer>
diff --git a/car/src/main/java/androidx/car/utils/CarUxRestrictionsUtils.java b/car/src/main/java/androidx/car/utils/CarUxRestrictionsUtils.java
index e06ba80..7fec3c0 100644
--- a/car/src/main/java/androidx/car/utils/CarUxRestrictionsUtils.java
+++ b/car/src/main/java/androidx/car/utils/CarUxRestrictionsUtils.java
@@ -20,7 +20,6 @@
 
 import android.car.drivingstate.CarUxRestrictions;
 import android.content.Context;
-import androidx.annotation.RestrictTo;
 import android.text.InputFilter;
 import android.widget.TextView;
 
@@ -28,7 +27,7 @@
 import java.util.Arrays;
 import java.util.List;
 
-import androidx.car.R;
+import androidx.annotation.RestrictTo;
 
 /**
  * Utility class that helps {@code View}s comply with {@link CarUxRestrictions}.
@@ -53,8 +52,7 @@
      */
     public static void comply(Context context, CarUxRestrictions carUxRestrictions, TextView tv) {
         if (sStringLengthFilter == null) {
-            int lengthLimit = context.getResources().getInteger(
-                    R.integer.car_list_item_text_length_limit);
+            int lengthLimit = carUxRestrictions.getMaxRestrictedStringLength();
             sStringLengthFilter = new InputFilter.LengthFilter(lengthLimit);
         }
 
diff --git a/compat/res/values/dimens.xml b/compat/res/values/dimens.xml
index 41abbe6..9ece458 100644
--- a/compat/res/values/dimens.xml
+++ b/compat/res/values/dimens.xml
@@ -72,8 +72,8 @@
     <dimen name="notification_right_side_padding_top">2dp</dimen>
 
     <!-- the maximum width of the large icon, above which it will be downscaled -->
-    <dimen name="notification_icon_max_width">320dp</dimen>
+    <dimen name="compat_notification_large_icon_max_width">320dp</dimen>
 
     <!-- the maximum height of the large icon, above which it will be downscaled -->
-    <dimen name="notification_icon_max_height">320dp</dimen>
+    <dimen name="compat_notification_large_icon_max_height">320dp</dimen>
 </resources>
diff --git a/compat/src/main/java/androidx/core/app/NotificationCompat.java b/compat/src/main/java/androidx/core/app/NotificationCompat.java
index aba40d1..9e60574 100644
--- a/compat/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/compat/src/main/java/androidx/core/app/NotificationCompat.java
@@ -977,8 +977,10 @@
             }
 
             Resources res = mContext.getResources();
-            int maxWidth = res.getDimensionPixelSize(R.dimen.notification_icon_max_width);
-            int maxHeight = res.getDimensionPixelSize(R.dimen.notification_icon_max_height);
+            int maxWidth =
+                    res.getDimensionPixelSize(R.dimen.compat_notification_large_icon_max_width);
+            int maxHeight =
+                    res.getDimensionPixelSize(R.dimen.compat_notification_large_icon_max_height);
             if (icon.getWidth() <= maxWidth && icon.getHeight() <= maxHeight) {
                 return icon;
             }
diff --git a/media-widget/OWNERS b/media-widget/OWNERS
new file mode 100644
index 0000000..c740b62
--- /dev/null
+++ b/media-widget/OWNERS
@@ -0,0 +1,6 @@
+sungsoo@google.com
+insun@google.com
+jinpark@google.com
+hdmoon@google.com
+jaewan@google.com
+akersten@google.com
\ No newline at end of file
diff --git a/media-widget/api/current.txt b/media-widget/api/current.txt
new file mode 100644
index 0000000..d531c3f
--- /dev/null
+++ b/media-widget/api/current.txt
@@ -0,0 +1,36 @@
+package androidx.media.widget {
+
+  public class MediaControlView2 extends android.view.ViewGroup {
+    ctor public MediaControlView2(android.content.Context);
+    ctor public MediaControlView2(android.content.Context, android.util.AttributeSet);
+    ctor public MediaControlView2(android.content.Context, android.util.AttributeSet, int);
+    ctor public MediaControlView2(android.content.Context, android.util.AttributeSet, int, int);
+    method public void onMeasure(int, int);
+    method public void requestPlayButtonFocus();
+  }
+
+  public class VideoView2 extends android.view.ViewGroup {
+    ctor public VideoView2(android.content.Context);
+    ctor public VideoView2(android.content.Context, android.util.AttributeSet);
+    ctor public VideoView2(android.content.Context, android.util.AttributeSet, int);
+    ctor public VideoView2(android.content.Context, android.util.AttributeSet, int, int);
+    method public androidx.media.widget.MediaControlView2 getMediaControlView2();
+    method public float getSpeed();
+    method public int getViewType();
+    method public boolean isSubtitleEnabled();
+    method public void onAttachedToWindow();
+    method public void onDetachedFromWindow();
+    method public void onMeasure(int, int);
+    method public void setAudioAttributes(android.media.AudioAttributes);
+    method public void setAudioFocusRequest(int);
+    method public void setMediaControlView2(androidx.media.widget.MediaControlView2, long);
+    method public void setSpeed(float);
+    method public void setSubtitleEnabled(boolean);
+    method public void setVideoUri(android.net.Uri, java.util.Map<java.lang.String, java.lang.String>);
+    method public void setViewType(int);
+    field public static final int VIEW_TYPE_SURFACEVIEW = 0; // 0x0
+    field public static final int VIEW_TYPE_TEXTUREVIEW = 1; // 0x1
+  }
+
+}
+
diff --git a/media-widget/build.gradle b/media-widget/build.gradle
new file mode 100644
index 0000000..e6ff65d
--- /dev/null
+++ b/media-widget/build.gradle
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+    api(project(":media"))
+    api(project(":mediarouter"))
+    api(project(":appcompat"))
+    api(project(":palette"))
+
+    androidTestImplementation(TEST_RUNNER_TMP, libs.exclude_for_espresso)
+    androidTestImplementation(ESPRESSO_CORE_TMP, libs.exclude_for_espresso)
+    androidTestImplementation(TEST_RULES_TMP, libs.exclude_for_espresso)
+    androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation project(':internal-testutils')
+}
+
+supportLibrary {
+    name = "Android Support Media Widget"
+    publish = true
+    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenGroup = LibraryGroups.MEDIA
+    inceptionYear = "2011"
+    description = "Android Support Media Widget"
+    minSdkVersion = 19
+    failOnDeprecationWarnings = false
+    failOnDeprecationWarnings = false
+}
diff --git a/media-widget/src/androidTest/AndroidManifest.xml b/media-widget/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..dd6e9e0
--- /dev/null
+++ b/media-widget/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.media.widget.test">
+    <uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
+
+    <application>
+        <activity android:name="androidx.media.widget.VideoView2TestActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:label="VideoView2TestActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/media/src/androidTest/java/androidx/widget/MediaUtils2.java b/media-widget/src/androidTest/java/androidx/media/widget/MediaUtils2.java
similarity index 98%
rename from media/src/androidTest/java/androidx/widget/MediaUtils2.java
rename to media-widget/src/androidTest/java/androidx/media/widget/MediaUtils2.java
index 29eb9f6..59b2567 100644
--- a/media/src/androidTest/java/androidx/widget/MediaUtils2.java
+++ b/media-widget/src/androidTest/java/androidx/media/widget/MediaUtils2.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.widget;
+package androidx.media.widget;
 
 import android.content.Context;
 import android.content.res.AssetFileDescriptor;
diff --git a/media/src/androidTest/java/androidx/widget/VideoView2Test.java b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java
similarity index 98%
rename from media/src/androidTest/java/androidx/widget/VideoView2Test.java
rename to media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java
index 7f3dcfc..2876e47 100644
--- a/media/src/androidTest/java/androidx/widget/VideoView2Test.java
+++ b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.widget;
+package androidx.media.widget;
 
 import static android.content.Context.KEYGUARD_SERVICE;
 
@@ -44,7 +44,7 @@
 import android.view.WindowManager;
 
 import androidx.media.AudioAttributesCompat;
-import androidx.media.test.R;
+import androidx.media.widget.test.R;
 
 import org.junit.After;
 import org.junit.Before;
diff --git a/media/src/androidTest/java/androidx/widget/VideoView2TestActivity.java b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2TestActivity.java
similarity index 93%
rename from media/src/androidTest/java/androidx/widget/VideoView2TestActivity.java
rename to media-widget/src/androidTest/java/androidx/media/widget/VideoView2TestActivity.java
index 26a9c92..d6a3ebc 100644
--- a/media/src/androidTest/java/androidx/widget/VideoView2TestActivity.java
+++ b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2TestActivity.java
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.widget;
+package androidx.media.widget;
 
 import android.app.Activity;
 import android.os.Bundle;
 
-import androidx.media.test.R;
+import androidx.media.widget.test.R;
 
 /**
  * A minimal application for {@link VideoView2} test.
diff --git a/media-widget/src/androidTest/res/layout/videoview2_layout.xml b/media-widget/src/androidTest/res/layout/videoview2_layout.xml
new file mode 100644
index 0000000..40e07d5
--- /dev/null
+++ b/media-widget/src/androidTest/res/layout/videoview2_layout.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <androidx.media.widget.VideoView2
+        android:id="@+id/videoview"
+        android:layout_width="160dp"
+        android:layout_height="120dp"/>
+</LinearLayout>
diff --git a/media-widget/src/androidTest/res/raw/testvideo.3gp b/media-widget/src/androidTest/res/raw/testvideo.3gp
new file mode 100644
index 0000000..8329311
--- /dev/null
+++ b/media-widget/src/androidTest/res/raw/testvideo.3gp
Binary files differ
diff --git a/media-widget/src/androidTest/res/values/themes.xml b/media-widget/src/androidTest/res/values/themes.xml
new file mode 100644
index 0000000..2843f5e
--- /dev/null
+++ b/media-widget/src/androidTest/res/values/themes.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+
+    <style name="HasWindowTitle">
+        <item name="windowNoTitle">false</item>
+    </style>
+
+</resources>
\ No newline at end of file
diff --git a/media-widget/src/main/AndroidManifest.xml b/media-widget/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..824903e
--- /dev/null
+++ b/media-widget/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest package="androidx.media.widget"/>
diff --git a/media/src/main/java/androidx/widget/BaseLayout.java b/media-widget/src/main/java/androidx/media/widget/BaseLayout.java
similarity index 93%
rename from media/src/main/java/androidx/widget/BaseLayout.java
rename to media-widget/src/main/java/androidx/media/widget/BaseLayout.java
index 127006e..0b6988a 100644
--- a/media/src/main/java/androidx/widget/BaseLayout.java
+++ b/media-widget/src/main/java/androidx/media/widget/BaseLayout.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.widget;
+package androidx.media.widget;
 
 import android.content.Context;
 import android.graphics.drawable.Drawable;
@@ -22,8 +22,6 @@
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewGroup.LayoutParams;
-import android.view.ViewGroup.MarginLayoutParams;
 
 import androidx.annotation.AttrRes;
 import androidx.annotation.NonNull;
@@ -83,8 +81,8 @@
         int count = getChildCount();
 
         final boolean measureMatchParentChildren =
-                View.MeasureSpec.getMode(widthMeasureSpec) != View.MeasureSpec.EXACTLY
-                        || View.MeasureSpec.getMode(heightMeasureSpec) != View.MeasureSpec.EXACTLY;
+                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY
+                        || MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
         mMatchParentChildren.clear();
 
         int maxHeight = 0;
@@ -144,8 +142,8 @@
                     final int width = Math.max(0, getMeasuredWidth()
                             - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                             - lp.leftMargin - lp.rightMargin);
-                    childWidthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
-                            width, View.MeasureSpec.EXACTLY);
+                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+                            width, MeasureSpec.EXACTLY);
                 } else {
                     childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                             getPaddingLeftWithForeground() + getPaddingRightWithForeground()
@@ -157,8 +155,8 @@
                     final int height = Math.max(0, getMeasuredHeight()
                             - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                             - lp.topMargin - lp.bottomMargin);
-                    childHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec(
-                            height, View.MeasureSpec.EXACTLY);
+                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+                            height, MeasureSpec.EXACTLY);
                 } else {
                     childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                             getPaddingTopWithForeground() + getPaddingBottomWithForeground()
diff --git a/media/src/main/java/androidx/widget/MediaControlView2.java b/media-widget/src/main/java/androidx/media/widget/MediaControlView2.java
similarity index 91%
rename from media/src/main/java/androidx/widget/MediaControlView2.java
rename to media-widget/src/main/java/androidx/media/widget/MediaControlView2.java
index 51d427f..81085c3 100644
--- a/media/src/main/java/androidx/widget/MediaControlView2.java
+++ b/media-widget/src/main/java/androidx/media/widget/MediaControlView2.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.widget;
+package androidx.media.widget;
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
@@ -27,6 +27,7 @@
 import android.support.v4.media.session.PlaybackStateCompat;
 import android.util.AttributeSet;
 import android.view.Gravity;
+import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
@@ -50,11 +51,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
-import androidx.media.R;
 import androidx.media.SessionToken2;
-// import androidx.mediarouter.app.MediaRouteButton;
-// import androidx.mediarouter.media.MediaRouter;
-// import androidx.mediarouter.media.MediaRouteSelector;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -65,38 +62,37 @@
 import java.util.Locale;
 
 /**
- * @hide
- * A View that contains the controls for MediaPlayer2.
- * It provides a wide range of UI including buttons such as "Play/Pause", "Rewind", "Fast Forward",
- * "Subtitle", "Full Screen", and it is also possible to add multiple custom buttons.
+ * A View that contains the controls for {@link android.media.MediaPlayer}.
+ * It provides a wide range of buttons that serve the following functions: play/pause,
+ * rewind/fast-forward, skip to next/previous, select subtitle track, enter/exit full screen mode,
+ * adjust video quality, select audio track, mute/unmute, and adjust playback speed.
  *
  * <p>
  * <em> MediaControlView2 can be initialized in two different ways: </em>
- * 1) When VideoView2 is initialized, it automatically initializes a MediaControlView2 instance and
- * adds it to the view.
- * 2) Initialize MediaControlView2 programmatically and add it to a ViewGroup instance.
+ * 1) When initializing {@link VideoView2} a default MediaControlView2 is created.
+ * 2) Initialize MediaControlView2 programmatically and add it to a {@link ViewGroup} instance.
  *
  * In the first option, VideoView2 automatically connects MediaControlView2 to MediaController,
- * which is necessary to communicate with MediaSession2. In the second option, however, the
- * developer needs to manually retrieve a MediaController instance and set it to MediaControlView2
- * by calling setController(MediaController controller).
+ * which is necessary to communicate with MediaSession. In the second option, however, the
+ * developer needs to manually retrieve a MediaController instance from MediaSession and set it to
+ * MediaControlView2.
  *
  * <p>
  * There is no separate method that handles the show/hide behavior for MediaControlView2. Instead,
- * one can directly change the visibility of this view by calling View.setVisibility(int). The
- * values supported are View.VISIBLE and View.GONE.
- * In addition, the following customization is supported:
- * Set focus to the play/pause button by calling requestPlayButtonFocus().
+ * one can directly change the visibility of this view by calling {@link View#setVisibility(int)}.
+ * The values supported are View.VISIBLE and View.GONE.
  *
  * <p>
- * It is also possible to add custom buttons with custom icons and actions inside MediaControlView2.
- * Those buttons will be shown when the overflow button is clicked.
- * See VideoView2#setCustomActions for more details on how to add.
+ * In addition, the following customizations are supported:
+ * 1) Set focus to the play/pause button by calling requestPlayButtonFocus().
+ * 2) Set full screen mode
+ *
  */
 @RequiresApi(21) // TODO correct minSdk API use incompatibilities and remove before release.
-@RestrictTo(LIBRARY_GROUP)
 public class MediaControlView2 extends BaseLayout {
-    /** @hide */
+    /**
+     * @hide
+     */
     @RestrictTo(LIBRARY_GROUP)
     @IntDef({
             BUTTON_PLAY_PAUSE,
@@ -185,8 +181,6 @@
 
     static final String ARGUMENT_KEY_FULLSCREEN = "fullScreen";
 
-    // TODO: Make these constants public api to support custom video view.
-    // TODO: Combine these constants into one regarding TrackInfo.
     static final String KEY_VIDEO_TRACK_COUNT = "VideoTrackCount";
     static final String KEY_AUDIO_TRACK_COUNT = "AudioTrackCount";
     static final String KEY_SUBTITLE_TRACK_COUNT = "SubtitleTrackCount";
@@ -194,8 +188,6 @@
     static final String KEY_SELECTED_AUDIO_INDEX = "SelectedAudioIndex";
     static final String KEY_SELECTED_SUBTITLE_INDEX = "SelectedSubtitleIndex";
     static final String EVENT_UPDATE_TRACK_STATUS = "UpdateTrackStatus";
-
-    // TODO: Remove this once integrating with MediaSession2 & MediaMetadata2
     static final String KEY_STATE_IS_ADVERTISEMENT = "MediaTypeAdvertisement";
     static final String EVENT_UPDATE_MEDIA_TYPE_STATUS = "UpdateMediaTypeStatus";
 
@@ -203,8 +195,8 @@
     static final String COMMAND_SHOW_SUBTITLE = "showSubtitle";
     // String for sending command to hide subtitle to MediaSession.
     static final String COMMAND_HIDE_SUBTITLE = "hideSubtitle";
-    // TODO: remove once the implementation is revised
-    public static final String COMMAND_SET_FULLSCREEN = "setFullscreen";
+    // String for sending command to set fullscreen to MediaSession.
+    static final String COMMAND_SET_FULLSCREEN = "setFullscreen";
     // String for sending command to select audio track to MediaSession.
     static final String COMMAND_SELECT_AUDIO_TRACK = "SelectTrack";
     // String for sending command to set playback speed to MediaSession.
@@ -228,7 +220,6 @@
 
     private static final int SIZE_TYPE_EMBEDDED = 0;
     private static final int SIZE_TYPE_FULL = 1;
-    // TODO: add support for Minimal size type.
     private static final int SIZE_TYPE_MINIMAL = 2;
 
     private static final int MAX_PROGRESS = 1000;
@@ -282,9 +273,6 @@
     private TextView mTitleView;
     private View mAdExternalLink;
     private ImageButton mBackButton;
-    // TODO (b/77158231) revive
-    // private MediaRouteButton mRouteButton;
-    // private MediaRouteSelector mRouteSelector;
 
     // Relating to Center View
     private ViewGroup mCenterView;
@@ -357,12 +345,6 @@
 
     public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs,
             int defStyleAttr, int defStyleRes) {
-//        super((instance, superProvider, privateProvider) ->
-//                ApiLoader.getProvider().createMediaControlView2(
-//                        (MediaControlView2) instance, superProvider, privateProvider,
-//                        attrs, defStyleAttr, defStyleRes),
-//                context, attrs, defStyleAttr, defStyleRes);
-//        mProvider.initialize(attrs, defStyleAttr, defStyleRes);
         super(context, attrs, defStyleAttr, defStyleRes);
 
         mResources = getContext().getResources();
@@ -373,27 +355,32 @@
 
     /**
      * Sets MediaSession2 token to control corresponding MediaSession2.
+     * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     public void setMediaSessionToken(SessionToken2 token) {
-        //mProvider.setMediaSessionToken_impl(token);
     }
 
     /**
      * Registers a callback to be invoked when the fullscreen mode should be changed.
      * @param l The callback that will be run
+     * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     public void setOnFullScreenListener(OnFullScreenListener l) {
-        //mProvider.setOnFullScreenListener_impl(l);
     }
 
     /**
+     * Sets MediaController instance to MediaControlView2, which makes it possible to send and
+     * receive data between MediaControlView2 and VideoView2. This method does not need to be called
+     * when MediaControlView2 is initialized with VideoView2.
      * @hide TODO: remove once the implementation is revised
      */
     @RestrictTo(LIBRARY_GROUP)
     public void setController(MediaControllerCompat controller) {
         mController = controller;
         if (controller != null) {
-            mControls = controller.getTransportControls();
+            mControls = mController.getTransportControls();
             // Set mMetadata and mPlaybackState to existing MediaSession variables since they may
             // be called before the callback is called
             mPlaybackState = mController.getPlaybackState();
@@ -427,8 +414,6 @@
      */
     @RestrictTo(LIBRARY_GROUP)
     public void setButtonVisibility(@Button int button, /*@Visibility*/ int visibility) {
-        // TODO: add member variables for Fast-Forward/Prvious/Rewind buttons to save visibility in
-        // order to prevent being overriden inside updateLayout().
         switch (button) {
             case MediaControlView2.BUTTON_PLAY_PAUSE:
                 if (mPlayPauseButton != null && canPause()) {
@@ -497,7 +482,9 @@
     /**
      * Interface definition of a callback to be invoked to inform the fullscreen mode is changed.
      * Application should handle the fullscreen mode accordingly.
+     * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     public interface OnFullScreenListener {
         /**
          * Called to indicate a fullscreen mode change.
@@ -515,7 +502,6 @@
         return false;
     }
 
-    // TODO: Should this function be removed?
     @Override
     public boolean onTrackballEvent(MotionEvent ev) {
         return false;
@@ -549,7 +535,6 @@
                     R.dimen.mcv2_embedded_icon_size);
             int marginSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_margin);
 
-            // TODO: add support for Advertisement Mode.
             if (mMediaType == MEDIA_TYPE_DEFAULT) {
                 // Max number of icons inside BottomBarRightView for Music mode is 4.
                 int maxIconCount = 4;
@@ -579,7 +564,6 @@
             mPrevWidth = currWidth;
             mPrevHeight = currHeight;
         }
-        // TODO: move this to a different location.
         // Update title bar parameters in order to avoid overlap between title view and the right
         // side of the title bar.
         updateTitleBarLayout();
@@ -589,7 +573,6 @@
     public void setEnabled(boolean enabled) {
         super.setEnabled(enabled);
 
-        // TODO: Merge the below code with disableUnsupportedButtons().
         if (mPlayPauseButton != null) {
             mPlayPauseButton.setEnabled(enabled);
         }
@@ -624,20 +607,6 @@
         }
     }
 
-    // TODO (b/77158231) revive once androidx.mediarouter.* packagaes are available.
-    /*
-    void setRouteSelector(MediaRouteSelector selector) {
-        mRouteSelector = selector;
-        if (mRouteSelector != null && !mRouteSelector.isEmpty()) {
-            mRouteButton.setRouteSelector(selector, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
-            mRouteButton.setVisibility(View.VISIBLE);
-        } else {
-            mRouteButton.setRouteSelector(MediaRouteSelector.EMPTY);
-            mRouteButton.setVisibility(View.GONE);
-        }
-    }
-    */
-
     ///////////////////////////////////////////////////
     // Protected or private methods
     ///////////////////////////////////////////////////
@@ -696,20 +665,19 @@
      *
      * @return The controller view.
      */
-    // TODO: This was "protected". Determine if it should be protected in MCV2.
     private ViewGroup makeControllerView() {
         ViewGroup root = (ViewGroup) inflateLayout(getContext(), R.layout.media_controller);
         initControllerView(root);
         return root;
     }
 
-    // TODO(b/76444971) make sure this is compatible with ApiHelper's one in updatable.
     private View inflateLayout(Context context, int resId) {
         LayoutInflater inflater = (LayoutInflater) context
                 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
         return inflater.inflate(resId, null);
     }
 
+    @SuppressWarnings("deprecation")
     private void initControllerView(ViewGroup v) {
         // Relating to Title Bar View
         mTitleBar = v.findViewById(R.id.title_bar);
@@ -720,9 +688,6 @@
             mBackButton.setOnClickListener(mBackListener);
             mBackButton.setVisibility(View.GONE);
         }
-        // TODO (b/77158231) revive
-        // mRouteButton = v.findViewById(R.id.cast);
-
         // Relating to Center View
         mCenterView = v.findViewById(R.id.center_view);
         mTransportControls = inflateTransportControls(R.layout.embedded_transport_controls);
@@ -775,7 +740,6 @@
         mFullScreenButton = v.findViewById(R.id.fullscreen);
         if (mFullScreenButton != null) {
             mFullScreenButton.setOnClickListener(mFullScreenListener);
-            // TODO: Show Fullscreen button when only it is possible.
         }
         mOverflowButtonRight = v.findViewById(R.id.overflow_right);
         if (mOverflowButtonRight != null) {
@@ -819,7 +783,7 @@
         mSettingsWindowMargin = (-1) * mResources.getDimensionPixelSize(
                 R.dimen.mcv2_settings_offset);
         mSettingsWindow = new PopupWindow(mSettingsListView, mEmbeddedSettingsItemWidth,
-                ViewGroup.LayoutParams.WRAP_CONTENT, true);
+                LayoutParams.WRAP_CONTENT, true);
     }
 
     /**
@@ -837,13 +801,6 @@
             if (mFfwdButton != null && !canSeekForward()) {
                 mFfwdButton.setEnabled(false);
             }
-            // TODO What we really should do is add a canSeek to the MediaPlayerControl interface;
-            // this scheme can break the case when applications want to allow seek through the
-            // progress bar but disable forward/backward buttons.
-            //
-            // However, currently the flags SEEK_BACKWARD_AVAILABLE, SEEK_FORWARD_AVAILABLE,
-            // and SEEK_AVAILABLE are all (un)set together; as such the aforementioned issue
-            // shouldn't arise in existing applications.
             if (mProgress != null && !canSeekBackward() && !canSeekForward()) {
                 mProgress.setEnabled(false);
             }
@@ -1033,14 +990,14 @@
         }
     };
 
-    private final View.OnClickListener mPlayPauseListener = new View.OnClickListener() {
+    private final OnClickListener mPlayPauseListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
             togglePausePlayState();
         }
     };
 
-    private final View.OnClickListener mRewListener = new View.OnClickListener() {
+    private final OnClickListener mRewListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
             int pos = getCurrentPosition() - REWIND_TIME_MS;
@@ -1049,7 +1006,7 @@
         }
     };
 
-    private final View.OnClickListener mFfwdListener = new View.OnClickListener() {
+    private final OnClickListener mFfwdListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
             int pos = getCurrentPosition() + FORWARD_TIME_MS;
@@ -1058,28 +1015,32 @@
         }
     };
 
-    private final View.OnClickListener mNextListener = new View.OnClickListener() {
+    private final OnClickListener mNextListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
             mControls.skipToNext();
         }
     };
 
-    private final View.OnClickListener mPrevListener = new View.OnClickListener() {
+    private final OnClickListener mPrevListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
             mControls.skipToPrevious();
         }
     };
 
-    private final View.OnClickListener mBackListener = new View.OnClickListener() {
+    private final OnClickListener mBackListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
-            // TODO: implement
+            View parent = (View) getParent();
+            if (parent != null) {
+                parent.onKeyDown(KeyEvent.KEYCODE_BACK,
+                        new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK));
+            }
         }
     };
 
-    private final View.OnClickListener mSubtitleListener = new View.OnClickListener() {
+    private final OnClickListener mSubtitleListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
             mSettingsMode = SETTINGS_MODE_SUBTITLE_TRACK;
@@ -1089,7 +1050,7 @@
         }
     };
 
-    private final View.OnClickListener mVideoQualityListener = new View.OnClickListener() {
+    private final OnClickListener mVideoQualityListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
             mSettingsMode = SETTINGS_MODE_VIDEO_QUALITY;
@@ -1099,11 +1060,10 @@
         }
     };
 
-    private final View.OnClickListener mFullScreenListener = new View.OnClickListener() {
+    private final OnClickListener mFullScreenListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
             final boolean isEnteringFullScreen = !mIsFullScreen;
-            // TODO: Re-arrange the button layouts according to the UX.
             if (isEnteringFullScreen) {
                 mFullScreenButton.setImageDrawable(
                         mResources.getDrawable(R.drawable.ic_fullscreen_exit, null));
@@ -1119,7 +1079,7 @@
         }
     };
 
-    private final View.OnClickListener mOverflowRightListener = new View.OnClickListener() {
+    private final OnClickListener mOverflowRightListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
             mBasicControls.setVisibility(View.GONE);
@@ -1127,7 +1087,7 @@
         }
     };
 
-    private final View.OnClickListener mOverflowLeftListener = new View.OnClickListener() {
+    private final OnClickListener mOverflowLeftListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
             mBasicControls.setVisibility(View.VISIBLE);
@@ -1135,7 +1095,7 @@
         }
     };
 
-    private final View.OnClickListener mMuteButtonListener = new View.OnClickListener() {
+    private final OnClickListener mMuteButtonListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
             if (!mIsMute) {
@@ -1156,7 +1116,7 @@
         }
     };
 
-    private final View.OnClickListener mSettingsButtonListener = new View.OnClickListener() {
+    private final OnClickListener mSettingsButtonListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
             mSettingsMode = SETTINGS_MODE_MAIN;
@@ -1180,7 +1140,6 @@
                         mSubSettingsAdapter.setCheckPosition(mSelectedSpeedIndex);
                         mSettingsMode = SETTINGS_MODE_PLAYBACK_SPEED;
                     } else if (position == SETTINGS_MODE_HELP) {
-                        // TODO: implement this.
                         mSettingsWindow.dismiss();
                         return;
                     }
@@ -1211,7 +1170,6 @@
                     mSettingsWindow.dismiss();
                     break;
                 case SETTINGS_MODE_HELP:
-                    // TODO: implement this.
                     break;
                 case SETTINGS_MODE_SUBTITLE_TRACK:
                     if (position != mSelectedSubtitleTrackIndex) {
@@ -1237,7 +1195,6 @@
                     mSettingsWindow.dismiss();
                     break;
                 case SETTINGS_MODE_VIDEO_QUALITY:
-                    // TODO: add support for video quality
                     mSelectedVideoQualityIndex = position;
                     mSettingsWindow.dismiss();
                     break;
@@ -1358,10 +1315,6 @@
         int embeddedWidth = mTimeView.getWidth() + embeddedBottomBarRightWidthMax;
         int screenMaxLength = Math.max(screenWidth, screenHeight);
 
-        if (fullWidth > screenMaxLength) {
-            // TODO: screen may be smaller than the length needed for Full size.
-        }
-
         boolean isFullSize = (mMediaType == MEDIA_TYPE_DEFAULT) ? (currWidth == screenMaxLength) :
                 (currWidth == screenWidth && currHeight == screenHeight);
 
@@ -1389,6 +1342,7 @@
         }
     }
 
+    @SuppressWarnings("deprecation")
     private void updateLayoutForSizeChange(int sizeType) {
         mSizeType = sizeType;
         RelativeLayout.LayoutParams timeViewParams =
@@ -1519,7 +1473,6 @@
                 mRewButton.setVisibility(View.GONE);
             }
         }
-        // TODO: Add support for Next and Previous buttons
         mNextButton = v.findViewById(R.id.next);
         if (mNextButton != null) {
             mNextButton.setOnClickListener(mNextListener);
@@ -1594,7 +1547,6 @@
                 mSettingsWindowMargin - totalHeight, Gravity.BOTTOM | Gravity.RIGHT);
     }
 
-    @RequiresApi(26) // TODO correct minSdk API use incompatibilities and remove before release.
     private class MediaControllerCallback extends MediaControllerCompat.Callback {
         @Override
         public void onPlaybackStateChanged(PlaybackStateCompat state) {
@@ -1666,16 +1618,12 @@
                 for (final PlaybackStateCompat.CustomAction action : customActions) {
                     ImageButton button = new ImageButton(getContext(),
                             null /* AttributeSet */, 0 /* Style */);
-                    // TODO: Apply R.style.BottomBarButton to this button using library context.
                     // Refer Constructor with argument (int defStyleRes) of View.java
                     button.setImageResource(action.getIcon());
-                    button.setTooltipText(action.getName());
                     final String actionString = action.getAction().toString();
-                    button.setOnClickListener(new View.OnClickListener() {
+                    button.setOnClickListener(new OnClickListener() {
                         @Override
                         public void onClick(View v) {
-                            // TODO: Currently, we are just sending extras that came from session.
-                            // Is it the right behavior?
                             mControls.sendCustomAction(actionString, action.getExtras());
                             setVisibility(View.VISIBLE);
                         }
@@ -1704,7 +1652,6 @@
                     mAudioTrackCount = extras.getInt(KEY_AUDIO_TRACK_COUNT);
                     mAudioTrackList = new ArrayList<String>();
                     if (mAudioTrackCount > 0) {
-                        // TODO: add more text about track info.
                         for (int i = 0; i < mAudioTrackCount; i++) {
                             String track = mResources.getString(
                                     R.string.MediaControlView2_audio_track_number_text, i + 1);
@@ -1783,14 +1730,12 @@
         @Override
         public long getItemId(int position) {
             // Auto-generated method stub--does not have any purpose here
-            // TODO: implement this.
             return 0;
         }
 
         @Override
         public Object getItem(int position) {
             // Auto-generated method stub--does not have any purpose here
-            // TODO: implement this.
             return null;
         }
 
@@ -1834,7 +1779,6 @@
         }
     }
 
-    // TODO: extend this class from SettingsAdapter
     private class SubSettingsAdapter extends BaseAdapter {
         private List<String> mTexts;
         private int mCheckPosition;
@@ -1861,14 +1805,12 @@
         @Override
         public long getItemId(int position) {
             // Auto-generated method stub--does not have any purpose here
-            // TODO: implement this.
             return 0;
         }
 
         @Override
         public Object getItem(int position) {
             // Auto-generated method stub--does not have any purpose here
-            // TODO: implement this.
             return null;
         }
 
diff --git a/media-widget/src/main/java/androidx/media/widget/SubtitleView.java b/media-widget/src/main/java/androidx/media/widget/SubtitleView.java
new file mode 100644
index 0000000..1faff2f
--- /dev/null
+++ b/media-widget/src/main/java/androidx/media/widget/SubtitleView.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.os.Looper;
+import android.util.AttributeSet;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.media.subtitle.SubtitleController.Anchor;
+import androidx.media.subtitle.SubtitleTrack.RenderingWidget;
+
+@RequiresApi(21)
+class SubtitleView extends BaseLayout implements Anchor {
+    private static final String TAG = "SubtitleView";
+
+    private RenderingWidget mSubtitleWidget;
+    private RenderingWidget.OnChangedListener mSubtitlesChangedListener;
+
+    SubtitleView(Context context) {
+        this(context, null);
+    }
+
+    SubtitleView(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    SubtitleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    SubtitleView(
+            Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public void setSubtitleWidget(RenderingWidget subtitleWidget) {
+        if (mSubtitleWidget == subtitleWidget) {
+            return;
+        }
+
+        final boolean attachedToWindow = isAttachedToWindow();
+        if (mSubtitleWidget != null) {
+            if (attachedToWindow) {
+                mSubtitleWidget.onDetachedFromWindow();
+            }
+
+            mSubtitleWidget.setOnChangedListener(null);
+        }
+        mSubtitleWidget = subtitleWidget;
+
+        if (subtitleWidget != null) {
+            if (mSubtitlesChangedListener == null) {
+                mSubtitlesChangedListener = new RenderingWidget.OnChangedListener() {
+                    @Override
+                    public void onChanged(RenderingWidget renderingWidget) {
+                        invalidate();
+                    }
+                };
+            }
+
+            setWillNotDraw(false);
+            subtitleWidget.setOnChangedListener(mSubtitlesChangedListener);
+
+            if (attachedToWindow) {
+                subtitleWidget.onAttachedToWindow();
+                requestLayout();
+            }
+        } else {
+            setWillNotDraw(true);
+        }
+
+        invalidate();
+    }
+
+    @Override
+    public Looper getSubtitleLooper() {
+        return Looper.getMainLooper();
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        if (mSubtitleWidget != null) {
+            mSubtitleWidget.onAttachedToWindow();
+        }
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        if (mSubtitleWidget != null) {
+            mSubtitleWidget.onDetachedFromWindow();
+        }
+    }
+
+    @Override
+    public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+
+        if (mSubtitleWidget != null) {
+            final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+            final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+            mSubtitleWidget.setSize(width, height);
+        }
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        super.draw(canvas);
+
+        if (mSubtitleWidget != null) {
+            final int saveCount = canvas.save();
+            canvas.translate(getPaddingLeft(), getPaddingTop());
+            mSubtitleWidget.draw(canvas);
+            canvas.restoreToCount(saveCount);
+        }
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return SubtitleView.class.getName();
+    }
+}
diff --git a/media/src/main/java/androidx/widget/VideoSurfaceView.java b/media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java
similarity index 97%
rename from media/src/main/java/androidx/widget/VideoSurfaceView.java
rename to media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java
index eafa6f3..4415ea9 100644
--- a/media/src/main/java/androidx/widget/VideoSurfaceView.java
+++ b/media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package androidx.widget;
+package androidx.media.widget;
 
-import static androidx.widget.VideoView2.VIEW_TYPE_SURFACEVIEW;
+import static androidx.media.widget.VideoView2.VIEW_TYPE_SURFACEVIEW;
 
 import android.content.Context;
 import android.graphics.Rect;
@@ -30,7 +30,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 
-@RequiresApi(21)
+@RequiresApi(28)
 class VideoSurfaceView extends SurfaceView implements VideoViewInterface, SurfaceHolder.Callback {
     private static final String TAG = "VideoSurfaceView";
     private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
diff --git a/media/src/main/java/androidx/widget/VideoTextureView.java b/media-widget/src/main/java/androidx/media/widget/VideoTextureView.java
similarity index 97%
rename from media/src/main/java/androidx/widget/VideoTextureView.java
rename to media-widget/src/main/java/androidx/media/widget/VideoTextureView.java
index 836fdc3..f9f8df8 100644
--- a/media/src/main/java/androidx/widget/VideoTextureView.java
+++ b/media-widget/src/main/java/androidx/media/widget/VideoTextureView.java
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package androidx.widget;
+package androidx.media.widget;
 
-import static androidx.widget.VideoView2.VIEW_TYPE_TEXTUREVIEW;
+import static androidx.media.widget.VideoView2.VIEW_TYPE_TEXTUREVIEW;
 
 import android.content.Context;
 import android.graphics.SurfaceTexture;
@@ -30,7 +30,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 
-@RequiresApi(21)
+@RequiresApi(28)
 class VideoTextureView extends TextureView
         implements VideoViewInterface, TextureView.SurfaceTextureListener {
     private static final String TAG = "VideoTextureView";
diff --git a/media/src/main/java/androidx/widget/VideoView2.java b/media-widget/src/main/java/androidx/media/widget/VideoView2.java
similarity index 82%
rename from media/src/main/java/androidx/widget/VideoView2.java
rename to media-widget/src/main/java/androidx/media/widget/VideoView2.java
index 3cb1717..e246c09 100644
--- a/media/src/main/java/androidx/widget/VideoView2.java
+++ b/media-widget/src/main/java/androidx/media/widget/VideoView2.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.widget;
+package androidx.media.widget;
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
@@ -31,7 +31,9 @@
 import android.media.AudioManager;
 import android.media.MediaMetadataRetriever;
 import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnSubtitleDataListener;
 import android.media.PlaybackParams;
+import android.media.SubtitleData;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.ResultReceiver;
@@ -47,7 +49,6 @@
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.ViewGroup.LayoutParams;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityManager;
 import android.widget.ImageView;
@@ -63,8 +64,10 @@
 import androidx.media.DataSourceDesc;
 import androidx.media.MediaItem2;
 import androidx.media.MediaMetadata2;
-import androidx.media.R;
 import androidx.media.SessionToken2;
+import androidx.media.subtitle.ClosedCaptionRenderer;
+import androidx.media.subtitle.SubtitleController;
+import androidx.media.subtitle.SubtitleTrack;
 import androidx.palette.graphics.Palette;
 
 import java.io.IOException;
@@ -75,11 +78,9 @@
 import java.util.Map;
 import java.util.concurrent.Executor;
 
-// TODO: Replace MediaSession wtih MediaSession2 once MediaSession2 is submitted.
 /**
- * @hide
- * Displays a video file.  VideoView2 class is a View class which is wrapping {@link MediaPlayer}
- * so that developers can easily implement a video rendering application.
+ * Displays a video file.  VideoView2 class is a ViewGroup class which is wrapping
+ * {@link MediaPlayer} so that developers can easily implement a video rendering application.
  *
  * <p>
  * <em> Data sources that VideoView2 supports : </em>
@@ -100,20 +101,12 @@
  * VideoView2 covers and inherits the most of
  * VideoView's functionalities. The main differences are
  * <ul>
- * <li> VideoView2 inherits FrameLayout and renders videos using SurfaceView and TextureView
+ * <li> VideoView2 inherits ViewGroup and renders videos using SurfaceView and TextureView
  * selectively while VideoView inherits SurfaceView class.
  * <li> VideoView2 is integrated with MediaControlView2 and a default MediaControlView2 instance is
- * attached to VideoView2 by default. If a developer does not want to use the default
- * MediaControlView2, needs to set enableControlView attribute to false. For instance,
- * <pre>
- * &lt;VideoView2
- *     android:id="@+id/video_view"
- *     xmlns:widget="http://schemas.android.com/apk/com.android.media.update"
- *     widget:enableControlView="false" /&gt;
- * </pre>
- * If a developer wants to attach a customed MediaControlView2, then set enableControlView attribute
- * to false and assign the customed media control widget using {@link #setMediaControlView2}.
- * <li> VideoView2 is integrated with MediaPlayer while VideoView is integrated with MediaPlayer.
+ * attached to VideoView2 by default.
+ * <li> If a developer wants to attach a customed MediaControlView2,
+ * assign the customed media control widget using {@link #setMediaControlView2}.
  * <li> VideoView2 is integrated with MediaSession and so it responses with media key events.
  * A VideoView2 keeps a MediaSession instance internally and connects it to a corresponding
  * MediaControlView2 instance.
@@ -135,8 +128,7 @@
  * and restore these on their own in {@link android.app.Activity#onSaveInstanceState} and
  * {@link android.app.Activity#onRestoreInstanceState}.
  */
-@RequiresApi(21) // TODO correct minSdk API use incompatibilities and remove before release.
-@RestrictTo(LIBRARY_GROUP)
+@RequiresApi(28) // TODO correct minSdk API use incompatibilities and remove before release.
 public class VideoView2 extends BaseLayout implements VideoViewInterface.SurfaceListener {
     /** @hide */
     @RestrictTo(LIBRARY_GROUP)
@@ -162,7 +154,7 @@
     public static final int VIEW_TYPE_TEXTUREVIEW = 1;
 
     private static final String TAG = "VideoView2";
-    private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     private static final long DEFAULT_SHOW_CONTROLLER_INTERVAL_MS = 2000;
 
     private static final int STATE_ERROR = -1;
@@ -178,7 +170,6 @@
 
     private static final int SIZE_TYPE_EMBEDDED = 0;
     private static final int SIZE_TYPE_FULL = 1;
-    // TODO: add support for Minimal size type.
     private static final int SIZE_TYPE_MINIMAL = 2;
 
     private AccessibilityManager mAccessibilityManager;
@@ -187,9 +178,9 @@
     private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain
     private boolean mAudioFocused = false;
 
-    private Pair<Executor, VideoView2.OnCustomActionListener> mCustomActionListenerRecord;
-    private VideoView2.OnViewTypeChangedListener mViewTypeChangedListener;
-    private VideoView2.OnFullScreenRequestListener mFullScreenRequestListener;
+    private Pair<Executor, OnCustomActionListener> mCustomActionListenerRecord;
+    private OnViewTypeChangedListener mViewTypeChangedListener;
+    private OnFullScreenRequestListener mFullScreenRequestListener;
 
     private VideoViewInterface mCurrentView;
     private VideoTextureView mTextureView;
@@ -206,7 +197,6 @@
     private Bundle mMediaTypeData;
     private String mTitle;
 
-    // TODO: move music view inside SurfaceView/TextureView or implement VideoViewInterface.
     private WindowManager mManager;
     private Resources mResources;
     private View mMusicView;
@@ -232,102 +222,24 @@
 
     private ArrayList<Integer> mVideoTrackIndices;
     private ArrayList<Integer> mAudioTrackIndices;
-    // private ArrayList<Pair<Integer, SubtitleTrack>> mSubtitleTrackIndices;
-    // private SubtitleController mSubtitleController;
+    private ArrayList<Pair<Integer, SubtitleTrack>> mSubtitleTrackIndices;
+    private SubtitleController mSubtitleController;
 
     // selected video/audio/subtitle track index as MediaPlayer returns
     private int mSelectedVideoTrackIndex;
     private int mSelectedAudioTrackIndex;
     private int mSelectedSubtitleTrackIndex;
 
-    // private SubtitleView mSubtitleView;
+    private SubtitleView mSubtitleView;
     private boolean mSubtitleEnabled;
 
     private float mSpeed;
-    // TODO: Remove mFallbackSpeed when integration with MediaPlayer's new setPlaybackParams().
-    // Refer: https://docs.google.com/document/d/1nzAfns6i2hJ3RkaUre3QMT6wsDedJ5ONLiA_OOBFFX8/edit
     private float mFallbackSpeed;  // keep the original speed before 'pause' is called.
     private float mVolumeLevelFloat;
     private int mVolumeLevel;
 
     private long mShowControllerIntervalMs;
 
-    // private MediaRouter mMediaRouter;
-    // private MediaRouteSelector mRouteSelector;
-    // private MediaRouter.RouteInfo mRoute;
-    // private RoutePlayer mRoutePlayer;
-
-    // TODO (b/77158231)
-    /*
-    private final MediaRouter.Callback mRouterCallback = new MediaRouter.Callback() {
-        @Override
-        public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
-            if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
-                // Stop local playback (if necessary)
-                resetPlayer();
-                mRoute = route;
-                mRoutePlayer = new RoutePlayer(getContext(), route);
-                mRoutePlayer.setPlayerEventCallback(new RoutePlayer.PlayerEventCallback() {
-                    @Override
-                    public void onPlayerStateChanged(MediaItemStatus itemStatus) {
-                        PlaybackStateCompat.Builder psBuilder = new PlaybackStateCompat.Builder();
-                        psBuilder.setActions(RoutePlayer.PLAYBACK_ACTIONS);
-                        long position = itemStatus.getContentPosition();
-                        switch (itemStatus.getPlaybackState()) {
-                            case MediaItemStatus.PLAYBACK_STATE_PENDING:
-                                psBuilder.setState(PlaybackStateCompat.STATE_NONE, position, 0);
-                                mCurrentState = STATE_IDLE;
-                                break;
-                            case MediaItemStatus.PLAYBACK_STATE_PLAYING:
-                                psBuilder.setState(PlaybackStateCompat.STATE_PLAYING, position, 1);
-                                mCurrentState = STATE_PLAYING;
-                                break;
-                            case MediaItemStatus.PLAYBACK_STATE_PAUSED:
-                                psBuilder.setState(PlaybackStateCompat.STATE_PAUSED, position, 0);
-                                mCurrentState = STATE_PAUSED;
-                                break;
-                            case MediaItemStatus.PLAYBACK_STATE_BUFFERING:
-                                psBuilder.setState(
-                                        PlaybackStateCompat.STATE_BUFFERING, position, 0);
-                                mCurrentState = STATE_PAUSED;
-                                break;
-                            case MediaItemStatus.PLAYBACK_STATE_FINISHED:
-                                psBuilder.setState(PlaybackStateCompat.STATE_STOPPED, position, 0);
-                                mCurrentState = STATE_PLAYBACK_COMPLETED;
-                                break;
-                        }
-
-                        PlaybackStateCompat pbState = psBuilder.build();
-                        mMediaSession.setPlaybackState(pbState);
-
-                        MediaMetadataCompat.Builder mmBuilder = new MediaMetadataCompat.Builder();
-                        mmBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION,
-                                itemStatus.getContentDuration());
-                        mMediaSession.setMetadata(mmBuilder.build());
-                    }
-                });
-                // Start remote playback (if necessary)
-                mRoutePlayer.openVideo(mDsd);
-            }
-        }
-
-        @Override
-        public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route, int reason) {
-            if (mRoute != null && mRoutePlayer != null) {
-                mRoutePlayer.release();
-                mRoutePlayer = null;
-            }
-            if (mRoute == route) {
-                mRoute = null;
-            }
-            if (reason != MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
-                // TODO: Resume local playback  (if necessary)
-                openVideo(mDsd);
-            }
-        }
-    };
-    */
-
     public VideoView2(@NonNull Context context) {
         this(context, null);
     }
@@ -349,7 +261,6 @@
         mSpeed = 1.0f;
         mFallbackSpeed = mSpeed;
         mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
-        // TODO: add attributes to get this value.
         mShowControllerIntervalMs = DEFAULT_SHOW_CONTROLLER_INTERVAL_MS;
 
         mAccessibilityManager = (AccessibilityManager) context.getSystemService(
@@ -362,7 +273,6 @@
         setFocusableInTouchMode(true);
         requestFocus();
 
-        // TODO: try to keep a single child at a time rather than always having both.
         mTextureView = new VideoTextureView(getContext());
         mSurfaceView = new VideoSurfaceView(getContext());
         LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
@@ -375,10 +285,10 @@
         addView(mTextureView);
         addView(mSurfaceView);
 
-        // mSubtitleView = new SubtitleView(getContext());
-        // mSubtitleView.setLayoutParams(params);
-        // mSubtitleView.setBackgroundColor(0);
-        // addView(mSubtitleView);
+        mSubtitleView = new SubtitleView(getContext());
+        mSubtitleView.setLayoutParams(params);
+        mSubtitleView.setBackgroundColor(0);
+        addView(mSubtitleView);
 
         boolean enableControlView = (attrs == null) || attrs.getAttributeBooleanValue(
                 "http://schemas.android.com/apk/res/android",
@@ -391,7 +301,6 @@
                 "http://schemas.android.com/apk/res/android",
                 "enableSubtitle", false);
 
-        // TODO: Choose TextureView when SurfaceView cannot be created.
         // Choose surface view by default
         int viewType = (attrs == null) ? VideoView2.VIEW_TYPE_SURFACEVIEW
                 : attrs.getAttributeIntValue(
@@ -408,15 +317,6 @@
             mSurfaceView.setVisibility(View.GONE);
             mCurrentView = mTextureView;
         }
-
-        // TODO (b/77158231)
-        /*
-        MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
-        builder.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
-        builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
-        builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
-        mRouteSelector = builder.build();
-        */
     }
 
     /**
@@ -429,10 +329,6 @@
     public void setMediaControlView2(MediaControlView2 mediaControlView, long intervalMs) {
         mMediaControlView = mediaControlView;
         mShowControllerIntervalMs = intervalMs;
-        // TODO: Call MediaControlView2.setRouteSelector only when cast availalbe.
-        // TODO (b/77158231)
-        // mMediaControlView.setRouteSelector(mRouteSelector);
-
         if (isAttachedToWindow()) {
             attachMediaControlView();
         }
@@ -472,7 +368,7 @@
      * Returns MediaController instance which is connected with MediaSession that VideoView2 is
      * using. This method should be called when VideoView2 is attached to window, or it throws
      * IllegalStateException, since internal MediaSession instance is not available until
-     * this view is attached to window. Please check {@link android.view.View#isAttachedToWindow}
+     * this view is attached to window. Please check {@link View#isAttachedToWindow}
      * before calling this method.
      *
      * @throws IllegalStateException if interal MediaSession is not created yet.
@@ -487,13 +383,14 @@
     }
 
     /**
-     * Returns {@link androidx.media.SessionToken2} so that developers create their own
+     * Returns {@link SessionToken2} so that developers create their own
      * {@link androidx.media.MediaController2} instance. This method should be called when
      * VideoView2 is attached to window, or it throws IllegalStateException.
      *
      * @throws IllegalStateException if interal MediaSession is not created yet.
      * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     public SessionToken2 getMediaSessionToken() {
         //return mProvider.getMediaSessionToken_impl();
         return null;
@@ -530,7 +427,6 @@
      * be reset to the normal speed 1.0f.
      * @param speed the playback speed. It should be positive.
      */
-    // TODO: Support this via MediaController2.
     public void setSpeed(float speed) {
         if (speed <= 0.0f) {
             Log.e(TAG, "Unsupported speed (" + speed + ") is ignored.");
@@ -544,6 +440,16 @@
     }
 
     /**
+     * Returns playback speed.
+     *
+     * It returns the same value that has been set by {@link #setSpeed}, if it was available value.
+     * If {@link #setSpeed} has not been called before, then the normal speed 1.0f will be returned.
+     */
+    public float getSpeed() {
+        return mSpeed;
+    }
+
+    /**
      * Sets which type of audio focus will be requested during the playback, or configures playback
      * to not request audio focus. Valid values for focus requests are
      * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT},
@@ -586,7 +492,7 @@
      *
      * @param path the path of the video.
      *
-     * @hide TODO remove
+     * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
     public void setVideoPath(String path) {
@@ -598,7 +504,7 @@
      *
      * @param uri the URI of the video.
      *
-     * @hide TODO remove
+     * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
     public void setVideoUri(Uri uri) {
@@ -614,11 +520,8 @@
      *                changed with key/value pairs through the headers parameter with
      *                "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
      *                to disallow or allow cross domain redirection.
-     *
-     * @hide TODO remove
      */
-    @RestrictTo(LIBRARY_GROUP)
-    public void setVideoUri(Uri uri, Map<String, String> headers) {
+    public void setVideoUri(Uri uri, @Nullable Map<String, String> headers) {
         mSeekWhenPrepared = 0;
         openVideo(uri, headers);
     }
@@ -628,9 +531,11 @@
      * object to VideoView2 is {@link #setDataSource}.
      * @param mediaItem the MediaItem2 to play
      * @see #setDataSource
+     *
+     * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     public void setMediaItem(@NonNull MediaItem2 mediaItem) {
-        //mProvider.setMediaItem_impl(mediaItem);
     }
 
     /**
@@ -639,8 +544,8 @@
      * @see #setMediaItem
      * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     public void setDataSource(@NonNull DataSourceDesc dataSource) {
-        //mProvider.setDataSource_impl(dataSource);
     }
 
     /**
@@ -689,7 +594,7 @@
      *                   buttons in {@link MediaControlView2}.
      * @param executor executor to run callbacks on.
      * @param listener A listener to be called when a custom button is clicked.
-     * @hide  TODO remove
+     * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
     public void setCustomActions(List<PlaybackStateCompat.CustomAction> actionList,
@@ -717,7 +622,7 @@
     /**
      * Registers a callback to be invoked when the fullscreen mode should be changed.
      * @param l The callback that will be run
-     * @hide  TODO remove
+     * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
     public void setFullScreenRequestListener(OnFullScreenRequestListener l) {
@@ -733,12 +638,7 @@
         mMediaSession.setCallback(new MediaSessionCallback());
         mMediaSession.setActive(true);
         mMediaController = mMediaSession.getController();
-        // TODO (b/77158231)
-        // mMediaRouter = MediaRouter.getInstance(getContext());
-        // mMediaRouter.setMediaSession(mMediaSession);
-        // mMediaRouter.addCallback(mRouteSelector, mRouterCallback);
         attachMediaControlView();
-        // TODO: remove this after moving MediaSession creating code inside initializing VideoView2
         if (mCurrentState == STATE_PREPARED) {
             extractTracks();
             extractMetadata();
@@ -794,7 +694,6 @@
 
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
-        // TODO: Test touch event handling logic thoroughly and simplify the logic.
         return super.dispatchTouchEvent(ev);
     }
 
@@ -894,7 +793,11 @@
     // Implements VideoViewInterface.SurfaceListener
     ///////////////////////////////////////////////////
 
+    /**
+     * @hide
+     */
     @Override
+    @RestrictTo(LIBRARY_GROUP)
     public void onSurfaceCreated(View view, int width, int height) {
         if (DEBUG) {
             Log.d(TAG, "onSurfaceCreated(). mCurrentState=" + mCurrentState
@@ -906,7 +809,11 @@
         }
     }
 
+    /**
+     * @hide
+     */
     @Override
+    @RestrictTo(LIBRARY_GROUP)
     public void onSurfaceDestroyed(View view) {
         if (DEBUG) {
             Log.d(TAG, "onSurfaceDestroyed(). mCurrentState=" + mCurrentState
@@ -914,16 +821,23 @@
         }
     }
 
+    /**
+     * @hide
+     */
     @Override
+    @RestrictTo(LIBRARY_GROUP)
     public void onSurfaceChanged(View view, int width, int height) {
-        // TODO: Do we need to call requestLayout here?
         if (DEBUG) {
             Log.d(TAG, "onSurfaceChanged(). width/height: " + width + "/" + height
                     + ", " + view.toString());
         }
     }
 
+    /**
+     * @hide
+     */
     @Override
+    @RestrictTo(LIBRARY_GROUP)
     public void onSurfaceTakeOverDone(VideoViewInterface view) {
         if (DEBUG) {
             Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view);
@@ -951,8 +865,6 @@
     }
 
     private boolean isInPlaybackState() {
-        // TODO (b/77158231)
-        // return (mMediaPlayer != null || mRoutePlayer != null)
         return (mMediaPlayer != null)
                 && mCurrentState != STATE_ERROR
                 && mCurrentState != STATE_IDLE
@@ -960,8 +872,6 @@
     }
 
     private boolean needToStart() {
-        // TODO (b/77158231)
-        // return (mMediaPlayer != null || mRoutePlayer != null)
         return (mMediaPlayer != null)
                 && isAudioGranted()
                 && isWaitingPlayback();
@@ -989,11 +899,6 @@
                 case AudioManager.AUDIOFOCUS_LOSS:
                 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
-                    // There is no way to distinguish pause() by transient
-                    // audio focus loss and by other explicit actions.
-                    // TODO: If we can distinguish those cases, change the code to resume when it
-                    // gains audio focus again for AUDIOFOCUS_LOSS_TRANSIENT and
-                    // AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
                     mAudioFocused = false;
                     if (isInPlaybackState() && mMediaPlayer.isPlaying()) {
                         mMediaController.getTransportControls().pause();
@@ -1004,6 +909,7 @@
         }
     };
 
+    @SuppressWarnings("deprecation")
     private void requestAudioFocus(int focusType) {
         int result;
         if (android.os.Build.VERSION.SDK_INT >= 26) {
@@ -1031,8 +937,6 @@
     private void openVideo(Uri uri, Map<String, String> headers) {
         resetPlayer();
         if (isRemotePlayback()) {
-            // TODO (b/77158231)
-            // mRoutePlayer.openVideo(dsd);
             return;
         }
 
@@ -1044,10 +948,9 @@
             mCurrentView.assignSurfaceToMediaPlayer(mMediaPlayer);
 
             final Context context = getContext();
-            // TODO: Add timely firing logic for more accurate sync between CC and video frame
-            // mSubtitleController = new SubtitleController(context);
-            // mSubtitleController.registerRenderer(new ClosedCaptionRenderer(context));
-            // mSubtitleController.setAnchor((SubtitleController.Anchor) mSubtitleView);
+            mSubtitleController = new SubtitleController(context);
+            mSubtitleController.registerRenderer(new ClosedCaptionRenderer(context));
+            mSubtitleController.setAnchor((SubtitleController.Anchor) mSubtitleView);
 
             mMediaPlayer.setOnPreparedListener(mPreparedListener);
             mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
@@ -1060,7 +963,7 @@
             mCurrentBufferPercentage = -1;
             mMediaPlayer.setDataSource(getContext(), uri, headers);
             mMediaPlayer.setAudioAttributes(mAudioAttributes);
-            // mMediaPlayer.setOnSubtitleDataListener(mSubtitleListener);
+            mMediaPlayer.setOnSubtitleDataListener(mSubtitleListener);
             // we don't set the target state here either, but preserve the
             // target state that was there before.
             mCurrentState = STATE_PREPARING;
@@ -1091,6 +994,7 @@
     /*
      * Reset the media player in any state
      */
+    @SuppressWarnings("deprecation")
     private void resetPlayer() {
         if (mMediaPlayer != null) {
             mMediaPlayer.reset();
@@ -1110,38 +1014,6 @@
 
     private void updatePlaybackState() {
         if (mStateBuilder == null) {
-            /*
-            // Get the capabilities of the player for this stream
-            mMetadata = mMediaPlayer.getMetadata(MediaPlayer.METADATA_ALL,
-                    MediaPlayer.BYPASS_METADATA_FILTER);
-
-            // Add Play action as default
-            long playbackActions = PlaybackStateCompat.ACTION_PLAY;
-            if (mMetadata != null) {
-                if (!mMetadata.has(Metadata.PAUSE_AVAILABLE)
-                        || mMetadata.getBoolean(Metadata.PAUSE_AVAILABLE)) {
-                    playbackActions |= PlaybackStateCompat.ACTION_PAUSE;
-                }
-                if (!mMetadata.has(Metadata.SEEK_BACKWARD_AVAILABLE)
-                        || mMetadata.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE)) {
-                    playbackActions |= PlaybackStateCompat.ACTION_REWIND;
-                }
-                if (!mMetadata.has(Metadata.SEEK_FORWARD_AVAILABLE)
-                        || mMetadata.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE)) {
-                    playbackActions |= PlaybackStateCompat.ACTION_FAST_FORWARD;
-                }
-                if (!mMetadata.has(Metadata.SEEK_AVAILABLE)
-                        || mMetadata.getBoolean(Metadata.SEEK_AVAILABLE)) {
-                    playbackActions |= PlaybackStateCompat.ACTION_SEEK_TO;
-                }
-            } else {
-                playbackActions |= (PlaybackStateCompat.ACTION_PAUSE
-                        | PlaybackStateCompat.ACTION_REWIND
-                        | PlaybackStateCompat.ACTION_FAST_FORWARD
-                        | PlaybackStateCompat.ACTION_SEEK_TO);
-            }
-            */
-            // TODO determine the actionable list based the metadata info.
             long playbackActions = PlaybackStateCompat.ACTION_PLAY
                     | PlaybackStateCompat.ACTION_PAUSE
                     | PlaybackStateCompat.ACTION_REWIND | PlaybackStateCompat.ACTION_FAST_FORWARD
@@ -1160,8 +1032,6 @@
         if (mCurrentState != STATE_ERROR
                 && mCurrentState != STATE_IDLE
                 && mCurrentState != STATE_PREPARING) {
-            // TODO: this should be replaced with MediaPlayer2.getBufferedPosition() once it is
-            // implemented.
             if (mCurrentBufferPercentage == -1) {
                 mStateBuilder.setBufferedPosition(-1);
             } else {
@@ -1208,7 +1078,6 @@
     };
 
     private void showController() {
-        // TODO: Decide what to show when the state is not in playback state
         if (mMediaControlView == null || !isInPlaybackState()
                 || (mIsMusicMediaType && mSizeType == SIZE_TYPE_FULL)) {
             return;
@@ -1232,7 +1101,6 @@
 
     private void applySpeed() {
         if (android.os.Build.VERSION.SDK_INT < 23) {
-            // TODO: MediaPlayer2 will cover this, or implement with SoundPool.
             return;
         }
         PlaybackParams params = mMediaPlayer.getPlaybackParams().allowDefaults();
@@ -1243,11 +1111,6 @@
                 mFallbackSpeed = mSpeed;
             } catch (IllegalArgumentException e) {
                 Log.e(TAG, "PlaybackParams has unsupported value: " + e);
-                // TODO: should revise this part after integrating with MP2.
-                // If mSpeed had an illegal value for speed rate, system will determine best
-                // handling (see PlaybackParams.AUDIO_FALLBACK_MODE_DEFAULT).
-                // Note: The pre-MP2 returns 0.0f when it is paused. In this case, VideoView2 will
-                // use mFallbackSpeed instead.
                 float fallbackSpeed = mMediaPlayer.getPlaybackParams().allowDefaults().getSpeed();
                 if (fallbackSpeed > 0.0f) {
                     mFallbackSpeed = fallbackSpeed;
@@ -1270,10 +1133,8 @@
         if (!isInPlaybackState()) {
             return;
         }
-    /*
         if (select) {
             if (mSubtitleTrackIndices.size() > 0) {
-                // TODO: make this selection dynamic
                 mSelectedSubtitleTrackIndex = mSubtitleTrackIndices.get(0).first;
                 mSubtitleController.selectTrack(mSubtitleTrackIndices.get(0).second);
                 mMediaPlayer.selectTrack(mSelectedSubtitleTrackIndex);
@@ -1286,31 +1147,25 @@
                 mSubtitleView.setVisibility(View.GONE);
             }
         }
-    */
     }
 
     private void extractTracks() {
         MediaPlayer.TrackInfo[] trackInfos = mMediaPlayer.getTrackInfo();
         mVideoTrackIndices = new ArrayList<>();
         mAudioTrackIndices = new ArrayList<>();
-        /*
         mSubtitleTrackIndices = new ArrayList<>();
         mSubtitleController.reset();
-        */
         for (int i = 0; i < trackInfos.length; ++i) {
             int trackType = trackInfos[i].getTrackType();
             if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO) {
                 mVideoTrackIndices.add(i);
             } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) {
                 mAudioTrackIndices.add(i);
-                /*
-            } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE
-                    || trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT) {
+            } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
                 SubtitleTrack track = mSubtitleController.addTrack(trackInfos[i].getFormat());
                 if (track != null) {
                     mSubtitleTrackIndices.add(new Pair<>(i, track));
                 }
-                */
             }
         }
         // Select first tracks as default
@@ -1327,12 +1182,10 @@
         Bundle data = new Bundle();
         data.putInt(MediaControlView2.KEY_VIDEO_TRACK_COUNT, mVideoTrackIndices.size());
         data.putInt(MediaControlView2.KEY_AUDIO_TRACK_COUNT, mAudioTrackIndices.size());
-        /*
         data.putInt(MediaControlView2.KEY_SUBTITLE_TRACK_COUNT, mSubtitleTrackIndices.size());
         if (mSubtitleTrackIndices.size() > 0) {
             selectOrDeselectSubtitle(mSubtitleEnabled);
         }
-        */
         mMediaSession.sendSessionEvent(MediaControlView2.EVENT_UPDATE_TRACK_STATUS, data);
     }
 
@@ -1352,6 +1205,7 @@
         }
     }
 
+    @SuppressWarnings("deprecation")
     private void extractAudioMetadata() {
         if (!mIsMusicMediaType) {
             return;
@@ -1366,12 +1220,10 @@
             Bitmap bitmap = BitmapFactory.decodeByteArray(album, 0, album.length);
             mMusicAlbumDrawable = new BitmapDrawable(bitmap);
 
-            // TODO: replace with visualizer
             Palette.Builder builder = Palette.from(bitmap);
             builder.generate(new Palette.PaletteAsyncListener() {
                 @Override
                 public void onGenerated(Palette palette) {
-                    // TODO: add dominant color for default album image.
                     mDominantColor = palette.getDominantColor(0);
                     if (mMusicView != null) {
                         mMusicView.setBackgroundColor(mDominantColor);
@@ -1445,7 +1297,6 @@
         addView(mMusicView, 0);
     }
 
-    /*
     OnSubtitleDataListener mSubtitleListener =
             new OnSubtitleDataListener() {
                 @Override
@@ -1473,7 +1324,6 @@
                     }
                 }
             };
-            */
 
     MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener =
             new MediaPlayer.OnVideoSizeChangedListener() {
@@ -1505,8 +1355,6 @@
             // Create and set playback state for MediaControlView2
             updatePlaybackState();
 
-            // TODO: change this to send TrackInfos to MediaControlView2
-            // TODO: create MediaSession when initializing VideoView2
             if (mMediaSession != null) {
                 extractTracks();
             }
@@ -1550,20 +1398,12 @@
             // Get and set duration and title values as MediaMetadata for MediaControlView2
             MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
 
-            // TODO: Get title via other public APIs.
-            /*
-            if (mMetadata != null && mMetadata.has(Metadata.TITLE)) {
-                mTitle = mMetadata.getString(Metadata.TITLE);
-            }
-            */
             builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
             builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, mMediaPlayer.getDuration());
 
             if (mMediaSession != null) {
                 mMediaSession.setMetadata(builder.build());
 
-                // TODO: merge this code with the above code when integrating with
-                // MediaSession2.
                 if (mNeedUpdateMediaType) {
                     mMediaSession.sendSessionEvent(
                             MediaControlView2.EVENT_UPDATE_MEDIA_TYPE_STATUS, mMediaTypeData);
@@ -1583,6 +1423,7 @@
 
     MediaPlayer.OnCompletionListener mCompletionListener = new MediaPlayer.OnCompletionListener() {
         @Override
+        @SuppressWarnings("deprecation")
         public void onCompletion(MediaPlayer mp) {
             mCurrentState = STATE_PLAYBACK_COMPLETED;
             mTargetState = STATE_PLAYBACK_COMPLETED;
@@ -1633,12 +1474,9 @@
         @Override
         public void onCommand(String command, Bundle args, ResultReceiver receiver) {
             if (isRemotePlayback()) {
-                // TODO (b/77158231)
-                // mRoutePlayer.onCommand(command, args, receiver);
             } else {
                 switch (command) {
                     case MediaControlView2.COMMAND_SHOW_SUBTITLE:
-                        /*
                         int subtitleIndex = args.getInt(
                                 MediaControlView2.KEY_SELECTED_SUBTITLE_INDEX,
                                 INVALID_TRACK_INDEX);
@@ -1649,7 +1487,6 @@
                                 setSubtitleEnabled(true);
                             }
                         }
-                        */
                         break;
                     case MediaControlView2.COMMAND_HIDE_SUBTITLE:
                         setSubtitleEnabled(false);
@@ -1711,8 +1548,6 @@
 
             if ((isInPlaybackState() && mCurrentView.hasAvailableSurface()) || mIsMusicMediaType) {
                 if (isRemotePlayback()) {
-                    // TODO (b/77158231)
-                    // mRoutePlayer.onPlay();
                 } else {
                     applySpeed();
                     mMediaPlayer.start();
@@ -1733,8 +1568,6 @@
         public void onPause() {
             if (isInPlaybackState()) {
                 if (isRemotePlayback()) {
-                    // TODO (b/77158231)
-                    // mRoutePlayer.onPause();
                     mCurrentState = STATE_PAUSED;
                 } else if (mMediaPlayer.isPlaying()) {
                     mMediaPlayer.pause();
@@ -1754,10 +1587,7 @@
         public void onSeekTo(long pos) {
             if (isInPlaybackState()) {
                 if (isRemotePlayback()) {
-                    // TODO (b/77158231)
-                    // mRoutePlayer.onSeekTo(pos);
                 } else {
-                    // TODO Refactor VideoView2 with FooImplBase and FooImplApiXX.
                     if (android.os.Build.VERSION.SDK_INT < 26) {
                         mMediaPlayer.seekTo((int) pos);
                     } else {
@@ -1774,8 +1604,6 @@
         @Override
         public void onStop() {
             if (isRemotePlayback()) {
-                // TODO (b/77158231)
-                // mRoutePlayer.onStop();
             } else {
                 resetPlayer();
             }
diff --git a/media/src/main/java/androidx/widget/VideoViewInterface.java b/media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java
similarity index 98%
rename from media/src/main/java/androidx/widget/VideoViewInterface.java
rename to media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java
index eca8c82..81b40a9 100644
--- a/media/src/main/java/androidx/widget/VideoViewInterface.java
+++ b/media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.widget;
+package androidx.media.widget;
 
 import android.media.MediaPlayer;
 import android.view.View;
diff --git a/media/src/main/res/drawable/custom_progress.xml b/media-widget/src/main/res/drawable/custom_progress.xml
similarity index 100%
rename from media/src/main/res/drawable/custom_progress.xml
rename to media-widget/src/main/res/drawable/custom_progress.xml
diff --git a/media/src/main/res/drawable/custom_progress_thumb.xml b/media-widget/src/main/res/drawable/custom_progress_thumb.xml
similarity index 100%
rename from media/src/main/res/drawable/custom_progress_thumb.xml
rename to media-widget/src/main/res/drawable/custom_progress_thumb.xml
diff --git a/media/src/main/res/drawable/ic_arrow_back.xml b/media-widget/src/main/res/drawable/ic_arrow_back.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_arrow_back.xml
rename to media-widget/src/main/res/drawable/ic_arrow_back.xml
diff --git a/media/src/main/res/drawable/ic_aspect_ratio.xml b/media-widget/src/main/res/drawable/ic_aspect_ratio.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_aspect_ratio.xml
rename to media-widget/src/main/res/drawable/ic_aspect_ratio.xml
diff --git a/media/src/main/res/drawable/ic_audiotrack.xml b/media-widget/src/main/res/drawable/ic_audiotrack.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_audiotrack.xml
rename to media-widget/src/main/res/drawable/ic_audiotrack.xml
diff --git a/media/src/main/res/drawable/ic_check.xml b/media-widget/src/main/res/drawable/ic_check.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_check.xml
rename to media-widget/src/main/res/drawable/ic_check.xml
diff --git a/media/src/main/res/drawable/ic_chevron_left.xml b/media-widget/src/main/res/drawable/ic_chevron_left.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_chevron_left.xml
rename to media-widget/src/main/res/drawable/ic_chevron_left.xml
diff --git a/media/src/main/res/drawable/ic_chevron_right.xml b/media-widget/src/main/res/drawable/ic_chevron_right.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_chevron_right.xml
rename to media-widget/src/main/res/drawable/ic_chevron_right.xml
diff --git a/media/src/main/res/drawable/ic_default_album_image.xml b/media-widget/src/main/res/drawable/ic_default_album_image.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_default_album_image.xml
rename to media-widget/src/main/res/drawable/ic_default_album_image.xml
diff --git a/media/src/main/res/drawable/ic_forward_30.xml b/media-widget/src/main/res/drawable/ic_forward_30.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_forward_30.xml
rename to media-widget/src/main/res/drawable/ic_forward_30.xml
diff --git a/media/src/main/res/drawable/ic_fullscreen.xml b/media-widget/src/main/res/drawable/ic_fullscreen.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_fullscreen.xml
rename to media-widget/src/main/res/drawable/ic_fullscreen.xml
diff --git a/media/src/main/res/drawable/ic_fullscreen_exit.xml b/media-widget/src/main/res/drawable/ic_fullscreen_exit.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_fullscreen_exit.xml
rename to media-widget/src/main/res/drawable/ic_fullscreen_exit.xml
diff --git a/media/src/main/res/drawable/ic_help.xml b/media-widget/src/main/res/drawable/ic_help.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_help.xml
rename to media-widget/src/main/res/drawable/ic_help.xml
diff --git a/media/src/main/res/drawable/ic_high_quality.xml b/media-widget/src/main/res/drawable/ic_high_quality.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_high_quality.xml
rename to media-widget/src/main/res/drawable/ic_high_quality.xml
diff --git a/media/src/main/res/drawable/ic_launch.xml b/media-widget/src/main/res/drawable/ic_launch.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_launch.xml
rename to media-widget/src/main/res/drawable/ic_launch.xml
diff --git a/media/src/main/res/drawable/ic_mute.xml b/media-widget/src/main/res/drawable/ic_mute.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_mute.xml
rename to media-widget/src/main/res/drawable/ic_mute.xml
diff --git a/media/src/main/res/drawable/ic_pause_circle_filled.xml b/media-widget/src/main/res/drawable/ic_pause_circle_filled.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_pause_circle_filled.xml
rename to media-widget/src/main/res/drawable/ic_pause_circle_filled.xml
diff --git a/media/src/main/res/drawable/ic_play_circle_filled.xml b/media-widget/src/main/res/drawable/ic_play_circle_filled.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_play_circle_filled.xml
rename to media-widget/src/main/res/drawable/ic_play_circle_filled.xml
diff --git a/media/src/main/res/drawable/ic_replay_circle_filled.xml b/media-widget/src/main/res/drawable/ic_replay_circle_filled.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_replay_circle_filled.xml
rename to media-widget/src/main/res/drawable/ic_replay_circle_filled.xml
diff --git a/media/src/main/res/drawable/ic_rewind_10.xml b/media-widget/src/main/res/drawable/ic_rewind_10.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_rewind_10.xml
rename to media-widget/src/main/res/drawable/ic_rewind_10.xml
diff --git a/media/src/main/res/drawable/ic_sd.xml b/media-widget/src/main/res/drawable/ic_sd.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_sd.xml
rename to media-widget/src/main/res/drawable/ic_sd.xml
diff --git a/media/src/main/res/drawable/ic_settings.xml b/media-widget/src/main/res/drawable/ic_settings.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_settings.xml
rename to media-widget/src/main/res/drawable/ic_settings.xml
diff --git a/media/src/main/res/drawable/ic_skip_next.xml b/media-widget/src/main/res/drawable/ic_skip_next.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_skip_next.xml
rename to media-widget/src/main/res/drawable/ic_skip_next.xml
diff --git a/media/src/main/res/drawable/ic_skip_previous.xml b/media-widget/src/main/res/drawable/ic_skip_previous.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_skip_previous.xml
rename to media-widget/src/main/res/drawable/ic_skip_previous.xml
diff --git a/media/src/main/res/drawable/ic_subtitle_off.xml b/media-widget/src/main/res/drawable/ic_subtitle_off.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_subtitle_off.xml
rename to media-widget/src/main/res/drawable/ic_subtitle_off.xml
diff --git a/media/src/main/res/drawable/ic_subtitle_on.xml b/media-widget/src/main/res/drawable/ic_subtitle_on.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_subtitle_on.xml
rename to media-widget/src/main/res/drawable/ic_subtitle_on.xml
diff --git a/media/src/main/res/drawable/ic_unmute.xml b/media-widget/src/main/res/drawable/ic_unmute.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_unmute.xml
rename to media-widget/src/main/res/drawable/ic_unmute.xml
diff --git a/media/src/main/res/layout/embedded_music.xml b/media-widget/src/main/res/layout/embedded_music.xml
similarity index 100%
rename from media/src/main/res/layout/embedded_music.xml
rename to media-widget/src/main/res/layout/embedded_music.xml
diff --git a/media/src/main/res/layout/embedded_settings_list_item.xml b/media-widget/src/main/res/layout/embedded_settings_list_item.xml
similarity index 100%
rename from media/src/main/res/layout/embedded_settings_list_item.xml
rename to media-widget/src/main/res/layout/embedded_settings_list_item.xml
diff --git a/media/src/main/res/layout/embedded_sub_settings_list_item.xml b/media-widget/src/main/res/layout/embedded_sub_settings_list_item.xml
similarity index 100%
rename from media/src/main/res/layout/embedded_sub_settings_list_item.xml
rename to media-widget/src/main/res/layout/embedded_sub_settings_list_item.xml
diff --git a/media/src/main/res/layout/embedded_transport_controls.xml b/media-widget/src/main/res/layout/embedded_transport_controls.xml
similarity index 100%
rename from media/src/main/res/layout/embedded_transport_controls.xml
rename to media-widget/src/main/res/layout/embedded_transport_controls.xml
diff --git a/media/src/main/res/layout/full_landscape_music.xml b/media-widget/src/main/res/layout/full_landscape_music.xml
similarity index 100%
rename from media/src/main/res/layout/full_landscape_music.xml
rename to media-widget/src/main/res/layout/full_landscape_music.xml
diff --git a/media/src/main/res/layout/full_portrait_music.xml b/media-widget/src/main/res/layout/full_portrait_music.xml
similarity index 100%
rename from media/src/main/res/layout/full_portrait_music.xml
rename to media-widget/src/main/res/layout/full_portrait_music.xml
diff --git a/media/src/main/res/layout/full_settings_list_item.xml b/media-widget/src/main/res/layout/full_settings_list_item.xml
similarity index 100%
rename from media/src/main/res/layout/full_settings_list_item.xml
rename to media-widget/src/main/res/layout/full_settings_list_item.xml
diff --git a/media/src/main/res/layout/full_sub_settings_list_item.xml b/media-widget/src/main/res/layout/full_sub_settings_list_item.xml
similarity index 100%
rename from media/src/main/res/layout/full_sub_settings_list_item.xml
rename to media-widget/src/main/res/layout/full_sub_settings_list_item.xml
diff --git a/media/src/main/res/layout/full_transport_controls.xml b/media-widget/src/main/res/layout/full_transport_controls.xml
similarity index 100%
rename from media/src/main/res/layout/full_transport_controls.xml
rename to media-widget/src/main/res/layout/full_transport_controls.xml
diff --git a/media/src/main/res/layout/media_controller.xml b/media-widget/src/main/res/layout/media_controller.xml
similarity index 100%
rename from media/src/main/res/layout/media_controller.xml
rename to media-widget/src/main/res/layout/media_controller.xml
diff --git a/media/src/main/res/layout/minimal_transport_controls.xml b/media-widget/src/main/res/layout/minimal_transport_controls.xml
similarity index 100%
rename from media/src/main/res/layout/minimal_transport_controls.xml
rename to media-widget/src/main/res/layout/minimal_transport_controls.xml
diff --git a/media/src/main/res/layout/settings_list.xml b/media-widget/src/main/res/layout/settings_list.xml
similarity index 100%
rename from media/src/main/res/layout/settings_list.xml
rename to media-widget/src/main/res/layout/settings_list.xml
diff --git a/media/src/main/res/layout/title_bar_gradient.xml b/media-widget/src/main/res/layout/title_bar_gradient.xml
similarity index 100%
rename from media/src/main/res/layout/title_bar_gradient.xml
rename to media-widget/src/main/res/layout/title_bar_gradient.xml
diff --git a/media/src/main/res/values/arrays.xml b/media-widget/src/main/res/values/arrays.xml
similarity index 100%
rename from media/src/main/res/values/arrays.xml
rename to media-widget/src/main/res/values/arrays.xml
diff --git a/media-widget/src/main/res/values/colors.xml b/media-widget/src/main/res/values/colors.xml
new file mode 100644
index 0000000..3aff9da
--- /dev/null
+++ b/media-widget/src/main/res/values/colors.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright 2018 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+<resources>
+
+    <!-- The color of the material notification background for media notifications when no custom
+     color is specified -->
+    <color name="notification_material_background_media_default_color">#ff424242</color>
+
+    <color name="gray">#808080</color>
+    <color name="white">#ffffff</color>
+    <color name="white_opacity_70">#B3ffffff</color>
+    <color name="black_opacity_70">#B3000000</color>
+    <color name="title_bar_gradient_start">#50000000</color>
+    <color name="title_bar_gradient_end">#00000000</color>
+    <color name="bottom_bar_background">#40202020</color>
+</resources>
\ No newline at end of file
diff --git a/media-widget/src/main/res/values/dimens.xml b/media-widget/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..796f345
--- /dev/null
+++ b/media-widget/src/main/res/values/dimens.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright 2018 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<resources>
+    <dimen name="mcv2_embedded_settings_width">150dp</dimen>
+    <dimen name="mcv2_embedded_settings_height">36dp</dimen>
+    <dimen name="mcv2_embedded_settings_icon_size">20dp</dimen>
+    <dimen name="mcv2_embedded_settings_text_height">18dp</dimen>
+    <dimen name="mcv2_embedded_settings_main_text_size">12sp</dimen>
+    <dimen name="mcv2_embedded_settings_sub_text_size">10sp</dimen>
+    <dimen name="mcv2_full_settings_width">225dp</dimen>
+    <dimen name="mcv2_full_settings_height">54dp</dimen>
+    <dimen name="mcv2_full_settings_icon_size">30dp</dimen>
+    <dimen name="mcv2_full_settings_text_height">27dp</dimen>
+    <dimen name="mcv2_full_settings_main_text_size">16sp</dimen>
+    <dimen name="mcv2_full_settings_sub_text_size">13sp</dimen>
+    <dimen name="mcv2_settings_offset">8dp</dimen>
+
+    <dimen name="mcv2_transport_controls_padding">4dp</dimen>
+    <dimen name="mcv2_pause_icon_size">36dp</dimen>
+    <dimen name="mcv2_full_icon_size">28dp</dimen>
+    <dimen name="mcv2_embedded_icon_size">24dp</dimen>
+    <dimen name="mcv2_minimal_icon_size">24dp</dimen>
+    <dimen name="mcv2_icon_margin">10dp</dimen>
+
+    <dimen name="mcv2_full_album_image_portrait_size">232dp</dimen>
+    <dimen name="mcv2_full_album_image_landscape_size">176dp</dimen>
+
+    <dimen name="mcv2_custom_progress_max_size">2dp</dimen>
+    <dimen name="mcv2_custom_progress_thumb_size">12dp</dimen>
+    <dimen name="mcv2_buffer_view_height">5dp</dimen>
+</resources>
diff --git a/media/src/main/res/values/strings.xml b/media-widget/src/main/res/values/strings.xml
similarity index 92%
rename from media/src/main/res/values/strings.xml
rename to media-widget/src/main/res/values/strings.xml
index 4f46ccf..10889df 100644
--- a/media/src/main/res/values/strings.xml
+++ b/media-widget/src/main/res/values/strings.xml
@@ -1,22 +1,22 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright 2018 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
+<!--
+  ~ Copyright 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-
     <!-- Name for the default system route prior to Jellybean. [CHAR LIMIT=30] -->
     <string name="mr_system_route_name">System</string>
 
@@ -48,7 +48,7 @@
     <!-- Button to disconnect from a media route.  [CHAR LIMIT=30] -->
     <string name="mr_controller_disconnect">Disconnect</string>
 
-    <!-- Button to stop playback and disconnect from a media route. [CHAR LIMIT=30] -->
+    <!-- Button to stop playback and disconnect from a media route. [CHAR LIMIT=32] -->
     <string name="mr_controller_stop_casting">Stop casting</string>
 
     <!-- Content description for accessibility (not shown on the screen): dialog close button. [CHAR LIMIT=NONE] -->
diff --git a/media/src/main/res/values/style.xml b/media-widget/src/main/res/values/styles.xml
similarity index 100%
rename from media/src/main/res/values/style.xml
rename to media-widget/src/main/res/values/styles.xml
diff --git a/media/build.gradle b/media/build.gradle
index e55188a..af0928b 100644
--- a/media/build.gradle
+++ b/media/build.gradle
@@ -9,7 +9,6 @@
 dependencies {
     api(project(":annotation"))
     api(project(":core"))
-    api(project(":palette"))
 
     androidTestImplementation(TEST_RUNNER_TMP, libs.exclude_for_espresso)
     androidTestImplementation(ESPRESSO_CORE_TMP, libs.exclude_for_espresso)
diff --git a/media/src/androidTest/AndroidManifest.xml b/media/src/androidTest/AndroidManifest.xml
index 4fd25ca..32987a5 100644
--- a/media/src/androidTest/AndroidManifest.xml
+++ b/media/src/androidTest/AndroidManifest.xml
@@ -19,14 +19,6 @@
     <uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
 
     <application>
-        <activity android:name="androidx.widget.VideoView2TestActivity"
-            android:configChanges="keyboardHidden|orientation|screenSize"
-            android:label="VideoVeiw2TestActivity">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
-            </intent-filter>
-        </activity>
         <activity android:name="androidx.media.MediaStubActivity"
                   android:label="MediaStubActivity"
                   android:screenOrientation="nosensor"
diff --git a/media/src/androidTest/res/layout/videoview2_layout.xml b/media/src/androidTest/res/layout/videoview2_layout.xml
deleted file mode 100644
index e3783c0..0000000
--- a/media/src/androidTest/res/layout/videoview2_layout.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:orientation="vertical"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <androidx.widget.VideoView2
-        android:id="@+id/videoview"
-        android:layout_width="160dp"
-        android:layout_height="120dp"/>
-</LinearLayout>
diff --git a/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java b/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
index 430913e..8f24837 100644
--- a/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
+++ b/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
@@ -1020,8 +1020,11 @@
      * Note that we cannot simply override {@link Service#attachBaseContext(Context)} and hide it
      * because lint checks considers the overriden method as the new public API that needs update
      * of current.txt.
+     *
+     * @hide
      */
-    void attachToBaseContext(Context base) {
+    @RestrictTo(LIBRARY)
+    public void attachToBaseContext(Context base) {
         attachBaseContext(base);
     }
 
diff --git a/media/src/main/java/androidx/media/MediaPlayer2.java b/media/src/main/java/androidx/media/MediaPlayer2.java
index 3802b9f..f1cd3b6 100644
--- a/media/src/main/java/androidx/media/MediaPlayer2.java
+++ b/media/src/main/java/androidx/media/MediaPlayer2.java
@@ -1524,16 +1524,6 @@
      */
     public static final int CALL_COMPLETED_PREPARE = 6;
 
-    /** The player just completed a call {@link #releaseDrm}.
-     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
-     */
-    public static final int CALL_COMPLETED_RELEASE_DRM = 12;
-
-    /** The player just completed a call {@link #restoreDrmKeys}.
-     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
-     */
-    public static final int CALL_COMPLETED_RESTORE_DRM_KEYS = 13;
-
     /** The player just completed a call {@link #seekTo}.
      * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
      */
@@ -1621,8 +1611,6 @@
             CALL_COMPLETED_PAUSE,
             CALL_COMPLETED_PLAY,
             CALL_COMPLETED_PREPARE,
-            CALL_COMPLETED_RELEASE_DRM,
-            CALL_COMPLETED_RESTORE_DRM_KEYS,
             CALL_COMPLETED_SEEK_TO,
             CALL_COMPLETED_SELECT_TRACK,
             CALL_COMPLETED_SET_AUDIO_ATTRIBUTES,
@@ -1673,12 +1661,6 @@
      */
     public static final int CALL_STATUS_ERROR_IO = 4;
 
-    /** Status code represents that DRM operation is called before preparing a DRM scheme through
-     *  {@link #prepareDrm}.
-     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
-     */
-    public static final int CALL_STATUS_NO_DRM_SCHEME = 5;
-
     /**
      * @hide
      */
@@ -1688,8 +1670,7 @@
             CALL_STATUS_INVALID_OPERATION,
             CALL_STATUS_BAD_VALUE,
             CALL_STATUS_PERMISSION_DENIED,
-            CALL_STATUS_ERROR_IO,
-            CALL_STATUS_NO_DRM_SCHEME})
+            CALL_STATUS_ERROR_IO})
     @Retention(RetentionPolicy.SOURCE)
     @RestrictTo(LIBRARY_GROUP)
     public @interface CallStatus {}
diff --git a/media/src/main/java/androidx/media/MediaPlayer2Impl.java b/media/src/main/java/androidx/media/MediaPlayer2Impl.java
index 50c490f..6127473 100644
--- a/media/src/main/java/androidx/media/MediaPlayer2Impl.java
+++ b/media/src/main/java/androidx/media/MediaPlayer2Impl.java
@@ -1367,16 +1367,11 @@
      */
     @Override
     public void releaseDrm() throws NoDrmSchemeException {
-        addTask(new Task(CALL_COMPLETED_RELEASE_DRM, false) {
-            @Override
-            void process() throws NoDrmSchemeException {
-                try {
-                    mPlayer.releaseDrm();
-                } catch (MediaPlayer.NoDrmSchemeException e) {
-                    throw new NoDrmSchemeException(e.getMessage());
-                }
-            }
-        });
+        try {
+            mPlayer.releaseDrm();
+        } catch (MediaPlayer.NoDrmSchemeException e) {
+            throw new NoDrmSchemeException(e.getMessage());
+        }
     }
 
 
@@ -1470,16 +1465,11 @@
     @Override
     public void restoreDrmKeys(@NonNull final byte[] keySetId)
             throws NoDrmSchemeException {
-        addTask(new Task(CALL_COMPLETED_RESTORE_DRM_KEYS, false) {
-            @Override
-            void process() throws NoDrmSchemeException {
-                try {
-                    mPlayer.restoreKeys(keySetId);
-                } catch (MediaPlayer.NoDrmSchemeException e) {
-                    throw new NoDrmSchemeException(e.getMessage());
-                }
-            }
-        });
+        try {
+            mPlayer.restoreKeys(keySetId);
+        } catch (MediaPlayer.NoDrmSchemeException e) {
+            throw new NoDrmSchemeException(e.getMessage());
+        }
     }
 
 
@@ -2040,8 +2030,6 @@
                 status = CALL_STATUS_PERMISSION_DENIED;
             } catch (IOException e) {
                 status = CALL_STATUS_ERROR_IO;
-            } catch (NoDrmSchemeException e) {
-                status = CALL_STATUS_NO_DRM_SCHEME;
             } catch (Exception e) {
                 status = CALL_STATUS_ERROR_UNKNOWN;
             }
diff --git a/media/src/main/java/androidx/media/subtitle/Cea608CCParser.java b/media/src/main/java/androidx/media/subtitle/Cea608CCParser.java
new file mode 100644
index 0000000..9205fba
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/Cea608CCParser.java
@@ -0,0 +1,987 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.subtitle;
+
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.TextPaint;
+import android.text.style.CharacterStyle;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+import android.text.style.UpdateAppearance;
+import android.util.Log;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * CCParser processes CEA-608 closed caption data.
+ *
+ * It calls back into OnDisplayChangedListener upon
+ * display change with styled text for rendering.
+ *
+ */
+class Cea608CCParser {
+    public static final int MAX_ROWS = 15;
+    public static final int MAX_COLS = 32;
+
+    private static final String TAG = "Cea608CCParser";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final int INVALID = -1;
+
+    // EIA-CEA-608: Table 70 - Control Codes
+    private static final int RCL = 0x20;
+    private static final int BS  = 0x21;
+    private static final int AOF = 0x22;
+    private static final int AON = 0x23;
+    private static final int DER = 0x24;
+    private static final int RU2 = 0x25;
+    private static final int RU3 = 0x26;
+    private static final int RU4 = 0x27;
+    private static final int FON = 0x28;
+    private static final int RDC = 0x29;
+    private static final int TR  = 0x2a;
+    private static final int RTD = 0x2b;
+    private static final int EDM = 0x2c;
+    private static final int CR  = 0x2d;
+    private static final int ENM = 0x2e;
+    private static final int EOC = 0x2f;
+
+    // Transparent Space
+    private static final char TS = '\u00A0';
+
+    // Captioning Modes
+    private static final int MODE_UNKNOWN = 0;
+    private static final int MODE_PAINT_ON = 1;
+    private static final int MODE_ROLL_UP = 2;
+    private static final int MODE_POP_ON = 3;
+    private static final int MODE_TEXT = 4;
+
+    private final DisplayListener mListener;
+
+    private int mMode = MODE_PAINT_ON;
+    private int mRollUpSize = 4;
+    private int mPrevCtrlCode = INVALID;
+
+    private CCMemory mDisplay = new CCMemory();
+    private CCMemory mNonDisplay = new CCMemory();
+    private CCMemory mTextMem = new CCMemory();
+
+    Cea608CCParser(DisplayListener listener) {
+        mListener = listener;
+    }
+
+    public void parse(byte[] data) {
+        CCData[] ccData = CCData.fromByteArray(data);
+
+        for (int i = 0; i < ccData.length; i++) {
+            if (DEBUG) {
+                Log.d(TAG, ccData[i].toString());
+            }
+
+            if (handleCtrlCode(ccData[i])
+                    || handleTabOffsets(ccData[i])
+                    || handlePACCode(ccData[i])
+                    || handleMidRowCode(ccData[i])) {
+                continue;
+            }
+
+            handleDisplayableChars(ccData[i]);
+        }
+    }
+
+    interface DisplayListener {
+        void onDisplayChanged(SpannableStringBuilder[] styledTexts);
+        CaptionStyle getCaptionStyle();
+    }
+
+    private CCMemory getMemory() {
+        // get the CC memory to operate on for current mode
+        switch (mMode) {
+            case MODE_POP_ON:
+                return mNonDisplay;
+            case MODE_TEXT:
+                // TODO(chz): support only caption mode for now,
+                // in text mode, dump everything to text mem.
+                return mTextMem;
+            case MODE_PAINT_ON:
+            case MODE_ROLL_UP:
+                return mDisplay;
+            default:
+                Log.w(TAG, "unrecoginized mode: " + mMode);
+        }
+        return mDisplay;
+    }
+
+    private boolean handleDisplayableChars(CCData ccData) {
+        if (!ccData.isDisplayableChar()) {
+            return false;
+        }
+
+        // Extended char includes 1 automatic backspace
+        if (ccData.isExtendedChar()) {
+            getMemory().bs();
+        }
+
+        getMemory().writeText(ccData.getDisplayText());
+
+        if (mMode == MODE_PAINT_ON || mMode == MODE_ROLL_UP) {
+            updateDisplay();
+        }
+
+        return true;
+    }
+
+    private boolean handleMidRowCode(CCData ccData) {
+        StyleCode m = ccData.getMidRow();
+        if (m != null) {
+            getMemory().writeMidRowCode(m);
+            return true;
+        }
+        return false;
+    }
+
+    private boolean handlePACCode(CCData ccData) {
+        PAC pac = ccData.getPAC();
+
+        if (pac != null) {
+            if (mMode == MODE_ROLL_UP) {
+                getMemory().moveBaselineTo(pac.getRow(), mRollUpSize);
+            }
+            getMemory().writePAC(pac);
+            return true;
+        }
+
+        return false;
+    }
+
+    private boolean handleTabOffsets(CCData ccData) {
+        int tabs = ccData.getTabOffset();
+
+        if (tabs > 0) {
+            getMemory().tab(tabs);
+            return true;
+        }
+
+        return false;
+    }
+
+    private boolean handleCtrlCode(CCData ccData) {
+        int ctrlCode = ccData.getCtrlCode();
+
+        if (mPrevCtrlCode != INVALID && mPrevCtrlCode == ctrlCode) {
+            // discard double ctrl codes (but if there's a 3rd one, we still take that)
+            mPrevCtrlCode = INVALID;
+            return true;
+        }
+
+        switch(ctrlCode) {
+            case RCL:
+                // select pop-on style
+                mMode = MODE_POP_ON;
+                break;
+            case BS:
+                getMemory().bs();
+                break;
+            case DER:
+                getMemory().der();
+                break;
+            case RU2:
+            case RU3:
+            case RU4:
+                mRollUpSize = (ctrlCode - 0x23);
+                // erase memory if currently in other style
+                if (mMode != MODE_ROLL_UP) {
+                    mDisplay.erase();
+                    mNonDisplay.erase();
+                }
+                // select roll-up style
+                mMode = MODE_ROLL_UP;
+                break;
+            case FON:
+                Log.i(TAG, "Flash On");
+                break;
+            case RDC:
+                // select paint-on style
+                mMode = MODE_PAINT_ON;
+                break;
+            case TR:
+                mMode = MODE_TEXT;
+                mTextMem.erase();
+                break;
+            case RTD:
+                mMode = MODE_TEXT;
+                break;
+            case EDM:
+                // erase display memory
+                mDisplay.erase();
+                updateDisplay();
+                break;
+            case CR:
+                if (mMode == MODE_ROLL_UP) {
+                    getMemory().rollUp(mRollUpSize);
+                } else {
+                    getMemory().cr();
+                }
+                if (mMode == MODE_ROLL_UP) {
+                    updateDisplay();
+                }
+                break;
+            case ENM:
+                // erase non-display memory
+                mNonDisplay.erase();
+                break;
+            case EOC:
+                // swap display/non-display memory
+                swapMemory();
+                // switch to pop-on style
+                mMode = MODE_POP_ON;
+                updateDisplay();
+                break;
+            case INVALID:
+            default:
+                mPrevCtrlCode = INVALID;
+                return false;
+        }
+
+        mPrevCtrlCode = ctrlCode;
+
+        // handled
+        return true;
+    }
+
+    private void updateDisplay() {
+        if (mListener != null) {
+            CaptionStyle captionStyle = mListener.getCaptionStyle();
+            mListener.onDisplayChanged(mDisplay.getStyledText(captionStyle));
+        }
+    }
+
+    private void swapMemory() {
+        CCMemory temp = mDisplay;
+        mDisplay = mNonDisplay;
+        mNonDisplay = temp;
+    }
+
+    private static class StyleCode {
+        static final int COLOR_WHITE = 0;
+        static final int COLOR_GREEN = 1;
+        static final int COLOR_BLUE = 2;
+        static final int COLOR_CYAN = 3;
+        static final int COLOR_RED = 4;
+        static final int COLOR_YELLOW = 5;
+        static final int COLOR_MAGENTA = 6;
+        static final int COLOR_INVALID = 7;
+
+        static final int STYLE_ITALICS   = 0x00000001;
+        static final int STYLE_UNDERLINE = 0x00000002;
+
+        static final String[] sColorMap = {
+            "WHITE", "GREEN", "BLUE", "CYAN", "RED", "YELLOW", "MAGENTA", "INVALID"
+        };
+
+        final int mStyle;
+        final int mColor;
+
+        static StyleCode fromByte(byte data2) {
+            int style = 0;
+            int color = (data2 >> 1) & 0x7;
+
+            if ((data2 & 0x1) != 0) {
+                style |= STYLE_UNDERLINE;
+            }
+
+            if (color == COLOR_INVALID) {
+                // WHITE ITALICS
+                color = COLOR_WHITE;
+                style |= STYLE_ITALICS;
+            }
+
+            return new StyleCode(style, color);
+        }
+
+        StyleCode(int style, int color) {
+            mStyle = style;
+            mColor = color;
+        }
+
+        boolean isItalics() {
+            return (mStyle & STYLE_ITALICS) != 0;
+        }
+
+        boolean isUnderline() {
+            return (mStyle & STYLE_UNDERLINE) != 0;
+        }
+
+        int getColor() {
+            return mColor;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder str = new StringBuilder();
+            str.append("{");
+            str.append(sColorMap[mColor]);
+            if ((mStyle & STYLE_ITALICS) != 0) {
+                str.append(", ITALICS");
+            }
+            if ((mStyle & STYLE_UNDERLINE) != 0) {
+                str.append(", UNDERLINE");
+            }
+            str.append("}");
+
+            return str.toString();
+        }
+    }
+
+    private static class PAC extends StyleCode {
+        final int mRow;
+        final int mCol;
+
+        static PAC fromBytes(byte data1, byte data2) {
+            int[] rowTable = {11, 1, 3, 12, 14, 5, 7, 9};
+            int row = rowTable[data1 & 0x07] + ((data2 & 0x20) >> 5);
+            int style = 0;
+            if ((data2 & 1) != 0) {
+                style |= STYLE_UNDERLINE;
+            }
+            if ((data2 & 0x10) != 0) {
+                // indent code
+                int indent = (data2 >> 1) & 0x7;
+                return new PAC(row, indent * 4, style, COLOR_WHITE);
+            } else {
+                // style code
+                int color = (data2 >> 1) & 0x7;
+
+                if (color == COLOR_INVALID) {
+                    // WHITE ITALICS
+                    color = COLOR_WHITE;
+                    style |= STYLE_ITALICS;
+                }
+                return new PAC(row, -1, style, color);
+            }
+        }
+
+        PAC(int row, int col, int style, int color) {
+            super(style, color);
+            mRow = row;
+            mCol = col;
+        }
+
+        boolean isIndentPAC() {
+            return (mCol >= 0);
+        }
+
+        int getRow() {
+            return mRow;
+        }
+
+        int getCol() {
+            return mCol;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("{%d, %d}, %s",
+                    mRow, mCol, super.toString());
+        }
+    }
+
+    /**
+     * Mutable version of BackgroundSpan to facilitate text rendering with edge styles.
+     */
+    public static class MutableBackgroundColorSpan extends CharacterStyle
+            implements UpdateAppearance {
+        private int mColor;
+
+        MutableBackgroundColorSpan(int color) {
+            mColor = color;
+        }
+
+        public void setBackgroundColor(int color) {
+            mColor = color;
+        }
+
+        public int getBackgroundColor() {
+            return mColor;
+        }
+
+        @Override
+        public void updateDrawState(TextPaint ds) {
+            ds.bgColor = mColor;
+        }
+    }
+
+    /* CCLineBuilder keeps track of displayable chars, as well as
+     * MidRow styles and PACs, for a single line of CC memory.
+     *
+     * It generates styled text via getStyledText() method.
+     */
+    private static class CCLineBuilder {
+        private final StringBuilder mDisplayChars;
+        private final StyleCode[] mMidRowStyles;
+        private final StyleCode[] mPACStyles;
+
+        CCLineBuilder(String str) {
+            mDisplayChars = new StringBuilder(str);
+            mMidRowStyles = new StyleCode[mDisplayChars.length()];
+            mPACStyles = new StyleCode[mDisplayChars.length()];
+        }
+
+        void setCharAt(int index, char ch) {
+            mDisplayChars.setCharAt(index, ch);
+            mMidRowStyles[index] = null;
+        }
+
+        void setMidRowAt(int index, StyleCode m) {
+            mDisplayChars.setCharAt(index, ' ');
+            mMidRowStyles[index] = m;
+        }
+
+        void setPACAt(int index, PAC pac) {
+            mPACStyles[index] = pac;
+        }
+
+        char charAt(int index) {
+            return mDisplayChars.charAt(index);
+        }
+
+        int length() {
+            return mDisplayChars.length();
+        }
+
+        void applyStyleSpan(
+                SpannableStringBuilder styledText,
+                StyleCode s, int start, int end) {
+            if (s.isItalics()) {
+                styledText.setSpan(
+                        new StyleSpan(android.graphics.Typeface.ITALIC),
+                        start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+            }
+            if (s.isUnderline()) {
+                styledText.setSpan(
+                        new UnderlineSpan(),
+                        start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+            }
+        }
+
+        SpannableStringBuilder getStyledText(CaptionStyle captionStyle) {
+            SpannableStringBuilder styledText = new SpannableStringBuilder(mDisplayChars);
+            int start = -1, next = 0;
+            int styleStart = -1;
+            StyleCode curStyle = null;
+            while (next < mDisplayChars.length()) {
+                StyleCode newStyle = null;
+                if (mMidRowStyles[next] != null) {
+                    // apply mid-row style change
+                    newStyle = mMidRowStyles[next];
+                } else if (mPACStyles[next] != null && (styleStart < 0 || start < 0)) {
+                    // apply PAC style change, only if:
+                    // 1. no style set, or
+                    // 2. style set, but prev char is none-displayable
+                    newStyle = mPACStyles[next];
+                }
+                if (newStyle != null) {
+                    curStyle = newStyle;
+                    if (styleStart >= 0 && start >= 0) {
+                        applyStyleSpan(styledText, newStyle, styleStart, next);
+                    }
+                    styleStart = next;
+                }
+
+                if (mDisplayChars.charAt(next) != TS) {
+                    if (start < 0) {
+                        start = next;
+                    }
+                } else if (start >= 0) {
+                    int expandedStart = mDisplayChars.charAt(start) == ' ' ? start : start - 1;
+                    int expandedEnd = mDisplayChars.charAt(next - 1) == ' ' ? next : next + 1;
+                    styledText.setSpan(
+                            new MutableBackgroundColorSpan(captionStyle.backgroundColor),
+                            expandedStart, expandedEnd,
+                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                    if (styleStart >= 0) {
+                        applyStyleSpan(styledText, curStyle, styleStart, expandedEnd);
+                    }
+                    start = -1;
+                }
+                next++;
+            }
+
+            return styledText;
+        }
+    }
+
+    /*
+     * CCMemory models a console-style display.
+     */
+    private static class CCMemory {
+        private final String mBlankLine;
+        private final CCLineBuilder[] mLines = new CCLineBuilder[MAX_ROWS + 2];
+        private int mRow;
+        private int mCol;
+
+        CCMemory() {
+            char[] blank = new char[MAX_COLS + 2];
+            Arrays.fill(blank, TS);
+            mBlankLine = new String(blank);
+        }
+
+        void erase() {
+            // erase all lines
+            for (int i = 0; i < mLines.length; i++) {
+                mLines[i] = null;
+            }
+            mRow = MAX_ROWS;
+            mCol = 1;
+        }
+
+        void der() {
+            if (mLines[mRow] != null) {
+                for (int i = 0; i < mCol; i++) {
+                    if (mLines[mRow].charAt(i) != TS) {
+                        for (int j = mCol; j < mLines[mRow].length(); j++) {
+                            mLines[j].setCharAt(j, TS);
+                        }
+                        return;
+                    }
+                }
+                mLines[mRow] = null;
+            }
+        }
+
+        void tab(int tabs) {
+            moveCursorByCol(tabs);
+        }
+
+        void bs() {
+            moveCursorByCol(-1);
+            if (mLines[mRow] != null) {
+                mLines[mRow].setCharAt(mCol, TS);
+                if (mCol == MAX_COLS - 1) {
+                    // Spec recommendation:
+                    // if cursor was at col 32, move cursor
+                    // back to col 31 and erase both col 31&32
+                    mLines[mRow].setCharAt(MAX_COLS, TS);
+                }
+            }
+        }
+
+        void cr() {
+            moveCursorTo(mRow + 1, 1);
+        }
+
+        void rollUp(int windowSize) {
+            int i;
+            for (i = 0; i <= mRow - windowSize; i++) {
+                mLines[i] = null;
+            }
+            int startRow = mRow - windowSize + 1;
+            if (startRow < 1) {
+                startRow = 1;
+            }
+            for (i = startRow; i < mRow; i++) {
+                mLines[i] = mLines[i + 1];
+            }
+            for (i = mRow; i < mLines.length; i++) {
+                // clear base row
+                mLines[i] = null;
+            }
+            // default to col 1, in case PAC is not sent
+            mCol = 1;
+        }
+
+        void writeText(String text) {
+            for (int i = 0; i < text.length(); i++) {
+                getLineBuffer(mRow).setCharAt(mCol, text.charAt(i));
+                moveCursorByCol(1);
+            }
+        }
+
+        void writeMidRowCode(StyleCode m) {
+            getLineBuffer(mRow).setMidRowAt(mCol, m);
+            moveCursorByCol(1);
+        }
+
+        void writePAC(PAC pac) {
+            if (pac.isIndentPAC()) {
+                moveCursorTo(pac.getRow(), pac.getCol());
+            } else {
+                moveCursorTo(pac.getRow(), 1);
+            }
+            getLineBuffer(mRow).setPACAt(mCol, pac);
+        }
+
+        SpannableStringBuilder[] getStyledText(CaptionStyle captionStyle) {
+            ArrayList<SpannableStringBuilder> rows = new ArrayList<>(MAX_ROWS);
+            for (int i = 1; i <= MAX_ROWS; i++) {
+                rows.add(mLines[i] != null ? mLines[i].getStyledText(captionStyle) : null);
+            }
+            return rows.toArray(new SpannableStringBuilder[MAX_ROWS]);
+        }
+
+        private static int clamp(int x, int min, int max) {
+            return x < min ? min : (x > max ? max : x);
+        }
+
+        private void moveCursorTo(int row, int col) {
+            mRow = clamp(row, 1, MAX_ROWS);
+            mCol = clamp(col, 1, MAX_COLS);
+        }
+
+        private void moveCursorToRow(int row) {
+            mRow = clamp(row, 1, MAX_ROWS);
+        }
+
+        private void moveCursorByCol(int col) {
+            mCol = clamp(mCol + col, 1, MAX_COLS);
+        }
+
+        private void moveBaselineTo(int baseRow, int windowSize) {
+            if (mRow == baseRow) {
+                return;
+            }
+            int actualWindowSize = windowSize;
+            if (baseRow < actualWindowSize) {
+                actualWindowSize = baseRow;
+            }
+            if (mRow < actualWindowSize) {
+                actualWindowSize = mRow;
+            }
+
+            int i;
+            if (baseRow < mRow) {
+                // copy from bottom to top row
+                for (i = actualWindowSize - 1; i >= 0; i--) {
+                    mLines[baseRow - i] = mLines[mRow - i];
+                }
+            } else {
+                // copy from top to bottom row
+                for (i = 0; i < actualWindowSize; i++) {
+                    mLines[baseRow - i] = mLines[mRow - i];
+                }
+            }
+            // clear rest of the rows
+            for (i = 0; i <= baseRow - windowSize; i++) {
+                mLines[i] = null;
+            }
+            for (i = baseRow + 1; i < mLines.length; i++) {
+                mLines[i] = null;
+            }
+        }
+
+        private CCLineBuilder getLineBuffer(int row) {
+            if (mLines[row] == null) {
+                mLines[row] = new CCLineBuilder(mBlankLine);
+            }
+            return mLines[row];
+        }
+    }
+
+    /*
+     * CCData parses the raw CC byte pair into displayable chars,
+     * misc control codes, Mid-Row or Preamble Address Codes.
+     */
+    private static class CCData {
+        private final byte mType;
+        private final byte mData1;
+        private final byte mData2;
+
+        private static final String[] sCtrlCodeMap = {
+            "RCL", "BS" , "AOF", "AON",
+            "DER", "RU2", "RU3", "RU4",
+            "FON", "RDC", "TR" , "RTD",
+            "EDM", "CR" , "ENM", "EOC",
+        };
+
+        private static final String[] sSpecialCharMap = {
+            "\u00AE",
+            "\u00B0",
+            "\u00BD",
+            "\u00BF",
+            "\u2122",
+            "\u00A2",
+            "\u00A3",
+            "\u266A", // Eighth note
+            "\u00E0",
+            "\u00A0", // Transparent space
+            "\u00E8",
+            "\u00E2",
+            "\u00EA",
+            "\u00EE",
+            "\u00F4",
+            "\u00FB",
+        };
+
+        private static final String[] sSpanishCharMap = {
+            // Spanish and misc chars
+            "\u00C1", // A
+            "\u00C9", // E
+            "\u00D3", // I
+            "\u00DA", // O
+            "\u00DC", // U
+            "\u00FC", // u
+            "\u2018", // opening single quote
+            "\u00A1", // inverted exclamation mark
+            "*",
+            "'",
+            "\u2014", // em dash
+            "\u00A9", // Copyright
+            "\u2120", // Servicemark
+            "\u2022", // round bullet
+            "\u201C", // opening double quote
+            "\u201D", // closing double quote
+            // French
+            "\u00C0",
+            "\u00C2",
+            "\u00C7",
+            "\u00C8",
+            "\u00CA",
+            "\u00CB",
+            "\u00EB",
+            "\u00CE",
+            "\u00CF",
+            "\u00EF",
+            "\u00D4",
+            "\u00D9",
+            "\u00F9",
+            "\u00DB",
+            "\u00AB",
+            "\u00BB"
+        };
+
+        private static final String[] sProtugueseCharMap = {
+            // Portuguese
+            "\u00C3",
+            "\u00E3",
+            "\u00CD",
+            "\u00CC",
+            "\u00EC",
+            "\u00D2",
+            "\u00F2",
+            "\u00D5",
+            "\u00F5",
+            "{",
+            "}",
+            "\\",
+            "^",
+            "_",
+            "|",
+            "~",
+            // German and misc chars
+            "\u00C4",
+            "\u00E4",
+            "\u00D6",
+            "\u00F6",
+            "\u00DF",
+            "\u00A5",
+            "\u00A4",
+            "\u2502", // vertical bar
+            "\u00C5",
+            "\u00E5",
+            "\u00D8",
+            "\u00F8",
+            "\u250C", // top-left corner
+            "\u2510", // top-right corner
+            "\u2514", // lower-left corner
+            "\u2518", // lower-right corner
+        };
+
+        static CCData[] fromByteArray(byte[] data) {
+            CCData[] ccData = new CCData[data.length / 3];
+
+            for (int i = 0; i < ccData.length; i++) {
+                ccData[i] = new CCData(
+                        data[i * 3],
+                        data[i * 3 + 1],
+                        data[i * 3 + 2]);
+            }
+
+            return ccData;
+        }
+
+        CCData(byte type, byte data1, byte data2) {
+            mType = type;
+            mData1 = data1;
+            mData2 = data2;
+        }
+
+        int getCtrlCode() {
+            if ((mData1 == 0x14 || mData1 == 0x1c)
+                    && mData2 >= 0x20 && mData2 <= 0x2f) {
+                return mData2;
+            }
+            return INVALID;
+        }
+
+        StyleCode getMidRow() {
+            // only support standard Mid-row codes, ignore
+            // optional background/foreground mid-row codes
+            if ((mData1 == 0x11 || mData1 == 0x19)
+                    && mData2 >= 0x20 && mData2 <= 0x2f) {
+                return StyleCode.fromByte(mData2);
+            }
+            return null;
+        }
+
+        PAC getPAC() {
+            if ((mData1 & 0x70) == 0x10
+                    && (mData2 & 0x40) == 0x40
+                    && ((mData1 & 0x07) != 0 || (mData2 & 0x20) == 0)) {
+                return PAC.fromBytes(mData1, mData2);
+            }
+            return null;
+        }
+
+        int getTabOffset() {
+            if ((mData1 == 0x17 || mData1 == 0x1f)
+                    && mData2 >= 0x21 && mData2 <= 0x23) {
+                return mData2 & 0x3;
+            }
+            return 0;
+        }
+
+        boolean isDisplayableChar() {
+            return isBasicChar() || isSpecialChar() || isExtendedChar();
+        }
+
+        String getDisplayText() {
+            String str = getBasicChars();
+
+            if (str == null) {
+                str =  getSpecialChar();
+
+                if (str == null) {
+                    str = getExtendedChar();
+                }
+            }
+
+            return str;
+        }
+
+        private String ctrlCodeToString(int ctrlCode) {
+            return sCtrlCodeMap[ctrlCode - 0x20];
+        }
+
+        private boolean isBasicChar() {
+            return mData1 >= 0x20 && mData1 <= 0x7f;
+        }
+
+        private boolean isSpecialChar() {
+            return ((mData1 == 0x11 || mData1 == 0x19)
+                    && mData2 >= 0x30 && mData2 <= 0x3f);
+        }
+
+        private boolean isExtendedChar() {
+            return ((mData1 == 0x12 || mData1 == 0x1A
+                    || mData1 == 0x13 || mData1 == 0x1B)
+                    && mData2 >= 0x20 && mData2 <= 0x3f);
+        }
+
+        private char getBasicChar(byte data) {
+            char c;
+            // replace the non-ASCII ones
+            switch (data) {
+                case 0x2A: c = '\u00E1'; break;
+                case 0x5C: c = '\u00E9'; break;
+                case 0x5E: c = '\u00ED'; break;
+                case 0x5F: c = '\u00F3'; break;
+                case 0x60: c = '\u00FA'; break;
+                case 0x7B: c = '\u00E7'; break;
+                case 0x7C: c = '\u00F7'; break;
+                case 0x7D: c = '\u00D1'; break;
+                case 0x7E: c = '\u00F1'; break;
+                case 0x7F: c = '\u2588'; break; // Full block
+                default: c = (char) data; break;
+            }
+            return c;
+        }
+
+        private String getBasicChars() {
+            if (mData1 >= 0x20 && mData1 <= 0x7f) {
+                StringBuilder builder = new StringBuilder(2);
+                builder.append(getBasicChar(mData1));
+                if (mData2 >= 0x20 && mData2 <= 0x7f) {
+                    builder.append(getBasicChar(mData2));
+                }
+                return builder.toString();
+            }
+
+            return null;
+        }
+
+        private String getSpecialChar() {
+            if ((mData1 == 0x11 || mData1 == 0x19)
+                    && mData2 >= 0x30 && mData2 <= 0x3f) {
+                return sSpecialCharMap[mData2 - 0x30];
+            }
+
+            return null;
+        }
+
+        private String getExtendedChar() {
+            if ((mData1 == 0x12 || mData1 == 0x1A) && mData2 >= 0x20 && mData2 <= 0x3f) {
+                // 1 Spanish/French char
+                return sSpanishCharMap[mData2 - 0x20];
+            } else if ((mData1 == 0x13 || mData1 == 0x1B) && mData2 >= 0x20 && mData2 <= 0x3f) {
+                // 1 Portuguese/German/Danish char
+                return sProtugueseCharMap[mData2 - 0x20];
+            }
+
+            return null;
+        }
+
+        @Override
+        public String toString() {
+            String str;
+
+            if (mData1 < 0x10 && mData2 < 0x10) {
+                // Null Pad, ignore
+                return String.format("[%d]Null: %02x %02x", mType, mData1, mData2);
+            }
+
+            int ctrlCode = getCtrlCode();
+            if (ctrlCode != INVALID) {
+                return String.format("[%d]%s", mType, ctrlCodeToString(ctrlCode));
+            }
+
+            int tabOffset = getTabOffset();
+            if (tabOffset > 0) {
+                return String.format("[%d]Tab%d", mType, tabOffset);
+            }
+
+            PAC pac = getPAC();
+            if (pac != null) {
+                return String.format("[%d]PAC: %s", mType, pac.toString());
+            }
+
+            StyleCode m = getMidRow();
+            if (m != null) {
+                return String.format("[%d]Mid-row: %s", mType, m.toString());
+            }
+
+            if (isDisplayableChar()) {
+                return String.format("[%d]Displayable: %s (%02x %02x)",
+                        mType, getDisplayText(), mData1, mData2);
+            }
+
+            return String.format("[%d]Invalid: %02x %02x", mType, mData1, mData2);
+        }
+    }
+}
diff --git a/media/src/main/java/androidx/media/subtitle/ClosedCaptionRenderer.java b/media/src/main/java/androidx/media/subtitle/ClosedCaptionRenderer.java
new file mode 100644
index 0000000..90ff516
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/ClosedCaptionRenderer.java
@@ -0,0 +1,402 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.subtitle;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.media.MediaFormat;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.media.R;
+
+import java.util.ArrayList;
+
+// Note: This is forked from android.media.ClosedCaptionRenderer since P
+/**
+ * @hide
+ */
+@RequiresApi(28)
+@RestrictTo(LIBRARY_GROUP)
+public class ClosedCaptionRenderer extends SubtitleController.Renderer {
+    private final Context mContext;
+    private Cea608CCWidget mCCWidget;
+
+    public ClosedCaptionRenderer(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    public boolean supports(MediaFormat format) {
+        if (format.containsKey(MediaFormat.KEY_MIME)) {
+            String mimeType = format.getString(MediaFormat.KEY_MIME);
+            return MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType);
+        }
+        return false;
+    }
+
+    @Override
+    public SubtitleTrack createTrack(MediaFormat format) {
+        String mimeType = format.getString(MediaFormat.KEY_MIME);
+        if (MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType)) {
+            if (mCCWidget == null) {
+                mCCWidget = new Cea608CCWidget(mContext);
+            }
+            return new Cea608CaptionTrack(mCCWidget, format);
+        }
+        throw new RuntimeException("No matching format: " + format.toString());
+    }
+
+    static class Cea608CaptionTrack extends SubtitleTrack {
+        private final Cea608CCParser mCCParser;
+        private final Cea608CCWidget mRenderingWidget;
+
+        Cea608CaptionTrack(Cea608CCWidget renderingWidget, MediaFormat format) {
+            super(format);
+
+            mRenderingWidget = renderingWidget;
+            mCCParser = new Cea608CCParser(mRenderingWidget);
+        }
+
+        @Override
+        public void onData(byte[] data, boolean eos, long runID) {
+            mCCParser.parse(data);
+        }
+
+        @Override
+        public RenderingWidget getRenderingWidget() {
+            return mRenderingWidget;
+        }
+
+        @Override
+        public void updateView(ArrayList<Cue> activeCues) {
+            // Overriding with NO-OP, CC rendering by-passes this
+        }
+    }
+
+    /**
+     * Widget capable of rendering CEA-608 closed captions.
+     */
+    class Cea608CCWidget extends ClosedCaptionWidget implements Cea608CCParser.DisplayListener {
+        private static final String DUMMY_TEXT = "1234567890123456789012345678901234";
+        private final Rect mTextBounds = new Rect();
+
+        Cea608CCWidget(Context context) {
+            this(context, null);
+        }
+
+        Cea608CCWidget(Context context, AttributeSet attrs) {
+            this(context, attrs, 0);
+        }
+
+        Cea608CCWidget(Context context, AttributeSet attrs, int defStyle) {
+            this(context, attrs, defStyle, 0);
+        }
+
+        Cea608CCWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+            super(context, attrs, defStyleAttr, defStyleRes);
+        }
+
+        @Override
+        public ClosedCaptionLayout createCaptionLayout(Context context) {
+            return new CCLayout(context);
+        }
+
+        @Override
+        public void onDisplayChanged(SpannableStringBuilder[] styledTexts) {
+            ((CCLayout) mClosedCaptionLayout).update(styledTexts);
+
+            if (mListener != null) {
+                mListener.onChanged(this);
+            }
+        }
+
+        @Override
+        public CaptionStyle getCaptionStyle() {
+            return mCaptionStyle;
+        }
+
+        private class CCLineBox extends TextView {
+            private static final float FONT_PADDING_RATIO = 0.75f;
+            private static final float EDGE_OUTLINE_RATIO = 0.1f;
+            private static final float EDGE_SHADOW_RATIO = 0.05f;
+            private float mOutlineWidth;
+            private float mShadowRadius;
+            private float mShadowOffset;
+
+            private int mTextColor = Color.WHITE;
+            private int mBgColor = Color.BLACK;
+            private int mEdgeType = CaptionStyle.EDGE_TYPE_NONE;
+            private int mEdgeColor = Color.TRANSPARENT;
+
+            CCLineBox(Context context) {
+                super(context);
+                setGravity(Gravity.CENTER);
+                setBackgroundColor(Color.TRANSPARENT);
+                setTextColor(Color.WHITE);
+                setTypeface(Typeface.MONOSPACE);
+                setVisibility(View.INVISIBLE);
+
+                final Resources res = getContext().getResources();
+
+                // get the default (will be updated later during measure)
+                mOutlineWidth = res.getDimensionPixelSize(
+                        R.dimen.subtitle_outline_width);
+                mShadowRadius = res.getDimensionPixelSize(
+                        R.dimen.subtitle_shadow_radius);
+                mShadowOffset = res.getDimensionPixelSize(
+                        R.dimen.subtitle_shadow_offset);
+            }
+
+            void setCaptionStyle(CaptionStyle captionStyle) {
+                mTextColor = captionStyle.foregroundColor;
+                mBgColor = captionStyle.backgroundColor;
+                mEdgeType = captionStyle.edgeType;
+                mEdgeColor = captionStyle.edgeColor;
+
+                setTextColor(mTextColor);
+                if (mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
+                    setShadowLayer(mShadowRadius, mShadowOffset, mShadowOffset, mEdgeColor);
+                } else {
+                    setShadowLayer(0, 0, 0, 0);
+                }
+                invalidate();
+            }
+
+            @Override
+            protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+                float fontSize = MeasureSpec.getSize(heightMeasureSpec) * FONT_PADDING_RATIO;
+                setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
+
+                mOutlineWidth = EDGE_OUTLINE_RATIO * fontSize + 1.0f;
+                mShadowRadius = EDGE_SHADOW_RATIO * fontSize + 1.0f;
+                mShadowOffset = mShadowRadius;
+
+                // set font scale in the X direction to match the required width
+                setScaleX(1.0f);
+                getPaint().getTextBounds(DUMMY_TEXT, 0, DUMMY_TEXT.length(), mTextBounds);
+                float actualTextWidth = mTextBounds.width();
+                float requiredTextWidth = MeasureSpec.getSize(widthMeasureSpec);
+                setScaleX(requiredTextWidth / actualTextWidth);
+
+                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+            }
+
+            @Override
+            protected void onDraw(Canvas c) {
+                if (mEdgeType == CaptionStyle.EDGE_TYPE_UNSPECIFIED
+                        || mEdgeType == CaptionStyle.EDGE_TYPE_NONE
+                        || mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
+                    // these edge styles don't require a second pass
+                    super.onDraw(c);
+                    return;
+                }
+
+                if (mEdgeType == CaptionStyle.EDGE_TYPE_OUTLINE) {
+                    drawEdgeOutline(c);
+                } else {
+                    // Raised or depressed
+                    drawEdgeRaisedOrDepressed(c);
+                }
+            }
+
+            @SuppressWarnings("WrongCall")
+            private void drawEdgeOutline(Canvas c) {
+                TextPaint textPaint = getPaint();
+
+                Paint.Style previousStyle = textPaint.getStyle();
+                Paint.Join previousJoin = textPaint.getStrokeJoin();
+                float previousWidth = textPaint.getStrokeWidth();
+
+                setTextColor(mEdgeColor);
+                textPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+                textPaint.setStrokeJoin(Paint.Join.ROUND);
+                textPaint.setStrokeWidth(mOutlineWidth);
+
+                // Draw outline and background only.
+                super.onDraw(c);
+
+                // Restore original settings.
+                setTextColor(mTextColor);
+                textPaint.setStyle(previousStyle);
+                textPaint.setStrokeJoin(previousJoin);
+                textPaint.setStrokeWidth(previousWidth);
+
+                // Remove the background.
+                setBackgroundSpans(Color.TRANSPARENT);
+                // Draw foreground only.
+                super.onDraw(c);
+                // Restore the background.
+                setBackgroundSpans(mBgColor);
+            }
+
+            @SuppressWarnings("WrongCall")
+            private void drawEdgeRaisedOrDepressed(Canvas c) {
+                TextPaint textPaint = getPaint();
+
+                Paint.Style previousStyle = textPaint.getStyle();
+                textPaint.setStyle(Paint.Style.FILL);
+
+                final boolean raised = mEdgeType == CaptionStyle.EDGE_TYPE_RAISED;
+                final int colorUp = raised ? Color.WHITE : mEdgeColor;
+                final int colorDown = raised ? mEdgeColor : Color.WHITE;
+                final float offset = mShadowRadius / 2f;
+
+                // Draw background and text with shadow up
+                setShadowLayer(mShadowRadius, -offset, -offset, colorUp);
+                super.onDraw(c);
+
+                // Remove the background.
+                setBackgroundSpans(Color.TRANSPARENT);
+
+                // Draw text with shadow down
+                setShadowLayer(mShadowRadius, +offset, +offset, colorDown);
+                super.onDraw(c);
+
+                // Restore settings
+                textPaint.setStyle(previousStyle);
+
+                // Restore the background.
+                setBackgroundSpans(mBgColor);
+            }
+
+            private void setBackgroundSpans(int color) {
+                CharSequence text = getText();
+                if (text instanceof Spannable) {
+                    Spannable spannable = (Spannable) text;
+                    Cea608CCParser.MutableBackgroundColorSpan[] bgSpans = spannable.getSpans(
+                            0, spannable.length(), Cea608CCParser.MutableBackgroundColorSpan.class);
+                    for (int i = 0; i < bgSpans.length; i++) {
+                        bgSpans[i].setBackgroundColor(color);
+                    }
+                }
+            }
+        }
+
+        private class CCLayout extends LinearLayout implements ClosedCaptionLayout {
+            private static final int MAX_ROWS = Cea608CCParser.MAX_ROWS;
+            private static final float SAFE_AREA_RATIO = 0.9f;
+
+            private final CCLineBox[] mLineBoxes = new CCLineBox[MAX_ROWS];
+
+            CCLayout(Context context) {
+                super(context);
+                setGravity(Gravity.START);
+                setOrientation(LinearLayout.VERTICAL);
+                for (int i = 0; i < MAX_ROWS; i++) {
+                    mLineBoxes[i] = new CCLineBox(getContext());
+                    addView(mLineBoxes[i], LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+                }
+            }
+
+            @Override
+            public void setCaptionStyle(CaptionStyle captionStyle) {
+                for (int i = 0; i < MAX_ROWS; i++) {
+                    mLineBoxes[i].setCaptionStyle(captionStyle);
+                }
+            }
+
+            @Override
+            public void setFontScale(float fontScale) {
+                // Ignores the font scale changes of the system wide CC preference.
+            }
+
+            void update(SpannableStringBuilder[] textBuffer) {
+                for (int i = 0; i < MAX_ROWS; i++) {
+                    if (textBuffer[i] != null) {
+                        mLineBoxes[i].setText(textBuffer[i], TextView.BufferType.SPANNABLE);
+                        mLineBoxes[i].setVisibility(View.VISIBLE);
+                    } else {
+                        mLineBoxes[i].setVisibility(View.INVISIBLE);
+                    }
+                }
+            }
+
+            @Override
+            protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+                int safeWidth = getMeasuredWidth();
+                int safeHeight = getMeasuredHeight();
+
+                // CEA-608 assumes 4:3 video
+                if (safeWidth * 3 >= safeHeight * 4) {
+                    safeWidth = safeHeight * 4 / 3;
+                } else {
+                    safeHeight = safeWidth * 3 / 4;
+                }
+                safeWidth = (int) (safeWidth * SAFE_AREA_RATIO);
+                safeHeight = (int) (safeHeight * SAFE_AREA_RATIO);
+
+                int lineHeight = safeHeight / MAX_ROWS;
+                int lineHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+                        lineHeight, MeasureSpec.EXACTLY);
+                int lineWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+                        safeWidth, MeasureSpec.EXACTLY);
+
+                for (int i = 0; i < MAX_ROWS; i++) {
+                    mLineBoxes[i].measure(lineWidthMeasureSpec, lineHeightMeasureSpec);
+                }
+            }
+
+            @Override
+            protected void onLayout(boolean changed, int l, int t, int r, int b) {
+                // safe caption area
+                int viewPortWidth = r - l;
+                int viewPortHeight = b - t;
+                int safeWidth, safeHeight;
+                // CEA-608 assumes 4:3 video
+                if (viewPortWidth * 3 >= viewPortHeight * 4) {
+                    safeWidth = viewPortHeight * 4 / 3;
+                    safeHeight = viewPortHeight;
+                } else {
+                    safeWidth = viewPortWidth;
+                    safeHeight = viewPortWidth * 3 / 4;
+                }
+                safeWidth = (int) (safeWidth * SAFE_AREA_RATIO);
+                safeHeight = (int) (safeHeight * SAFE_AREA_RATIO);
+                int left = (viewPortWidth - safeWidth) / 2;
+                int top = (viewPortHeight - safeHeight) / 2;
+
+                for (int i = 0; i < MAX_ROWS; i++) {
+                    mLineBoxes[i].layout(
+                            left,
+                            top + safeHeight * i / MAX_ROWS,
+                            left + safeWidth,
+                            top + safeHeight * (i + 1) / MAX_ROWS);
+                }
+            }
+        }
+    }
+}
diff --git a/media/src/main/java/androidx/media/subtitle/ClosedCaptionWidget.java b/media/src/main/java/androidx/media/subtitle/ClosedCaptionWidget.java
new file mode 100644
index 0000000..a3d3e47
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/ClosedCaptionWidget.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.subtitle;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.CaptioningManager;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
+import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
+
+import androidx.annotation.RequiresApi;
+
+/**
+ * Abstract widget class to render a closed caption track.
+ */
+@RequiresApi(28)
+abstract class ClosedCaptionWidget extends ViewGroup implements SubtitleTrack.RenderingWidget {
+
+    interface ClosedCaptionLayout {
+        void setCaptionStyle(CaptionStyle captionStyle);
+        void setFontScale(float scale);
+    }
+
+    /** Captioning manager, used to obtain and track caption properties. */
+    private final CaptioningManager mManager;
+
+    /** Current caption style. */
+    protected CaptionStyle mCaptionStyle;
+
+    /** Callback for rendering changes. */
+    protected OnChangedListener mListener;
+
+    /** Concrete layout of CC. */
+    protected ClosedCaptionLayout mClosedCaptionLayout;
+
+    /** Whether a caption style change listener is registered. */
+    private boolean mHasChangeListener;
+
+    ClosedCaptionWidget(Context context) {
+        this(context, null);
+    }
+
+    ClosedCaptionWidget(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyle) {
+        this(context, attrs, defStyle, 0);
+    }
+
+    ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        // Cannot render text over video when layer type is hardware.
+        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+
+        mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
+        mCaptionStyle = mManager.getUserStyle();
+
+        mClosedCaptionLayout = createCaptionLayout(context);
+        mClosedCaptionLayout.setCaptionStyle(mCaptionStyle);
+        mClosedCaptionLayout.setFontScale(mManager.getFontScale());
+        addView((ViewGroup) mClosedCaptionLayout, LayoutParams.MATCH_PARENT,
+                LayoutParams.MATCH_PARENT);
+
+        requestLayout();
+    }
+
+    public abstract ClosedCaptionLayout createCaptionLayout(Context context);
+
+    @Override
+    public void setOnChangedListener(OnChangedListener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    public void setSize(int width, int height) {
+        final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
+        final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+
+        measure(widthSpec, heightSpec);
+        layout(0, 0, width, height);
+    }
+
+    @Override
+    public void setVisible(boolean visible) {
+        if (visible) {
+            setVisibility(View.VISIBLE);
+        } else {
+            setVisibility(View.GONE);
+        }
+
+        manageChangeListener();
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        manageChangeListener();
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        manageChangeListener();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        ((ViewGroup) mClosedCaptionLayout).measure(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        ((ViewGroup) mClosedCaptionLayout).layout(l, t, r, b);
+    }
+
+    /**
+     * Manages whether this renderer is listening for caption style changes.
+     */
+    private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() {
+        @Override
+        public void onUserStyleChanged(CaptionStyle userStyle) {
+            mCaptionStyle = userStyle;
+            mClosedCaptionLayout.setCaptionStyle(mCaptionStyle);
+        }
+
+        @Override
+        public void onFontScaleChanged(float fontScale) {
+            mClosedCaptionLayout.setFontScale(fontScale);
+        }
+    };
+
+    private void manageChangeListener() {
+        final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE;
+        if (mHasChangeListener != needsListener) {
+            mHasChangeListener = needsListener;
+
+            if (needsListener) {
+                mManager.addCaptioningChangeListener(mCaptioningListener);
+            } else {
+                mManager.removeCaptioningChangeListener(mCaptioningListener);
+            }
+        }
+    }
+}
+
diff --git a/media/src/main/java/androidx/media/subtitle/MediaTimeProvider.java b/media/src/main/java/androidx/media/subtitle/MediaTimeProvider.java
new file mode 100644
index 0000000..b6f0a14
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/MediaTimeProvider.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.subtitle;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import androidx.annotation.RestrictTo;
+
+// Note: This is just copied from android.media.MediaTimeProvider.
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public interface MediaTimeProvider {
+    // we do not allow negative media time
+    /**
+     * Presentation time value if no timed event notification is requested.
+     */
+    long NO_TIME = -1;
+
+    /**
+     * Cancels all previous notification request from this listener if any.  It
+     * registers the listener to get seek and stop notifications.  If timeUs is
+     * not negative, it also registers the listener for a timed event
+     * notification when the presentation time reaches (becomes greater) than
+     * the value specified.  This happens immediately if the current media time
+     * is larger than or equal to timeUs.
+     *
+     * @param timeUs presentation time to get timed event callback at (or
+     *               {@link #NO_TIME})
+     */
+    void notifyAt(long timeUs, OnMediaTimeListener listener);
+
+    /**
+     * Cancels all previous notification request from this listener if any.  It
+     * registers the listener to get seek and stop notifications.  If the media
+     * is stopped, the listener will immediately receive a stop notification.
+     * Otherwise, it will receive a timed event notificaton.
+     */
+    void scheduleUpdate(OnMediaTimeListener listener);
+
+    /**
+     * Cancels all previous notification request from this listener if any.
+     */
+    void cancelNotifications(OnMediaTimeListener listener);
+
+    /**
+     * Get the current presentation time.
+     *
+     * @param precise   Whether getting a precise time is important. This is
+     *                  more costly.
+     * @param monotonic Whether returned time should be monotonic: that is,
+     *                  greater than or equal to the last returned time.  Don't
+     *                  always set this to true.  E.g. this has undesired
+     *                  consequences if the media is seeked between calls.
+     * @throws IllegalStateException if the media is not initialized
+     */
+    long getCurrentTimeUs(boolean precise, boolean monotonic)
+            throws IllegalStateException;
+
+    /**
+     * Mediatime listener
+     */
+    public interface OnMediaTimeListener {
+        /**
+         * Called when the registered time was reached naturally.
+         *
+         * @param timeUs current media time
+         */
+        void onTimedEvent(long timeUs);
+
+        /**
+         * Called when the media time changed due to seeking.
+         *
+         * @param timeUs current media time
+         */
+        void onSeek(long timeUs);
+
+        /**
+         * Called when the playback stopped.  This is not called on pause, only
+         * on full stop, at which point there is no further current media time.
+         */
+        void onStop();
+    }
+}
+
diff --git a/media/src/main/java/androidx/media/subtitle/SubtitleController.java b/media/src/main/java/androidx/media/subtitle/SubtitleController.java
new file mode 100644
index 0000000..b6dfc2b
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/SubtitleController.java
@@ -0,0 +1,534 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.subtitle;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.media.MediaFormat;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.TrackInfo;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.view.accessibility.CaptioningManager;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.media.subtitle.SubtitleTrack.RenderingWidget;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+// Note: This is forked from android.media.SubtitleController since P
+/**
+ * The subtitle controller provides the architecture to display subtitles for a
+ * media source.  It allows specifying which tracks to display, on which anchor
+ * to display them, and also allows adding external, out-of-band subtitle tracks.
+ *
+ * @hide
+ */
+@RequiresApi(28)
+@RestrictTo(LIBRARY_GROUP)
+public class SubtitleController {
+    private MediaTimeProvider mTimeProvider;
+    private ArrayList<Renderer> mRenderers;
+    private ArrayList<SubtitleTrack> mTracks;
+    private final Object mRenderersLock = new Object();
+    private final Object mTracksLock = new Object();
+    private SubtitleTrack mSelectedTrack;
+    private boolean mShowing;
+    private CaptioningManager mCaptioningManager;
+    private Handler mHandler;
+
+    private static final int WHAT_SHOW = 1;
+    private static final int WHAT_HIDE = 2;
+    private static final int WHAT_SELECT_TRACK = 3;
+    private static final int WHAT_SELECT_DEFAULT_TRACK = 4;
+
+    private final Handler.Callback mCallback = new Handler.Callback() {
+        @Override
+        public boolean handleMessage(Message msg) {
+            switch (msg.what) {
+                case WHAT_SHOW:
+                    doShow();
+                    return true;
+                case WHAT_HIDE:
+                    doHide();
+                    return true;
+                case WHAT_SELECT_TRACK:
+                    doSelectTrack((SubtitleTrack) msg.obj);
+                    return true;
+                case WHAT_SELECT_DEFAULT_TRACK:
+                    doSelectDefaultTrack();
+                    return true;
+                default:
+                    return false;
+            }
+        }
+    };
+
+    private CaptioningManager.CaptioningChangeListener mCaptioningChangeListener =
+            new CaptioningManager.CaptioningChangeListener() {
+                @Override
+                public void onEnabledChanged(boolean enabled) {
+                    selectDefaultTrack();
+                }
+
+                @Override
+                public void onLocaleChanged(Locale locale) {
+                    selectDefaultTrack();
+                }
+            };
+
+    public SubtitleController(Context context) {
+        this(context, null, null);
+    }
+
+    /**
+     * Creates a subtitle controller for a media playback object that implements
+     * the MediaTimeProvider interface.
+     *
+     * @param timeProvider
+     */
+    public SubtitleController(
+            Context context,
+            MediaTimeProvider timeProvider,
+            Listener listener) {
+        mTimeProvider = timeProvider;
+        mListener = listener;
+
+        mRenderers = new ArrayList<Renderer>();
+        mShowing = false;
+        mTracks = new ArrayList<SubtitleTrack>();
+        mCaptioningManager =
+            (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        mCaptioningManager.removeCaptioningChangeListener(
+                mCaptioningChangeListener);
+        super.finalize();
+    }
+
+    /**
+     * @return the available subtitle tracks for this media. These include
+     * the tracks found by {@link MediaPlayer} as well as any tracks added
+     * manually via {@link #addTrack}.
+     */
+    public SubtitleTrack[] getTracks() {
+        synchronized (mTracksLock) {
+            SubtitleTrack[] tracks = new SubtitleTrack[mTracks.size()];
+            mTracks.toArray(tracks);
+            return tracks;
+        }
+    }
+
+    /**
+     * @return the currently selected subtitle track
+     */
+    public SubtitleTrack getSelectedTrack() {
+        return mSelectedTrack;
+    }
+
+    private RenderingWidget getRenderingWidget() {
+        if (mSelectedTrack == null) {
+            return null;
+        }
+        return mSelectedTrack.getRenderingWidget();
+    }
+
+    /**
+     * Selects a subtitle track.  As a result, this track will receive
+     * in-band data from the {@link MediaPlayer}.  However, this does
+     * not change the subtitle visibility.
+     *
+     * Should be called from the anchor's (UI) thread. {@see #Anchor.getSubtitleLooper}
+     *
+     * @param track The subtitle track to select.  This must be one of the
+     *              tracks in {@link #getTracks}.
+     * @return true if the track was successfully selected.
+     */
+    public boolean selectTrack(SubtitleTrack track) {
+        if (track != null && !mTracks.contains(track)) {
+            return false;
+        }
+
+        processOnAnchor(mHandler.obtainMessage(WHAT_SELECT_TRACK, track));
+        return true;
+    }
+
+    private void doSelectTrack(SubtitleTrack track) {
+        mTrackIsExplicit = true;
+        if (mSelectedTrack == track) {
+            return;
+        }
+
+        if (mSelectedTrack != null) {
+            mSelectedTrack.hide();
+            mSelectedTrack.setTimeProvider(null);
+        }
+
+        mSelectedTrack = track;
+        if (mAnchor != null) {
+            mAnchor.setSubtitleWidget(getRenderingWidget());
+        }
+
+        if (mSelectedTrack != null) {
+            mSelectedTrack.setTimeProvider(mTimeProvider);
+            mSelectedTrack.show();
+        }
+
+        if (mListener != null) {
+            mListener.onSubtitleTrackSelected(track);
+        }
+    }
+
+    /**
+     * @return the default subtitle track based on system preferences, or null,
+     * if no such track exists in this manager.
+     *
+     * Supports HLS-flags: AUTOSELECT, FORCED & DEFAULT.
+     *
+     * 1. If captioning is disabled, only consider FORCED tracks. Otherwise,
+     * consider all tracks, but prefer non-FORCED ones.
+     * 2. If user selected "Default" caption language:
+     *   a. If there is a considered track with DEFAULT=yes, returns that track
+     *      (favor the first one in the current language if there are more than
+     *      one default tracks, or the first in general if none of them are in
+     *      the current language).
+     *   b. Otherwise, if there is a track with AUTOSELECT=yes in the current
+     *      language, return that one.
+     *   c. If there are no default tracks, and no autoselectable tracks in the
+     *      current language, return null.
+     * 3. If there is a track with the caption language, select that one.  Prefer
+     * the one with AUTOSELECT=no.
+     *
+     * The default values for these flags are DEFAULT=no, AUTOSELECT=yes
+     * and FORCED=no.
+     */
+    public SubtitleTrack getDefaultTrack() {
+        SubtitleTrack bestTrack = null;
+        int bestScore = -1;
+
+        Locale selectedLocale = mCaptioningManager.getLocale();
+        Locale locale = selectedLocale;
+        if (locale == null) {
+            locale = Locale.getDefault();
+        }
+        boolean selectForced = !mCaptioningManager.isEnabled();
+
+        synchronized (mTracksLock) {
+            for (SubtitleTrack track: mTracks) {
+                MediaFormat format = track.getFormat();
+                String language = format.getString(MediaFormat.KEY_LANGUAGE);
+                boolean forced = MediaFormatUtil
+                        .getInteger(format, MediaFormat.KEY_IS_FORCED_SUBTITLE, 0) != 0;
+                boolean autoselect = MediaFormatUtil
+                        .getInteger(format, MediaFormat.KEY_IS_AUTOSELECT, 1) != 0;
+                boolean is_default = MediaFormatUtil
+                        .getInteger(format, MediaFormat.KEY_IS_DEFAULT, 0) != 0;
+
+                boolean languageMatches = locale == null
+                        || locale.getLanguage().equals("")
+                        || locale.getISO3Language().equals(language)
+                        || locale.getLanguage().equals(language);
+                // is_default is meaningless unless caption language is 'default'
+                int score = (forced ? 0 : 8)
+                        + (((selectedLocale == null) && is_default) ? 4 : 0)
+                        + (autoselect ? 0 : 2) + (languageMatches ? 1 : 0);
+
+                if (selectForced && !forced) {
+                    continue;
+                }
+
+                // we treat null locale/language as matching any language
+                if ((selectedLocale == null && is_default)
+                        || (languageMatches && (autoselect || forced || selectedLocale != null))) {
+                    if (score > bestScore) {
+                        bestScore = score;
+                        bestTrack = track;
+                    }
+                }
+            }
+        }
+        return bestTrack;
+    }
+
+    static class MediaFormatUtil {
+        MediaFormatUtil() { }
+        static int getInteger(MediaFormat format, String name, int defaultValue) {
+            try {
+                return format.getInteger(name);
+            } catch (NullPointerException | ClassCastException e) {
+                /* no such field or field of different type */
+            }
+            return defaultValue;
+        }
+    }
+
+    private boolean mTrackIsExplicit = false;
+    private boolean mVisibilityIsExplicit = false;
+
+    /** should be called from anchor thread */
+    public void selectDefaultTrack() {
+        processOnAnchor(mHandler.obtainMessage(WHAT_SELECT_DEFAULT_TRACK));
+    }
+
+    private void doSelectDefaultTrack() {
+        if (mTrackIsExplicit) {
+            if (mVisibilityIsExplicit) {
+                return;
+            }
+            // If track selection is explicit, but visibility
+            // is not, it falls back to the captioning setting
+            if (mCaptioningManager.isEnabled()
+                    || (mSelectedTrack != null && MediaFormatUtil.getInteger(
+                            mSelectedTrack.getFormat(),
+                            MediaFormat.KEY_IS_FORCED_SUBTITLE, 0) != 0)) {
+                show();
+            } else if (mSelectedTrack != null
+                    && mSelectedTrack.getTrackType() == TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
+                hide();
+            }
+            mVisibilityIsExplicit = false;
+        }
+
+        // We can have a default (forced) track even if captioning
+        // is not enabled.  This is handled by getDefaultTrack().
+        // Show this track unless subtitles were explicitly hidden.
+        SubtitleTrack track = getDefaultTrack();
+        if (track != null) {
+            selectTrack(track);
+            mTrackIsExplicit = false;
+            if (!mVisibilityIsExplicit) {
+                show();
+                mVisibilityIsExplicit = false;
+            }
+        }
+    }
+
+    /** must be called from anchor thread */
+    public void reset() {
+        checkAnchorLooper();
+        hide();
+        selectTrack(null);
+        mTracks.clear();
+        mTrackIsExplicit = false;
+        mVisibilityIsExplicit = false;
+        mCaptioningManager.removeCaptioningChangeListener(
+                mCaptioningChangeListener);
+    }
+
+    /**
+     * Adds a new, external subtitle track to the manager.
+     *
+     * @param format the format of the track that will include at least
+     *               the MIME type {@link MediaFormat@KEY_MIME}.
+     * @return the created {@link SubtitleTrack} object
+     */
+    public SubtitleTrack addTrack(MediaFormat format) {
+        synchronized (mRenderersLock) {
+            for (Renderer renderer: mRenderers) {
+                if (renderer.supports(format)) {
+                    SubtitleTrack track = renderer.createTrack(format);
+                    if (track != null) {
+                        synchronized (mTracksLock) {
+                            if (mTracks.size() == 0) {
+                                mCaptioningManager.addCaptioningChangeListener(
+                                        mCaptioningChangeListener);
+                            }
+                            mTracks.add(track);
+                        }
+                        return track;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Show the selected (or default) subtitle track.
+     *
+     * Should be called from the anchor's (UI) thread. {@see #Anchor.getSubtitleLooper}
+     */
+    public void show() {
+        processOnAnchor(mHandler.obtainMessage(WHAT_SHOW));
+    }
+
+    private void doShow() {
+        mShowing = true;
+        mVisibilityIsExplicit = true;
+        if (mSelectedTrack != null) {
+            mSelectedTrack.show();
+        }
+    }
+
+    /**
+     * Hide the selected (or default) subtitle track.
+     *
+     * Should be called from the anchor's (UI) thread. {@see #Anchor.getSubtitleLooper}
+     */
+    public void hide() {
+        processOnAnchor(mHandler.obtainMessage(WHAT_HIDE));
+    }
+
+    private void doHide() {
+        mVisibilityIsExplicit = true;
+        if (mSelectedTrack != null) {
+            mSelectedTrack.hide();
+        }
+        mShowing = false;
+    }
+
+    /**
+     * Interface for supporting a single or multiple subtitle types in {@link MediaPlayer}.
+     */
+    public abstract static class Renderer {
+        /**
+         * Called by {@link MediaPlayer}'s {@link SubtitleController} when a new
+         * subtitle track is detected, to see if it should use this object to
+         * parse and display this subtitle track.
+         *
+         * @param format the format of the track that will include at least
+         *               the MIME type {@link MediaFormat@KEY_MIME}.
+         *
+         * @return true if and only if the track format is supported by this
+         * renderer
+         */
+        public abstract boolean supports(MediaFormat format);
+
+        /**
+         * Called by {@link MediaPlayer}'s {@link SubtitleController} for each
+         * subtitle track that was detected and is supported by this object to
+         * create a {@link SubtitleTrack} object.  This object will be created
+         * for each track that was found.  If the track is selected for display,
+         * this object will be used to parse and display the track data.
+         *
+         * @param format the format of the track that will include at least
+         *               the MIME type {@link MediaFormat@KEY_MIME}.
+         * @return a {@link SubtitleTrack} object that will be used to parse
+         * and render the subtitle track.
+         */
+        public abstract SubtitleTrack createTrack(MediaFormat format);
+    }
+
+    /**
+     * Add support for a subtitle format in {@link MediaPlayer}.
+     *
+     * @param renderer a {@link SubtitleController.Renderer} object that adds
+     *                 support for a subtitle format.
+     */
+    public void registerRenderer(Renderer renderer) {
+        synchronized (mRenderersLock) {
+            // TODO how to get available renderers in the system
+            if (!mRenderers.contains(renderer)) {
+                // TODO should added renderers override existing ones (to allow replacing?)
+                mRenderers.add(renderer);
+            }
+        }
+    }
+
+    /**
+     * Returns true if one of the registered renders supports given media format.
+     *
+     * @param format a {@link MediaFormat} object
+     * @return true if this SubtitleController has a renderer that supports
+     * the media format.
+     */
+    public boolean hasRendererFor(MediaFormat format) {
+        synchronized (mRenderersLock) {
+            // TODO how to get available renderers in the system
+            for (Renderer renderer: mRenderers) {
+                if (renderer.supports(format)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Subtitle anchor, an object that is able to display a subtitle renderer,
+     * e.g. a VideoView.
+     */
+    public interface Anchor {
+        /**
+         * Anchor should use the supplied subtitle rendering widget, or
+         * none if it is null.
+         */
+        void setSubtitleWidget(RenderingWidget subtitleWidget);
+
+        /**
+         * Anchors provide the looper on which all track visibility changes
+         * (track.show/hide, setSubtitleWidget) will take place.
+         */
+        Looper getSubtitleLooper();
+    }
+
+    private Anchor mAnchor;
+
+    /**
+     *  called from anchor's looper (if any, both when unsetting and
+     *  setting)
+     */
+    public void setAnchor(Anchor anchor) {
+        if (mAnchor == anchor) {
+            return;
+        }
+
+        if (mAnchor != null) {
+            checkAnchorLooper();
+            mAnchor.setSubtitleWidget(null);
+        }
+        mAnchor = anchor;
+        mHandler = null;
+        if (mAnchor != null) {
+            mHandler = new Handler(mAnchor.getSubtitleLooper(), mCallback);
+            checkAnchorLooper();
+            mAnchor.setSubtitleWidget(getRenderingWidget());
+        }
+    }
+
+    private void checkAnchorLooper() {
+        assert mHandler != null : "Should have a looper already";
+        assert Looper.myLooper() == mHandler.getLooper()
+                : "Must be called from the anchor's looper";
+    }
+
+    private void processOnAnchor(Message m) {
+        assert mHandler != null : "Should have a looper already";
+        if (Looper.myLooper() == mHandler.getLooper()) {
+            mHandler.dispatchMessage(m);
+        } else {
+            mHandler.sendMessage(m);
+        }
+    }
+
+    interface Listener {
+        /**
+         * Called when a subtitle track has been selected.
+         *
+         * @param track selected subtitle track or null
+         */
+        void onSubtitleTrackSelected(SubtitleTrack track);
+    }
+
+    private Listener mListener;
+}
diff --git a/media/src/main/java/androidx/media/subtitle/SubtitleTrack.java b/media/src/main/java/androidx/media/subtitle/SubtitleTrack.java
new file mode 100644
index 0000000..30c1316
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/SubtitleTrack.java
@@ -0,0 +1,715 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.subtitle;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.graphics.Canvas;
+import android.media.MediaFormat;
+import android.media.MediaPlayer.TrackInfo;
+import android.media.SubtitleData;
+import android.os.Handler;
+import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.Pair;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+// Note: This is forked from android.media.SubtitleTrack since P
+/**
+ * A subtitle track abstract base class that is responsible for parsing and displaying
+ * an instance of a particular type of subtitle.
+ *
+ * @hide
+ */
+@RequiresApi(28)
+@RestrictTo(LIBRARY_GROUP)
+public abstract class SubtitleTrack implements MediaTimeProvider.OnMediaTimeListener {
+    private static final String TAG = "SubtitleTrack";
+    private long mLastUpdateTimeMs;
+    private long mLastTimeMs;
+
+    private Runnable mRunnable;
+
+    private final LongSparseArray<Run> mRunsByEndTime = new LongSparseArray<Run>();
+    private final LongSparseArray<Run> mRunsByID = new LongSparseArray<Run>();
+
+    private CueList mCues;
+    private final ArrayList<Cue> mActiveCues = new ArrayList<Cue>();
+    protected boolean mVisible;
+
+    public boolean DEBUG = false;
+
+    protected Handler mHandler = new Handler();
+
+    private MediaFormat mFormat;
+
+    public SubtitleTrack(MediaFormat format) {
+        mFormat = format;
+        mCues = new CueList();
+        clearActiveCues();
+        mLastTimeMs = -1;
+    }
+
+    public final MediaFormat getFormat() {
+        return mFormat;
+    }
+
+    private long mNextScheduledTimeMs = -1;
+
+    /**
+     * Called when there is input data for the subtitle track.
+     */
+    public void onData(SubtitleData data) {
+        long runID = data.getStartTimeUs() + 1;
+        onData(data.getData(), true /* eos */, runID);
+        setRunDiscardTimeMs(
+                runID,
+                (data.getStartTimeUs() + data.getDurationUs()) / 1000);
+    }
+
+    /**
+     * Called when there is input data for the subtitle track.  The
+     * complete subtitle for a track can include multiple whole units
+     * (runs).  Each of these units can have multiple sections.  The
+     * contents of a run are submitted in sequential order, with eos
+     * indicating the last section of the run.  Calls from different
+     * runs must not be intermixed.
+     *
+     * @param data subtitle data byte buffer
+     * @param eos true if this is the last section of the run.
+     * @param runID mostly-unique ID for this run of data.  Subtitle cues
+     *              with runID of 0 are discarded immediately after
+     *              display.  Cues with runID of ~0 are discarded
+     *              only at the deletion of the track object.  Cues
+     *              with other runID-s are discarded at the end of the
+     *              run, which defaults to the latest timestamp of
+     *              any of its cues (with this runID).
+     */
+    protected abstract void onData(byte[] data, boolean eos, long runID);
+
+    /**
+     * Called when adding the subtitle rendering widget to the view hierarchy,
+     * as well as when showing or hiding the subtitle track, or when the video
+     * surface position has changed.
+     *
+     * @return the widget that renders this subtitle track. For most renderers
+     *         there should be a single shared instance that is used for all
+     *         tracks supported by that renderer, as at most one subtitle track
+     *         is visible at one time.
+     */
+    public abstract RenderingWidget getRenderingWidget();
+
+    /**
+     * Called when the active cues have changed, and the contents of the subtitle
+     * view should be updated.
+     */
+    public abstract void updateView(ArrayList<Cue> activeCues);
+
+    protected synchronized void updateActiveCues(boolean rebuild, long timeMs) {
+        // out-of-order times mean seeking or new active cues being added
+        // (during their own timespan)
+        if (rebuild || mLastUpdateTimeMs > timeMs) {
+            clearActiveCues();
+        }
+
+        for (Iterator<Pair<Long, Cue>> it =
+                mCues.entriesBetween(mLastUpdateTimeMs, timeMs).iterator(); it.hasNext(); ) {
+            Pair<Long, Cue> event = it.next();
+            Cue cue = event.second;
+
+            if (cue.mEndTimeMs == event.first) {
+                // remove past cues
+                if (DEBUG) Log.v(TAG, "Removing " + cue);
+                mActiveCues.remove(cue);
+                if (cue.mRunID == 0) {
+                    it.remove();
+                }
+            } else if (cue.mStartTimeMs == event.first) {
+                // add new cues
+                // TRICKY: this will happen in start order
+                if (DEBUG) Log.v(TAG, "Adding " + cue);
+                if (cue.mInnerTimesMs != null) {
+                    cue.onTime(timeMs);
+                }
+                mActiveCues.add(cue);
+            } else if (cue.mInnerTimesMs != null) {
+                // cue is modified
+                cue.onTime(timeMs);
+            }
+        }
+
+        /* complete any runs */
+        while (mRunsByEndTime.size() > 0 && mRunsByEndTime.keyAt(0) <= timeMs) {
+            removeRunsByEndTimeIndex(0); // removes element
+        }
+        mLastUpdateTimeMs = timeMs;
+    }
+
+    private void removeRunsByEndTimeIndex(int ix) {
+        Run run = mRunsByEndTime.valueAt(ix);
+        while (run != null) {
+            Cue cue = run.mFirstCue;
+            while (cue != null) {
+                mCues.remove(cue);
+                Cue nextCue = cue.mNextInRun;
+                cue.mNextInRun = null;
+                cue = nextCue;
+            }
+            mRunsByID.remove(run.mRunID);
+            Run nextRun = run.mNextRunAtEndTimeMs;
+            run.mPrevRunAtEndTimeMs = null;
+            run.mNextRunAtEndTimeMs = null;
+            run = nextRun;
+        }
+        mRunsByEndTime.removeAt(ix);
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        /* remove all cues (untangle all cross-links) */
+        int size = mRunsByEndTime.size();
+        for (int ix = size - 1; ix >= 0; ix--) {
+            removeRunsByEndTimeIndex(ix);
+        }
+
+        super.finalize();
+    }
+
+    private synchronized void takeTime(long timeMs) {
+        mLastTimeMs = timeMs;
+    }
+
+    protected synchronized void clearActiveCues() {
+        if (DEBUG) Log.v(TAG, "Clearing " + mActiveCues.size() + " active cues");
+        mActiveCues.clear();
+        mLastUpdateTimeMs = -1;
+    }
+
+    protected void scheduleTimedEvents() {
+        /* get times for the next event */
+        if (mTimeProvider != null) {
+            mNextScheduledTimeMs = mCues.nextTimeAfter(mLastTimeMs);
+            if (DEBUG) Log.d(TAG, "sched @" + mNextScheduledTimeMs + " after " + mLastTimeMs);
+            mTimeProvider.notifyAt(mNextScheduledTimeMs >= 0
+                    ? (mNextScheduledTimeMs * 1000) : MediaTimeProvider.NO_TIME, this);
+        }
+    }
+
+    @Override
+    public void onTimedEvent(long timeUs) {
+        if (DEBUG) Log.d(TAG, "onTimedEvent " + timeUs);
+        synchronized (this) {
+            long timeMs = timeUs / 1000;
+            updateActiveCues(false, timeMs);
+            takeTime(timeMs);
+        }
+        updateView(mActiveCues);
+        scheduleTimedEvents();
+    }
+
+    @Override
+    public void onSeek(long timeUs) {
+        if (DEBUG) Log.d(TAG, "onSeek " + timeUs);
+        synchronized (this) {
+            long timeMs = timeUs / 1000;
+            updateActiveCues(true, timeMs);
+            takeTime(timeMs);
+        }
+        updateView(mActiveCues);
+        scheduleTimedEvents();
+    }
+
+    @Override
+    public void onStop() {
+        synchronized (this) {
+            if (DEBUG) Log.d(TAG, "onStop");
+            clearActiveCues();
+            mLastTimeMs = -1;
+        }
+        updateView(mActiveCues);
+        mNextScheduledTimeMs = -1;
+        mTimeProvider.notifyAt(MediaTimeProvider.NO_TIME, this);
+    }
+
+    protected MediaTimeProvider mTimeProvider;
+
+    /**
+     * Shows subtitle rendering widget
+     */
+    public void show() {
+        if (mVisible) {
+            return;
+        }
+
+        mVisible = true;
+        RenderingWidget renderingWidget = getRenderingWidget();
+        if (renderingWidget != null) {
+            renderingWidget.setVisible(true);
+        }
+        if (mTimeProvider != null) {
+            mTimeProvider.scheduleUpdate(this);
+        }
+    }
+
+    /**
+     * Hides subtitle rendering widget
+     */
+    public void hide() {
+        if (!mVisible) {
+            return;
+        }
+
+        if (mTimeProvider != null) {
+            mTimeProvider.cancelNotifications(this);
+        }
+        RenderingWidget renderingWidget = getRenderingWidget();
+        if (renderingWidget != null) {
+            renderingWidget.setVisible(false);
+        }
+        mVisible = false;
+    }
+
+    protected synchronized boolean addCue(Cue cue) {
+        mCues.add(cue);
+
+        if (cue.mRunID != 0) {
+            Run run = mRunsByID.get(cue.mRunID);
+            if (run == null) {
+                run = new Run();
+                mRunsByID.put(cue.mRunID, run);
+                run.mEndTimeMs = cue.mEndTimeMs;
+            } else if (run.mEndTimeMs < cue.mEndTimeMs) {
+                run.mEndTimeMs = cue.mEndTimeMs;
+            }
+
+            // link-up cues in the same run
+            cue.mNextInRun = run.mFirstCue;
+            run.mFirstCue = cue;
+        }
+
+        // if a cue is added that should be visible, need to refresh view
+        long nowMs = -1;
+        if (mTimeProvider != null) {
+            try {
+                nowMs = mTimeProvider.getCurrentTimeUs(
+                        false /* precise */, true /* monotonic */) / 1000;
+            } catch (IllegalStateException e) {
+                // handle as it we are not playing
+            }
+        }
+
+        if (DEBUG) {
+            Log.v(TAG, "mVisible=" + mVisible + ", "
+                    + cue.mStartTimeMs + " <= " + nowMs + ", "
+                    + cue.mEndTimeMs + " >= " + mLastTimeMs);
+        }
+
+        if (mVisible && cue.mStartTimeMs <= nowMs
+                // we don't trust nowMs, so check any cue since last callback
+                && cue.mEndTimeMs >= mLastTimeMs) {
+            if (mRunnable != null) {
+                mHandler.removeCallbacks(mRunnable);
+            }
+            final SubtitleTrack track = this;
+            final long thenMs = nowMs;
+            mRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    // even with synchronized, it is possible that we are going
+                    // to do multiple updates as the runnable could be already
+                    // running.
+                    synchronized (track) {
+                        mRunnable = null;
+                        updateActiveCues(true, thenMs);
+                        updateView(mActiveCues);
+                    }
+                }
+            };
+            // delay update so we don't update view on every cue.  TODO why 10?
+            if (mHandler.postDelayed(mRunnable, 10 /* delay */)) {
+                if (DEBUG) Log.v(TAG, "scheduling update");
+            } else {
+                if (DEBUG) Log.w(TAG, "failed to schedule subtitle view update");
+            }
+            return true;
+        }
+
+        if (mVisible && cue.mEndTimeMs >= mLastTimeMs
+                && (cue.mStartTimeMs < mNextScheduledTimeMs || mNextScheduledTimeMs < 0)) {
+            scheduleTimedEvents();
+        }
+
+        return false;
+    }
+
+    /**
+     * Sets MediaTimeProvider
+     */
+    public synchronized void setTimeProvider(MediaTimeProvider timeProvider) {
+        if (mTimeProvider == timeProvider) {
+            return;
+        }
+        if (mTimeProvider != null) {
+            mTimeProvider.cancelNotifications(this);
+        }
+        mTimeProvider = timeProvider;
+        if (mTimeProvider != null) {
+            mTimeProvider.scheduleUpdate(this);
+        }
+    }
+
+
+    static class CueList {
+        private static final String TAG = "CueList";
+        // simplistic, inefficient implementation
+        private SortedMap<Long, ArrayList<Cue>> mCues;
+        public boolean DEBUG = false;
+
+        private boolean addEvent(Cue cue, long timeMs) {
+            ArrayList<Cue> cues = mCues.get(timeMs);
+            if (cues == null) {
+                cues = new ArrayList<Cue>(2);
+                mCues.put(timeMs, cues);
+            } else if (cues.contains(cue)) {
+                // do not duplicate cues
+                return false;
+            }
+
+            cues.add(cue);
+            return true;
+        }
+
+        private void removeEvent(Cue cue, long timeMs) {
+            ArrayList<Cue> cues = mCues.get(timeMs);
+            if (cues != null) {
+                cues.remove(cue);
+                if (cues.size() == 0) {
+                    mCues.remove(timeMs);
+                }
+            }
+        }
+
+        public void add(Cue cue) {
+            // ignore non-positive-duration cues
+            if (cue.mStartTimeMs >= cue.mEndTimeMs) return;
+
+            if (!addEvent(cue, cue.mStartTimeMs)) {
+                return;
+            }
+
+            long lastTimeMs = cue.mStartTimeMs;
+            if (cue.mInnerTimesMs != null) {
+                for (long timeMs: cue.mInnerTimesMs) {
+                    if (timeMs > lastTimeMs && timeMs < cue.mEndTimeMs) {
+                        addEvent(cue, timeMs);
+                        lastTimeMs = timeMs;
+                    }
+                }
+            }
+
+            addEvent(cue, cue.mEndTimeMs);
+        }
+
+        public void remove(Cue cue) {
+            removeEvent(cue, cue.mStartTimeMs);
+            if (cue.mInnerTimesMs != null) {
+                for (long timeMs: cue.mInnerTimesMs) {
+                    removeEvent(cue, timeMs);
+                }
+            }
+            removeEvent(cue, cue.mEndTimeMs);
+        }
+
+        public Iterable<Pair<Long, Cue>> entriesBetween(
+                final long lastTimeMs, final long timeMs) {
+            return new Iterable<Pair<Long, Cue>>() {
+                @Override
+                public Iterator<Pair<Long, Cue>> iterator() {
+                    if (DEBUG) Log.d(TAG, "slice (" + lastTimeMs + ", " + timeMs + "]=");
+                    try {
+                        return new EntryIterator(
+                                mCues.subMap(lastTimeMs + 1, timeMs + 1));
+                    } catch (IllegalArgumentException e) {
+                        return new EntryIterator(null);
+                    }
+                }
+            };
+        }
+
+        public long nextTimeAfter(long timeMs) {
+            SortedMap<Long, ArrayList<Cue>> tail = null;
+            try {
+                tail = mCues.tailMap(timeMs + 1);
+                if (tail != null) {
+                    return tail.firstKey();
+                } else {
+                    return -1;
+                }
+            } catch (IllegalArgumentException e) {
+                return -1;
+            } catch (NoSuchElementException e) {
+                return -1;
+            }
+        }
+
+        class EntryIterator implements Iterator<Pair<Long, Cue>> {
+            @Override
+            public boolean hasNext() {
+                return !mDone;
+            }
+
+            @Override
+            public Pair<Long, Cue> next() {
+                if (mDone) {
+                    throw new NoSuchElementException("");
+                }
+                mLastEntry = new Pair<Long, Cue>(
+                        mCurrentTimeMs, mListIterator.next());
+                mLastListIterator = mListIterator;
+                if (!mListIterator.hasNext()) {
+                    nextKey();
+                }
+                return mLastEntry;
+            }
+
+            @Override
+            public void remove() {
+                // only allow removing end tags
+                if (mLastListIterator == null
+                        || mLastEntry.second.mEndTimeMs != mLastEntry.first) {
+                    throw new IllegalStateException("");
+                }
+
+                // remove end-cue
+                mLastListIterator.remove();
+                mLastListIterator = null;
+                if (mCues.get(mLastEntry.first).size() == 0) {
+                    mCues.remove(mLastEntry.first);
+                }
+
+                // remove rest of the cues
+                Cue cue = mLastEntry.second;
+                removeEvent(cue, cue.mStartTimeMs);
+                if (cue.mInnerTimesMs != null) {
+                    for (long timeMs: cue.mInnerTimesMs) {
+                        removeEvent(cue, timeMs);
+                    }
+                }
+            }
+
+            EntryIterator(SortedMap<Long, ArrayList<Cue>> cues) {
+                if (DEBUG) Log.v(TAG, cues + "");
+                mRemainingCues = cues;
+                mLastListIterator = null;
+                nextKey();
+            }
+
+            private void nextKey() {
+                do {
+                    try {
+                        if (mRemainingCues == null) {
+                            throw new NoSuchElementException("");
+                        }
+                        mCurrentTimeMs = mRemainingCues.firstKey();
+                        mListIterator =
+                            mRemainingCues.get(mCurrentTimeMs).iterator();
+                        try {
+                            mRemainingCues =
+                                mRemainingCues.tailMap(mCurrentTimeMs + 1);
+                        } catch (IllegalArgumentException e) {
+                            mRemainingCues = null;
+                        }
+                        mDone = false;
+                    } catch (NoSuchElementException e) {
+                        mDone = true;
+                        mRemainingCues = null;
+                        mListIterator = null;
+                        return;
+                    }
+                } while (!mListIterator.hasNext());
+            }
+
+            private long mCurrentTimeMs;
+            private Iterator<Cue> mListIterator;
+            private boolean mDone;
+            private SortedMap<Long, ArrayList<Cue>> mRemainingCues;
+            private Iterator<Cue> mLastListIterator;
+            private Pair<Long, Cue> mLastEntry;
+        }
+
+        CueList() {
+            mCues = new TreeMap<Long, ArrayList<Cue>>();
+        }
+    }
+
+    static class Cue {
+        public long mStartTimeMs;
+        public long mEndTimeMs;
+        public long[] mInnerTimesMs;
+        public long mRunID;
+
+        public Cue mNextInRun;
+
+        /**
+         * Called to inform current timeMs to the cue
+         */
+        public void onTime(long timeMs) { }
+    }
+
+    /** update mRunsByEndTime (with default end time) */
+    protected void finishedRun(long runID) {
+        if (runID != 0 && runID != ~0) {
+            Run run = mRunsByID.get(runID);
+            if (run != null) {
+                run.storeByEndTimeMs(mRunsByEndTime);
+            }
+        }
+    }
+
+    /** update mRunsByEndTime with given end time */
+    public void setRunDiscardTimeMs(long runID, long timeMs) {
+        if (runID != 0 && runID != ~0) {
+            Run run = mRunsByID.get(runID);
+            if (run != null) {
+                run.mEndTimeMs = timeMs;
+                run.storeByEndTimeMs(mRunsByEndTime);
+            }
+        }
+    }
+
+    /** whether this is a text track who fires events instead getting rendered */
+    public int getTrackType() {
+        return getRenderingWidget() == null
+                ? TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT
+                : TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE;
+    }
+
+
+    private static class Run {
+        public Cue mFirstCue;
+        public Run mNextRunAtEndTimeMs;
+        public Run mPrevRunAtEndTimeMs;
+        public long mEndTimeMs = -1;
+        public long mRunID = 0;
+        private long mStoredEndTimeMs = -1;
+
+        public void storeByEndTimeMs(LongSparseArray<Run> runsByEndTime) {
+            // remove old value if any
+            int ix = runsByEndTime.indexOfKey(mStoredEndTimeMs);
+            if (ix >= 0) {
+                if (mPrevRunAtEndTimeMs == null) {
+                    assert (this == runsByEndTime.valueAt(ix));
+                    if (mNextRunAtEndTimeMs == null) {
+                        runsByEndTime.removeAt(ix);
+                    } else {
+                        runsByEndTime.setValueAt(ix, mNextRunAtEndTimeMs);
+                    }
+                }
+                removeAtEndTimeMs();
+            }
+
+            // add new value
+            if (mEndTimeMs >= 0) {
+                mPrevRunAtEndTimeMs = null;
+                mNextRunAtEndTimeMs = runsByEndTime.get(mEndTimeMs);
+                if (mNextRunAtEndTimeMs != null) {
+                    mNextRunAtEndTimeMs.mPrevRunAtEndTimeMs = this;
+                }
+                runsByEndTime.put(mEndTimeMs, this);
+                mStoredEndTimeMs = mEndTimeMs;
+            }
+        }
+
+        public void removeAtEndTimeMs() {
+            Run prev = mPrevRunAtEndTimeMs;
+
+            if (mPrevRunAtEndTimeMs != null) {
+                mPrevRunAtEndTimeMs.mNextRunAtEndTimeMs = mNextRunAtEndTimeMs;
+                mPrevRunAtEndTimeMs = null;
+            }
+            if (mNextRunAtEndTimeMs != null) {
+                mNextRunAtEndTimeMs.mPrevRunAtEndTimeMs = prev;
+                mNextRunAtEndTimeMs = null;
+            }
+        }
+    }
+
+    /**
+     * Interface for rendering subtitles onto a Canvas.
+     */
+    public interface RenderingWidget {
+        /**
+         * Sets the widget's callback, which is used to send updates when the
+         * rendered data has changed.
+         *
+         * @param callback update callback
+         */
+        void setOnChangedListener(OnChangedListener callback);
+
+        /**
+         * Sets the widget's size.
+         *
+         * @param width width in pixels
+         * @param height height in pixels
+         */
+        void setSize(int width, int height);
+
+        /**
+         * Sets whether the widget should draw subtitles.
+         *
+         * @param visible true if subtitles should be drawn, false otherwise
+         */
+        void setVisible(boolean visible);
+
+        /**
+         * Renders subtitles onto a {@link Canvas}.
+         *
+         * @param c canvas on which to render subtitles
+         */
+        void draw(Canvas c);
+
+        /**
+         * Called when the widget is attached to a window.
+         */
+        void onAttachedToWindow();
+
+        /**
+         * Called when the widget is detached from a window.
+         */
+        void onDetachedFromWindow();
+
+        /**
+         * Callback used to send updates about changes to rendering data.
+         */
+        public interface OnChangedListener {
+            /**
+             * Called when the rendering data has changed.
+             *
+             * @param renderingWidget the widget whose data has changed
+             */
+            void onChanged(RenderingWidget renderingWidget);
+        }
+    }
+}
diff --git a/media/src/main/res/values/colors.xml b/media/src/main/res/values/colors.xml
index e44662a..d210220 100644
--- a/media/src/main/res/values/colors.xml
+++ b/media/src/main/res/values/colors.xml
@@ -19,12 +19,4 @@
     <!-- The color of the material notification background for media notifications when no custom
      color is specified -->
     <color name="notification_material_background_media_default_color">#ff424242</color>
-
-    <color name="gray">#808080</color>
-    <color name="white">#ffffff</color>
-    <color name="white_opacity_70">#B3ffffff</color>
-    <color name="black_opacity_70">#B3000000</color>
-    <color name="title_bar_gradient_start">#50000000</color>
-    <color name="title_bar_gradient_end">#00000000</color>
-    <color name="bottom_bar_background">#40202020</color>
 </resources>
\ No newline at end of file
diff --git a/media/src/main/res/values/dimens.xml b/media/src/main/res/values/dimens.xml
index 2d7b022..be50378 100644
--- a/media/src/main/res/values/dimens.xml
+++ b/media/src/main/res/values/dimens.xml
@@ -1,11 +1,12 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright 2018 The Android Open Source Project
+<!--
+     Copyright 2018 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
      You may obtain a copy of the License at
 
-          http://www.apache.org/licenses/LICENSE-2.0
+         http://www.apache.org/licenses/LICENSE-2.0
 
      Unless required by applicable law or agreed to in writing, software
      distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,60 +14,13 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-
 <resources>
-    <!-- Dialog size -->
-    <eat-comment />
-    <!-- The platform's desired fixed width for a dialog along the major axis
-         (the screen is in landscape). This may be either a fraction or a dimension.-->
-    <dimen name="mr_dialog_fixed_width_major">320dp</dimen>
-    <!-- The platform's desired fixed width for a dialog along the minor axis
-         (the screen is in portrait). This may be either a fraction or a dimension.-->
-    <dimen name="mr_dialog_fixed_width_minor">320dp</dimen>
+    <!-- Shadow radius for video subtitles. -->
+    <dimen name="subtitle_shadow_radius">2dp</dimen>
 
-    <!-- MediaRouteController's volume group list -->
-    <eat-comment />
-    <!-- Maximum height of volume group list. -->
-    <dimen name="mr_controller_volume_group_list_max_height">288dp</dimen>
-    <!-- Height of volume group item. -->
-    <dimen name="mr_controller_volume_group_list_item_height">68dp</dimen>
-    <!-- Size of an item's icon. -->
-    <dimen name="mr_controller_volume_group_list_item_icon_size">24dp</dimen>
+    <!-- Shadow offset for video subtitles. -->
+    <dimen name="subtitle_shadow_offset">2dp</dimen>
 
-    <dimen name="mr_controller_volume_group_list_padding_top">16dp</dimen>
-    <!-- Group list expand/collapse animation duration. -->
-    <integer name="mr_controller_volume_group_list_animation_duration_ms">400</integer>
-    <!-- Group list fade in animation duration. -->
-    <integer name="mr_controller_volume_group_list_fade_in_duration_ms">400</integer>
-    <!-- Group list fade out animation duration. -->
-    <integer name="mr_controller_volume_group_list_fade_out_duration_ms">200</integer>
-
-    <dimen name="mcv2_embedded_settings_width">150dp</dimen>
-    <dimen name="mcv2_embedded_settings_height">36dp</dimen>
-    <dimen name="mcv2_embedded_settings_icon_size">20dp</dimen>
-    <dimen name="mcv2_embedded_settings_text_height">18dp</dimen>
-    <dimen name="mcv2_embedded_settings_main_text_size">12sp</dimen>
-    <dimen name="mcv2_embedded_settings_sub_text_size">10sp</dimen>
-    <dimen name="mcv2_full_settings_width">225dp</dimen>
-    <dimen name="mcv2_full_settings_height">54dp</dimen>
-    <dimen name="mcv2_full_settings_icon_size">30dp</dimen>
-    <dimen name="mcv2_full_settings_text_height">27dp</dimen>
-    <dimen name="mcv2_full_settings_main_text_size">16sp</dimen>
-    <dimen name="mcv2_full_settings_sub_text_size">13sp</dimen>
-    <dimen name="mcv2_settings_offset">8dp</dimen>
-
-    <dimen name="mcv2_transport_controls_padding">4dp</dimen>
-    <dimen name="mcv2_pause_icon_size">36dp</dimen>
-    <dimen name="mcv2_full_icon_size">28dp</dimen>
-    <dimen name="mcv2_embedded_icon_size">24dp</dimen>
-    <dimen name="mcv2_minimal_icon_size">24dp</dimen>
-    <dimen name="mcv2_icon_margin">10dp</dimen>
-
-    <dimen name="mcv2_full_album_image_portrait_size">232dp</dimen>
-    <dimen name="mcv2_full_album_image_landscape_size">176dp</dimen>
-
-    <dimen name="mcv2_custom_progress_max_size">2dp</dimen>
-    <dimen name="mcv2_custom_progress_thumb_size">12dp</dimen>
-    <dimen name="mcv2_buffer_view_height">5dp</dimen>
-    <!-- TODO: adjust bottom bar view -->
+    <!-- Outline width for video subtitles. -->
+    <dimen name="subtitle_outline_width">2dp</dimen>
 </resources>
diff --git a/media/src/main/res/values/symbols.xml b/media/src/main/res/values/symbols.xml
deleted file mode 100644
index ee0e8c6..0000000
--- a/media/src/main/res/values/symbols.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* Copyright 2017, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-<resources>
-    <!--java-symbol type="id" name="cc" />
-    <java-symbol type="id" name="ffwd" />
-    <java-symbol type="id" name="mediacontroller_progress" />
-    <java-symbol type="id" name="next" />
-    <java-symbol type="id" name="pause" />
-    <java-symbol type="id" name="prev" />
-    <java-symbol type="id" name="rew" />
-    <java-symbol type="id" name="time" />
-    <java-symbol type="id" name="time_current" /-->
-</resources>
\ No newline at end of file
diff --git a/samples/SupportMediaDemos/OWNERS b/samples/SupportMediaDemos/OWNERS
new file mode 100644
index 0000000..2f5d227
--- /dev/null
+++ b/samples/SupportMediaDemos/OWNERS
@@ -0,0 +1,6 @@
+insun@google.com
+jinpark@google.com
+sungsoo@google.com
+hdmoon@google.com
+jaewan@google.com
+akersten@google.com
\ No newline at end of file
diff --git a/samples/SupportMediaDemos/build.gradle b/samples/SupportMediaDemos/build.gradle
new file mode 100644
index 0000000..fecf2a5
--- /dev/null
+++ b/samples/SupportMediaDemos/build.gradle
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+    id("SupportAndroidTestAppPlugin")
+}
+
+dependencies {
+    implementation(project(":media"))
+    implementation(project(":media-widget"))
+}
+
+android {
+    defaultConfig {
+        minSdkVersion = 19
+        targetSdkVersion = 26
+    }
+}
diff --git a/samples/SupportMediaDemos/src/main/AndroidManifest.xml b/samples/SupportMediaDemos/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5348161
--- /dev/null
+++ b/samples/SupportMediaDemos/src/main/AndroidManifest.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="com.example.androidx.media">
+
+    <uses-sdk tools:overrideLibrary="androidx.media.widget" />
+
+    <application android:label="Video View Test">
+
+        <!-- Video Selection Activity -->
+        <activity android:name="VideoSelector"
+            android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
+            android:configChanges="orientation|screenSize"
+            >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+
+        <!-- Video playback activity -->
+        <activity android:name="VideoViewTest"
+            android:configChanges="orientation|screenSize"
+            >
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW"/>
+            </intent-filter>
+        </activity>
+
+    </application>
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
+</manifest>
diff --git a/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoSelector.java b/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoSelector.java
new file mode 100644
index 0000000..a77789f
--- /dev/null
+++ b/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoSelector.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.androidx.media;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ListView;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.TreeMap;
+
+/**
+ * Start activity for the VideoViewTest application.  This class manages the UI
+ * which allows a user to select a video to play back.
+ */
+public class VideoSelector extends Activity {
+    private ListView      mSelectList;
+    private VideoItemList mSelectItems;
+    private EditText      mUrlText;
+    private CheckBox      mTextureViewCheckbox;
+    private CheckBox      mLoopingCheckbox;
+    private CheckBox      mAdvertisementCheckBox;
+
+    private static final String TEST_VID_STASH = "/sdcard";
+    private static final int EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE = 100;
+
+    private Intent createLaunchIntent(Context ctx, String url) {
+        Intent ret_val = new Intent(ctx, VideoViewTest.class);
+        ret_val.setData(Uri.parse(url));
+        ret_val.putExtra(
+                VideoViewTest.LOOPING_EXTRA_NAME, mLoopingCheckbox.isChecked());
+        ret_val.putExtra(
+                VideoViewTest.USE_TEXTURE_VIEW_EXTRA_NAME, mTextureViewCheckbox.isChecked());
+        ret_val.putExtra(
+                VideoViewTest.MEDIA_TYPE_ADVERTISEMENT, mAdvertisementCheckBox.isChecked());
+        return ret_val;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.video_selector);
+
+        mSelectList  = (ListView) findViewById(R.id.select_list);
+        final Button playButton = (Button) findViewById(R.id.play_button);
+        mUrlText = (EditText) findViewById(R.id.video_selection_input);
+        mSelectItems = null;
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
+                    == PackageManager.PERMISSION_GRANTED) {
+                setUpInitialItemList();
+            } else {
+                requestPermissions(new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
+                        EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
+            }
+        } else {
+            setUpInitialItemList();
+        }
+
+        playButton.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                Intent launch = createLaunchIntent(
+                        VideoSelector.this,
+                        mUrlText.getText().toString());
+                startActivity(launch);
+            }
+        });
+        mLoopingCheckbox = findViewById(R.id.looping_checkbox);
+        mLoopingCheckbox.setChecked(false);
+        mTextureViewCheckbox = findViewById(R.id.use_textureview_checkbox);
+        mTextureViewCheckbox.setChecked(false);
+        mAdvertisementCheckBox = findViewById(R.id.media_type_advertisement);
+        mAdvertisementCheckBox.setChecked(false);
+    }
+
+    @Override
+    public void onBackPressed() {
+        if ((null != mSelectItems) && (null != mSelectHandler) && !mSelectItems.getIsRoot()) {
+            mSelectHandler.onItemClick(null, null, 0, 0);
+        } else {
+            finish();
+        }
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) {
+        if (requestCode == EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) {
+            if (results.length > 0 && results[0] == PackageManager.PERMISSION_GRANTED) {
+                setUpInitialItemList();
+            }
+        }
+    }
+
+    private void setUpInitialItemList() {
+        mSelectItems = createVil(TEST_VID_STASH);
+        mSelectList.setAdapter(mSelectItems);
+        mSelectList.setOnItemClickListener(mSelectHandler);
+    }
+
+    /**
+     * VideoItem is a class used to represent a selectable item on the listbox
+     * used to select videos to playback.
+     */
+    private static class VideoItem {
+        private final String mToStringName;
+        private final String mName;
+        private final String mUrl;
+        private final boolean mIsDir;
+
+        VideoItem(String name, String url, boolean isDir) {
+            mName = name;
+            mUrl  = url;
+            mIsDir = isDir;
+
+            if (isDir) {
+                mToStringName = String.format("[dir] %s", name);
+            } else {
+                int ndx = url.indexOf(':');
+                if (ndx > 0) {
+                    mToStringName = String.format("[%s] %s", url.substring(0, ndx), name);
+                } else {
+                    mToStringName = name;
+                }
+            }
+        }
+
+        public static VideoItem createFromLinkFile(File f) {
+            VideoItem retVal = null;
+
+            try {
+                BufferedReader rd = new BufferedReader(new FileReader(f));
+                String name = rd.readLine();
+                String url  = rd.readLine();
+                if ((null != name) && (null != url)) {
+                    retVal = new VideoItem(name, url, false);
+                }
+            } catch (FileNotFoundException e) {
+            } catch (IOException e) {
+            }
+
+            return retVal;
+        }
+
+        @Override
+        public String toString() {
+            return mToStringName;
+        }
+
+        public String getName() {
+            return mName;
+        }
+
+        public String getUrl() {
+            return mUrl;
+        }
+
+        public boolean getIsDir() {
+            return mIsDir;
+        }
+    }
+
+    private OnItemClickListener mSelectHandler = new OnItemClickListener() {
+        public void onItemClick(AdapterView parent,
+                                View v,
+                                int position,
+                                long id) {
+            if ((position >= 0) && (position < mSelectItems.getCount())) {
+                VideoItem item = mSelectItems.getItem(position);
+                if (item.getIsDir()) {
+                    VideoItemList new_list = createVil(item.getUrl());
+                    if (null != new_list) {
+                        mSelectItems = new_list;
+                        mSelectList.setAdapter(mSelectItems);
+                    }
+                } else {
+                    Intent launch = createLaunchIntent(
+                            VideoSelector.this,
+                            item.getUrl());
+                    startActivity(launch);
+                }
+            }
+        }
+    };
+
+    /**
+     * VideoItemList is an array adapter of video items used by the android
+     * framework to populate the list of videos to select.
+     */
+    private class VideoItemList extends ArrayAdapter<VideoItem> {
+        private final String mPath;
+        private final boolean mIsRoot;
+
+        private VideoItemList(String path, boolean isRoot) {
+            super(VideoSelector.this,
+                  R.layout.video_list_item,
+                  R.id.video_list_item);
+            mPath = path;
+            mIsRoot = isRoot;
+        }
+
+        public String getPath() {
+            return mPath;
+        }
+
+        public boolean getIsRoot() {
+            return mIsRoot;
+        }
+    };
+
+    private VideoItemList createVil(String p) {
+        boolean is_root = TEST_VID_STASH.equals(p);
+
+        File dir = new File(p);
+        if (!dir.isDirectory() || !dir.canRead()) {
+            return null;
+        }
+
+        VideoItemList retVal = new VideoItemList(p, is_root);
+
+        // If this is not the root directory, go ahead and add the back link to
+        // our parent.
+        if (!is_root) {
+            retVal.add(new VideoItem("..", dir.getParentFile().getAbsolutePath(), true));
+        }
+
+        // Make a sorted list of directories and files contained in this
+        // directory.
+        TreeMap<String, VideoItem> dirs  = new TreeMap<String, VideoItem>();
+        TreeMap<String, VideoItem> files = new TreeMap<String, VideoItem>();
+
+        File search_dir = new File(p);
+        File[] flist = search_dir.listFiles();
+        if (null == flist) {
+            return retVal;
+        }
+
+        for (File f : flist) {
+            if (f.canRead()) {
+                if (f.isFile()) {
+                    String fname = f.getName();
+                    VideoItem newItem = null;
+
+                    if (fname.endsWith(".url")) {
+                        newItem = VideoItem.createFromLinkFile(f);
+                    } else {
+                        String url = "file://" + f.getAbsolutePath();
+                        newItem = new VideoItem(fname, url, false);
+                    }
+
+                    if (null != newItem) {
+                        files.put(newItem.getName(), newItem);
+                    }
+                } else if (f.isDirectory()) {
+                    VideoItem newItem = new VideoItem(f.getName(), f.getAbsolutePath(), true);
+                    dirs.put(newItem.getName(), newItem);
+                }
+            }
+        }
+
+        // now add the the sorted directories to the result set.
+        for (VideoItem vi : dirs.values()) {
+            retVal.add(vi);
+        }
+
+        // finally add the the sorted files to the result set.
+        for (VideoItem vi : files.values()) {
+            retVal.add(vi);
+        }
+
+        return retVal;
+    }
+}
diff --git a/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoViewTest.java b/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoViewTest.java
new file mode 100644
index 0000000..f37033d
--- /dev/null
+++ b/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoViewTest.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.androidx.media;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import androidx.media.widget.MediaControlView2;
+import androidx.media.widget.VideoView2;
+
+/**
+ * Test application for VideoView2/MediaControlView2
+ */
+@SuppressLint("NewApi")
+public class VideoViewTest extends Activity {
+    public static final String LOOPING_EXTRA_NAME =
+            "com.example.androidx.media.VideoViewTest.IsLooping";
+    public static final String USE_TEXTURE_VIEW_EXTRA_NAME =
+            "com.example.androidx.media.VideoViewTest.UseTextureView";
+    public static final String MEDIA_TYPE_ADVERTISEMENT =
+            "com.example.androidx.media.VideoViewTest.MediaTypeAdvertisement";
+    private static final String TAG = "VideoViewTest";
+
+    private MyVideoView mVideoView = null;
+    private float mSpeed = 1.0f;
+
+    private MediaControlView2 mMediaControlView = null;
+
+    private boolean mUseTextureView = false;
+    private int mPrevWidth;
+    private int mPrevHeight;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        //Remove title bar
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+
+        setContentView(R.layout.video_activity);
+
+        mVideoView = findViewById(R.id.video_view);
+        mVideoView.setActivity(this);
+
+        String errorString = null;
+        Intent intent = getIntent();
+        Uri contentUri;
+        if (intent == null || (contentUri = intent.getData()) == null || !contentUri.isAbsolute()) {
+            errorString = "Invalid intent";
+        } else {
+            mUseTextureView = intent.getBooleanExtra(USE_TEXTURE_VIEW_EXTRA_NAME, false);
+            if (mUseTextureView) {
+                mVideoView.setViewType(VideoView2.VIEW_TYPE_TEXTUREVIEW);
+            }
+
+            mVideoView.setFullScreenRequestListener(new FullScreenRequestListener());
+            mVideoView.setVideoUri(contentUri);
+
+            mMediaControlView = new MediaControlView2(this);
+            mVideoView.setMediaControlView2(mMediaControlView, 2000);
+        }
+        if (errorString != null) {
+            showErrorDialog(errorString);
+        }
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        if (mVideoView.isAttachedToWindow()) {
+            mVideoView.getMediaController().getTransportControls().play();
+            mVideoView.getMediaController().registerCallback(mMediaControllerCallback);
+        } else {
+            mVideoView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+                @Override
+                public void onViewAttachedToWindow(View v) {
+                    mVideoView.getMediaController().getTransportControls().play();
+                    mVideoView.getMediaController().registerCallback(mMediaControllerCallback);
+                }
+
+                @Override
+                public void onViewDetachedFromWindow(View v) {
+                    // No need to remove callback here since MediaSession has already been
+                    // destroyed.
+                }
+            });
+        }
+        setTitle(getViewTypeString(mVideoView));
+    }
+
+    @Override
+    protected void onPause() {
+        mVideoView.getMediaController().getTransportControls().pause();
+        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        super.onPause();
+    }
+
+    @Override
+    protected void onDestroy() {
+        Log.d(TAG, "onDestroy");
+        mVideoView.getMediaController().unregisterCallback(mMediaControllerCallback);
+        mVideoView.getMediaController().getTransportControls().stop();
+        super.onDestroy();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            int screenWidth = getResources().getDisplayMetrics().widthPixels;
+            if (ev.getRawX() < (screenWidth / 2.0f)) {
+                // TODO: getSpeed() not needed?
+                mSpeed -= 0.1f;
+            } else {
+                mSpeed += 0.1f;
+            }
+            mVideoView.setSpeed(mSpeed);
+            Toast.makeText(this, "speed rate: " + String.format("%.2f", mSpeed), Toast.LENGTH_SHORT)
+                    .show();
+        }
+        return super.onTouchEvent(ev);
+    }
+
+    private void showErrorDialog(String errorMessage) {
+        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        builder.setTitle("Playback error")
+                .setMessage(errorMessage)
+                .setCancelable(false)
+                .setPositiveButton("OK",
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialogInterface, int i) {
+                                finish();
+                            }
+                        }).show();
+    }
+
+    MediaControllerCompat.Callback mMediaControllerCallback = new MediaControllerCompat.Callback() {
+        @Override
+        public void onPlaybackStateChanged(PlaybackStateCompat state) {
+            switch (state.getState()) {
+                case PlaybackStateCompat.STATE_STOPPED:
+                    getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+                    break;
+                case PlaybackStateCompat.STATE_ERROR:
+                    showErrorDialog("Error: (" + state.getErrorMessage() + ")");
+                    break;
+            }
+        }
+    };
+
+    private class FullScreenRequestListener implements VideoView2.OnFullScreenRequestListener {
+        @Override
+        public void onFullScreenRequest(View view, boolean fullScreen) {
+            // TODO: Remove bottom controls after adding back button functionality.
+            if (mPrevHeight == 0 && mPrevWidth == 0) {
+                ViewGroup.LayoutParams params = mVideoView.getLayoutParams();
+                mPrevWidth = params.width;
+                mPrevHeight = params.height;
+            }
+
+            if (fullScreen) {
+                // Remove notification bar
+                getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+                        WindowManager.LayoutParams.FLAG_FULLSCREEN);
+
+                ViewGroup.LayoutParams params = mVideoView.getLayoutParams();
+                params.width = ViewGroup.LayoutParams.MATCH_PARENT;
+                params.height = ViewGroup.LayoutParams.MATCH_PARENT;
+                mVideoView.setLayoutParams(params);
+            } else {
+                // Restore notification bar
+                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+
+                ViewGroup.LayoutParams params = mVideoView.getLayoutParams();
+                params.width = mPrevWidth;
+                params.height = mPrevHeight;
+                mVideoView.setLayoutParams(params);
+            }
+            mVideoView.requestLayout();
+            mVideoView.invalidate();
+        }
+    }
+
+    /**
+     * Extension of the stock android video view used to hook and override
+     * keypress behavior.  Mainly used to make sure that certain keystrokes
+     * don't automatically bring up the andriod MediaController widget (which
+     * then steals focus)
+     *
+     * @author johngro@google.com (John Grossman)
+     */
+    public static class MyVideoView extends VideoView2 {
+        private float mDX;
+        private float mDY;
+        private Activity mActivity;
+
+        public MyVideoView(Context context) {
+            super(context);
+        }
+
+        public MyVideoView(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        public MyVideoView(Context context, AttributeSet attrs, int defStyle) {
+            super(context, attrs, defStyle);
+        }
+
+        @Override
+        public boolean onTouchEvent(MotionEvent ev) {
+            switch (ev.getAction()) {
+                case MotionEvent.ACTION_DOWN:
+                    mDX = ev.getRawX() - getX();
+                    mDY = ev.getRawY() - getY();
+                    super.onTouchEvent(ev);
+                    return true;
+                case MotionEvent.ACTION_MOVE:
+                    animate()
+                            .x(ev.getRawX() - mDX)
+                            .y(ev.getRawY() - mDY)
+                            .setDuration(0)
+                            .start();
+                    super.onTouchEvent(ev);
+                    return true;
+            }
+            return super.onTouchEvent(ev);
+        }
+
+        @Override
+        public boolean onKeyDown(int keyCode, KeyEvent event)  {
+            if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
+                mActivity.finish();
+            }
+            return true;
+        }
+
+        public void setActivity(Activity activity) {
+            mActivity = activity;
+        }
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        if (mVideoView.getViewType() == VideoView2.VIEW_TYPE_SURFACEVIEW) {
+            mVideoView.setViewType(VideoView2.VIEW_TYPE_TEXTUREVIEW);
+            Toast.makeText(this, "switch to TextureView", Toast.LENGTH_SHORT).show();
+            setTitle(getViewTypeString(mVideoView));
+        } else if (mVideoView.getViewType() == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
+            mVideoView.setViewType(VideoView2.VIEW_TYPE_SURFACEVIEW);
+            Toast.makeText(this, "switch to SurfaceView", Toast.LENGTH_SHORT).show();
+            setTitle(getViewTypeString(mVideoView));
+        }
+    }
+
+    private String getViewTypeString(VideoView2 videoView) {
+        if (videoView == null) {
+            return "Unknown";
+        }
+        int type = videoView.getViewType();
+        if (type == VideoView2.VIEW_TYPE_SURFACEVIEW) {
+            return "SurfaceView";
+        } else if (type == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
+            return "TextureView";
+        }
+        return "Unknown";
+    }
+}
diff --git a/samples/SupportMediaDemos/src/main/res/layout/video_activity.xml b/samples/SupportMediaDemos/src/main/res/layout/video_activity.xml
new file mode 100644
index 0000000..94784d4
--- /dev/null
+++ b/samples/SupportMediaDemos/src/main/res/layout/video_activity.xml
@@ -0,0 +1,141 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:widget="http://schemas.android.com/apk/com.android.media.update"
+    android:id="@+id/video_player_main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#3F51B5">
+
+    <!-- MediaControlView will be created manually in code -->
+    <view
+        android:id="@+id/video_view"
+        class="com.example.androidx.media.VideoViewTest$MyVideoView"
+        android:layout_width="250dp"
+        android:layout_height="250dp"
+        android:layout_centerInParent="true"
+        widget:enableControlView="false" />
+
+    <!-- Video Status -->
+    <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/vid_status"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentLeft="true"
+        android:layout_alignParentTop="true"
+        android:visibility="invisible">
+
+        <TableRow>
+
+            <TextView
+                style="@style/status_tag"
+                android:text="@string/vid_status_codec_tag" />
+
+            <TextView
+                android:id="@+id/vid_codec"
+                style="@style/status_value" />
+        </TableRow>
+
+        <TableRow>
+
+            <TextView
+                style="@style/status_tag"
+                android:text="@string/vid_status_bitrate_tag" />
+
+            <TextView
+                android:id="@+id/vid_bitrate"
+                style="@style/status_value" />
+        </TableRow>
+
+        <TableRow>
+
+            <TextView
+                style="@style/status_tag"
+                android:text="@string/vid_status_mode_tag" />
+
+            <TextView
+                android:id="@+id/vid_mode"
+                style="@style/status_value" />
+        </TableRow>
+
+        <TableRow>
+
+            <TextView
+                style="@style/status_tag"
+                android:text="@string/vid_status_outrect_tag" />
+
+            <TextView
+                android:id="@+id/vid_outrect"
+                style="@style/status_value" />
+        </TableRow>
+
+        <TableRow>
+
+            <TextView
+                style="@style/status_tag"
+                android:text="@string/vid_status_stereo_mode_tag" />
+
+            <TextView
+                android:id="@+id/vid_stereo_mode"
+                style="@style/status_value" />
+        </TableRow>
+    </TableLayout>
+
+    <!-- Audio Status -->
+    <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/aud_status"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentRight="true"
+        android:layout_alignParentTop="true"
+        android:visibility="invisible">
+
+        <TableRow>
+
+            <TextView
+                style="@style/status_tag"
+                android:text="@string/aud_status_codec_tag" />
+
+            <TextView
+                android:id="@+id/aud_codec"
+                style="@style/status_value" />
+        </TableRow>
+
+        <TableRow>
+
+            <TextView
+                style="@style/status_tag"
+                android:text="@string/aud_status_bitrate_tag" />
+
+            <TextView
+                android:id="@+id/aud_bitrate"
+                style="@style/status_value" />
+        </TableRow>
+
+        <TableRow>
+
+            <TextView
+                style="@style/status_tag"
+                android:text="@string/aud_status_mode_tag" />
+
+            <TextView
+                android:id="@+id/aud_mode"
+                style="@style/status_value" />
+        </TableRow>
+    </TableLayout>
+</RelativeLayout>
diff --git a/samples/SupportMediaDemos/src/main/res/layout/video_list_item.xml b/samples/SupportMediaDemos/src/main/res/layout/video_list_item.xml
new file mode 100644
index 0000000..c4e6045
--- /dev/null
+++ b/samples/SupportMediaDemos/src/main/res/layout/video_list_item.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/video_list_item"
+    style="@style/video_list_item_text"
+    android:padding="12sp"
+/>
diff --git a/samples/SupportMediaDemos/src/main/res/layout/video_selector.xml b/samples/SupportMediaDemos/src/main/res/layout/video_selector.xml
new file mode 100644
index 0000000..485090f
--- /dev/null
+++ b/samples/SupportMediaDemos/src/main/res/layout/video_selector.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/video_selector_main"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical"
+    android:padding="16sp">
+
+    <TextView
+        android:id="@+id/title_text"
+        style="@style/title_text"
+        android:layout_alignParentTop="true"
+        android:text="@string/select_video_prompt" />
+
+    <CheckBox
+        android:id="@+id/looping_checkbox"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentRight="true"
+        android:layout_below="@id/title_text"
+        android:text="@string/looping_text" />
+
+    <CheckBox
+        android:id="@+id/use_textureview_checkbox"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/title_text"
+        android:layout_toLeftOf="@id/looping_checkbox"
+        android:text="@string/texture_view_text" />
+
+    <CheckBox
+        android:id="@+id/media_type_advertisement"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/title_text"
+        android:layout_toLeftOf="@id/use_textureview_checkbox"
+        android:text="@string/advertisement_text" />
+
+    <Button
+        android:id="@+id/play_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentRight="true"
+        android:text="@string/play_button_text" />
+
+    <EditText
+        android:id="@+id/video_selection_input"
+        android:layout_width="400sp"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentLeft="true"
+        android:layout_toLeftOf="@id/play_button"
+        android:editable="true" />
+
+    <ListView
+        android:id="@+id/select_list"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:layout_above="@id/video_selection_input"
+        android:layout_below="@id/looping_checkbox" />
+</RelativeLayout>
diff --git a/samples/SupportMediaDemos/src/main/res/values/strings.xml b/samples/SupportMediaDemos/src/main/res/values/strings.xml
new file mode 100644
index 0000000..ba49f42
--- /dev/null
+++ b/samples/SupportMediaDemos/src/main/res/values/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="vid_status_codec_tag">Video Codec:</string>
+    <string name="vid_status_bitrate_tag">Avg. Bitrate:</string>
+    <string name="vid_status_mode_tag">Video Mode:</string>
+    <string name="vid_status_outrect_tag">Dest Rect:</string>
+    <string name="aud_status_codec_tag">Audio Codec:</string>
+    <string name="vid_status_stereo_mode_tag">Stereo Mode:</string>
+    <string name="aud_status_bitrate_tag">Bitrate:</string>
+    <string name="aud_status_mode_tag">Audio Mode:</string>
+    <string name="buf_status_fmt_string">Buffering: %d%%</string>
+    <string name="select_video_prompt">Select Video</string>
+    <string name="select_button_text">Select</string>
+    <string name="play_button_text">Play</string>
+    <string name="looping_text">Looping</string>
+    <string name="texture_view_text">Use TextureView</string>
+    <string name="advertisement_text">Advertisement</string>
+</resources>
diff --git a/samples/SupportMediaDemos/src/main/res/values/style.xml b/samples/SupportMediaDemos/src/main/res/values/style.xml
new file mode 100644
index 0000000..27ddd23
--- /dev/null
+++ b/samples/SupportMediaDemos/src/main/res/values/style.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+    <style name="basic_text">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+    </style>
+
+    <style name="title_text" parent="@style/basic_text">
+        <item name="android:textSize">24sp</item>
+    </style>
+
+    <style name="video_list_item_text" parent="@style/basic_text">
+        <item name="android:textSize">24sp</item>
+    </style>
+
+    <style name="status_text" parent="@style/basic_text">
+        <item name="android:textSize">24sp</item>
+    </style>
+    <style name="status_value" parent="@style/status_text">
+        <item name="android:gravity">left</item>
+    </style>
+    <style name="status_tag" parent="@style/status_text">
+        <item name="android:paddingRight">5dp</item>
+        <item name="android:gravity">right</item>
+    </style>
+</resources>
+
diff --git a/samples/SupportSliceDemos/src/main/AndroidManifest.xml b/samples/SupportSliceDemos/src/main/AndroidManifest.xml
index 355a77a..4d43211 100644
--- a/samples/SupportSliceDemos/src/main/AndroidManifest.xml
+++ b/samples/SupportSliceDemos/src/main/AndroidManifest.xml
@@ -48,6 +48,7 @@
 
         <provider android:authorities="com.example.androidx.slice.demos"
                   android:name=".SampleSliceProvider"
+                  android:exported="true"
                   android:grantUriPermissions="true">
             <intent-filter>
                 <action android:name="androidx.intent.SLICE_ACTION"/>
diff --git a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java
index f1e2976..622f047 100644
--- a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java
+++ b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java
@@ -24,7 +24,6 @@
 import android.content.ContentResolver;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.database.Cursor;
@@ -133,7 +132,6 @@
             mSearchView.setQuery(savedInstanceState.getString("SELECTED_QUERY"), true);
         }
 
-        grantPackage(getPackageName());
         // TODO: Listen for changes.
         updateAvailableSlices();
         if (TEST_INTENT) {
@@ -149,7 +147,6 @@
         mTypeMenu.add("Shortcut");
         mTypeMenu.add("Small");
         mTypeMenu.add("Large");
-        menu.add("Auth");
         super.onCreateOptionsMenu(menu);
         return true;
     }
@@ -157,9 +154,6 @@
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getTitle().toString()) {
-            case "Auth":
-                authAllSlices();
-                return true;
             case "Shortcut":
                 mTypeMenu.setIcon(R.drawable.ic_shortcut);
                 mSelectedMode = SliceView.MODE_SHORTCUT;
@@ -186,21 +180,6 @@
         outState.putString("SELECTED_QUERY", mSearchView.getQuery().toString());
     }
 
-    private void authAllSlices() {
-        List<ApplicationInfo> packages = getPackageManager().getInstalledApplications(0);
-        for (ApplicationInfo info : packages) {
-            grantPackage(info.packageName);
-        }
-    }
-
-    private void grantPackage(String packageName) {
-        for (int i = 0; i < URI_PATHS.length; i++) {
-            grantUriPermission(packageName, getUri(URI_PATHS[i], getApplicationContext()),
-                    Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
-                            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
-        }
-    }
-
     private void updateAvailableSlices() {
         mSliceUris.clear();
         List<PackageInfo> packageInfos = getPackageManager()
diff --git a/settings.gradle b/settings.gradle
index 16e5be7..d969d88 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -68,6 +68,7 @@
 includeProject(":loader", "loader")
 includeProject(":localbroadcastmanager", "localbroadcastmanager")
 includeProject(":media", "media")
+includeProject(":media-widget", "media-widget")
 includeProject(":mediarouter", "mediarouter")
 includeProject(":palette", "palette")
 includeProject(":palette-ktx", "palette/ktx")
@@ -122,6 +123,7 @@
 includeProject(":support-emoji-demos", new File(samplesRoot, "SupportEmojiDemos"))
 includeProject(":support-leanback-demos", new File(samplesRoot, "SupportLeanbackDemos"))
 //includeProject(":support-leanback-jank", new File(samplesRoot, "SupportLeanbackJank"))
+includeProject(":support-media-demos", new File(samplesRoot, "SupportMediaDemos"))
 includeProject(":support-percent-demos", new File(samplesRoot, "SupportPercentDemos"))
 includeProject(":support-preference-demos", new File(samplesRoot, "SupportPreferenceDemos"))
 includeProject(":support-slices-demos", new File(samplesRoot, "SupportSliceDemos"))
diff --git a/slices/core/src/androidTest/java/androidx/slice/SliceTest.java b/slices/core/src/androidTest/java/androidx/slice/SliceTest.java
index 041c8fe..e092c97 100644
--- a/slices/core/src/androidTest/java/androidx/slice/SliceTest.java
+++ b/slices/core/src/androidTest/java/androidx/slice/SliceTest.java
@@ -23,9 +23,9 @@
 import static android.app.slice.SliceItem.FORMAT_ACTION;
 import static android.app.slice.SliceItem.FORMAT_IMAGE;
 import static android.app.slice.SliceItem.FORMAT_INT;
+import static android.app.slice.SliceItem.FORMAT_LONG;
 import static android.app.slice.SliceItem.FORMAT_SLICE;
 import static android.app.slice.SliceItem.FORMAT_TEXT;
-import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
 import static android.app.slice.SliceProvider.SLICE_TYPE;
 
 import static org.junit.Assert.assertEquals;
@@ -184,7 +184,7 @@
         assertEquals(1, s.getItems().size());
 
         SliceItem item = s.getItems().get(0);
-        assertEquals(FORMAT_TIMESTAMP, item.getFormat());
+        assertEquals(FORMAT_LONG, item.getFormat());
         assertEquals(43, item.getTimestamp());
     }
 
diff --git a/slices/core/src/main/java/androidx/slice/Slice.java b/slices/core/src/main/java/androidx/slice/Slice.java
index a2c3325..aa077cb 100644
--- a/slices/core/src/main/java/androidx/slice/Slice.java
+++ b/slices/core/src/main/java/androidx/slice/Slice.java
@@ -35,7 +35,6 @@
 import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
 import static android.app.slice.SliceItem.FORMAT_SLICE;
 import static android.app.slice.SliceItem.FORMAT_TEXT;
-import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
 
 import static androidx.slice.SliceConvert.unwrap;
 import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
@@ -388,7 +387,7 @@
         }
 
         /**
-         * Add a timestamp to the slice being constructed
+         * Add a long to the slice being constructed
          * @param subType Optional template-specific type information
          * @see {@link SliceItem#getSubType()}
          */
@@ -399,6 +398,16 @@
         }
 
         /**
+         * Add a long to the slice being constructed
+         * @param subType Optional template-specific type information
+         * @see {@link SliceItem#getSubType()}
+         */
+        public Slice.Builder addLong(long time, @Nullable String subType,
+                @SliceHint List<String> hints) {
+            return addLong(time, subType, hints.toArray(new String[hints.size()]));
+        }
+
+        /**
          * Add a timestamp to the slice being constructed
          * @param subType Optional template-specific type information
          * @see {@link SliceItem#getSubType()}
@@ -407,7 +416,7 @@
         @Deprecated
         public Slice.Builder addTimestamp(long time, @Nullable String subType,
                 @SliceHint String... hints) {
-            mItems.add(new SliceItem(time, FORMAT_TIMESTAMP, subType, hints));
+            mItems.add(new SliceItem(time, FORMAT_LONG, subType, hints));
             return this;
         }
 
@@ -473,7 +482,7 @@
      */
     @RestrictTo(Scope.LIBRARY)
     public static void addHints(StringBuilder sb, String[] hints) {
-        if (hints.length == 0) return;
+        if (hints == null || hints.length == 0) return;
 
         sb.append("(");
         int end = hints.length - 1;
@@ -510,6 +519,6 @@
     private static Slice callBindSlice(Context context, Uri uri,
             Set<SliceSpec> supportedSpecs) {
         return SliceConvert.wrap(context.getSystemService(SliceManager.class)
-                .bindSlice(uri, new ArrayList<>(unwrap(supportedSpecs))));
+                .bindSlice(uri, unwrap(supportedSpecs)));
     }
 }
diff --git a/slices/core/src/main/java/androidx/slice/SliceConvert.java b/slices/core/src/main/java/androidx/slice/SliceConvert.java
index 73ff368..fea5b4c 100644
--- a/slices/core/src/main/java/androidx/slice/SliceConvert.java
+++ b/slices/core/src/main/java/androidx/slice/SliceConvert.java
@@ -19,17 +19,16 @@
 import static android.app.slice.SliceItem.FORMAT_ACTION;
 import static android.app.slice.SliceItem.FORMAT_IMAGE;
 import static android.app.slice.SliceItem.FORMAT_INT;
+import static android.app.slice.SliceItem.FORMAT_LONG;
 import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
 import static android.app.slice.SliceItem.FORMAT_SLICE;
 import static android.app.slice.SliceItem.FORMAT_TEXT;
-import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
 
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.collection.ArraySet;
 import androidx.core.graphics.drawable.IconCompat;
 
-import java.util.List;
 import java.util.Set;
 
 /**
@@ -43,9 +42,8 @@
      */
     public static android.app.slice.Slice unwrap(androidx.slice.Slice slice) {
         android.app.slice.Slice.Builder builder = new android.app.slice.Slice.Builder(
-                slice.getUri());
+                slice.getUri(), unwrap(slice.getSpec()));
         builder.addHints(slice.getHints());
-        builder.setSpec(unwrap(slice.getSpec()));
         for (androidx.slice.SliceItem item : slice.getItems()) {
             switch (item.getFormat()) {
                 case FORMAT_SLICE:
@@ -67,8 +65,8 @@
                 case FORMAT_INT:
                     builder.addInt(item.getInt(), item.getSubType(), item.getHints());
                     break;
-                case FORMAT_TIMESTAMP:
-                    builder.addTimestamp(item.getTimestamp(), item.getSubType(), item.getHints());
+                case FORMAT_LONG:
+                    builder.addLong(item.getLong(), item.getSubType(), item.getHints());
                     break;
             }
         }
@@ -119,8 +117,8 @@
                 case FORMAT_INT:
                     builder.addInt(item.getInt(), item.getSubType(), item.getHints());
                     break;
-                case FORMAT_TIMESTAMP:
-                    builder.addTimestamp(item.getTimestamp(), item.getSubType(), item.getHints());
+                case FORMAT_LONG:
+                    builder.addLong(item.getLong(), item.getSubType(), item.getHints());
                     break;
             }
         }
@@ -137,7 +135,7 @@
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     public static Set<androidx.slice.SliceSpec> wrap(
-            List<android.app.slice.SliceSpec> supportedSpecs) {
+            Set<android.app.slice.SliceSpec> supportedSpecs) {
         Set<androidx.slice.SliceSpec> ret = new ArraySet<>();
         for (android.app.slice.SliceSpec spec : supportedSpecs) {
             ret.add(wrap(spec));
diff --git a/slices/core/src/main/java/androidx/slice/SliceItem.java b/slices/core/src/main/java/androidx/slice/SliceItem.java
index 964c5df..f6ae848 100644
--- a/slices/core/src/main/java/androidx/slice/SliceItem.java
+++ b/slices/core/src/main/java/androidx/slice/SliceItem.java
@@ -23,7 +23,6 @@
 import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
 import static android.app.slice.SliceItem.FORMAT_SLICE;
 import static android.app.slice.SliceItem.FORMAT_TEXT;
-import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
 
 import static androidx.slice.Slice.addHints;
 
@@ -59,7 +58,7 @@
  * <li>{@link android.app.slice.SliceItem#FORMAT_IMAGE}</li>
  * <li>{@link android.app.slice.SliceItem#FORMAT_ACTION}</li>
  * <li>{@link android.app.slice.SliceItem#FORMAT_INT}</li>
- * <li>{@link android.app.slice.SliceItem#FORMAT_TIMESTAMP}</li>
+ * <li>{@link android.app.slice.SliceItem#FORMAT_LONG}</li>
  * <p>
  * The hints that a {@link SliceItem} are a set of strings which annotate
  * the content. The hints that are guaranteed to be understood by the system
@@ -78,7 +77,7 @@
      */
     @RestrictTo(Scope.LIBRARY)
     @StringDef({FORMAT_SLICE, FORMAT_TEXT, FORMAT_IMAGE, FORMAT_ACTION, FORMAT_INT,
-            FORMAT_TIMESTAMP, FORMAT_REMOTE_INPUT, FORMAT_LONG})
+            FORMAT_LONG, FORMAT_REMOTE_INPUT, FORMAT_LONG})
     public @interface SliceType {
     }
 
@@ -164,7 +163,7 @@
      * <li>{@link android.app.slice.SliceItem#FORMAT_IMAGE}</li>
      * <li>{@link android.app.slice.SliceItem#FORMAT_ACTION}</li>
      * <li>{@link android.app.slice.SliceItem#FORMAT_INT}</li>
-     * <li>{@link android.app.slice.SliceItem#FORMAT_TIMESTAMP}</li>
+     * <li>{@link android.app.slice.SliceItem#FORMAT_LONG}</li>
      * <li>{@link android.app.slice.SliceItem#FORMAT_REMOTE_INPUT}</li>
      * @see #getSubType() ()
      */
@@ -347,7 +346,7 @@
             case FORMAT_INT:
                 dest.putInt(OBJ, (Integer) mObj);
                 break;
-            case FORMAT_TIMESTAMP:
+            case FORMAT_LONG:
                 dest.putLong(OBJ, (Long) mObj);
                 break;
         }
@@ -369,7 +368,7 @@
                         new Slice(in.getBundle(OBJ_2)));
             case FORMAT_INT:
                 return in.getInt(OBJ);
-            case FORMAT_TIMESTAMP:
+            case FORMAT_LONG:
                 return in.getLong(OBJ);
         }
         throw new RuntimeException("Unsupported type " + type);
@@ -391,8 +390,8 @@
                 return "Action";
             case FORMAT_INT:
                 return "Int";
-            case FORMAT_TIMESTAMP:
-                return "Timestamp";
+            case FORMAT_LONG:
+                return "Long";
             case FORMAT_REMOTE_INPUT:
                 return "RemoteInput";
         }
diff --git a/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java b/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java
index 13f0c54..da602f5 100644
--- a/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java
+++ b/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java
@@ -28,6 +28,8 @@
 import androidx.core.util.ObjectsCompat;
 import androidx.slice.SliceSpec;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -70,6 +72,22 @@
         return prefs;
     }
 
+    /**
+     * Get pinned specs
+     */
+    public List<Uri> getPinnedSlices() {
+        List<Uri> pinned = new ArrayList<>();
+        for (String key : getPrefs().getAll().keySet()) {
+            if (key.startsWith(PIN_PREFIX)) {
+                Uri uri = Uri.parse(key.substring(PIN_PREFIX.length()));
+                if (!getPins(uri).isEmpty()) {
+                    pinned.add(uri);
+                }
+            }
+        }
+        return pinned;
+    }
+
     private Set<String> getPins(Uri uri) {
         return getPrefs().getStringSet(PIN_PREFIX + uri.toString(), new ArraySet<String>());
     }
diff --git a/slices/core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java b/slices/core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
index 5feaa1e..5fa33ae 100644
--- a/slices/core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
+++ b/slices/core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
@@ -20,15 +20,20 @@
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
 import android.content.DialogInterface.OnDismissListener;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.net.Uri;
 import android.os.Bundle;
+import android.text.Html;
+import android.text.TextPaint;
+import android.text.TextUtils;
 import android.util.Log;
 import android.widget.TextView;
 
 import androidx.annotation.RestrictTo;
 import androidx.appcompat.app.AlertDialog;
+import androidx.core.text.BidiFormatter;
 import androidx.slice.core.R;
 
 /**
@@ -39,6 +44,8 @@
 public class SlicePermissionActivity extends Activity implements OnClickListener,
         OnDismissListener {
 
+    private static final float MAX_LABEL_SIZE_PX = 500f;
+
     private static final String TAG = "SlicePermissionActivity";
 
     private Uri mUri;
@@ -55,8 +62,12 @@
 
         try {
             PackageManager pm = getPackageManager();
-            CharSequence app1 = pm.getApplicationInfo(mCallingPkg, 0).loadLabel(pm);
-            CharSequence app2 = pm.getApplicationInfo(mProviderPkg, 0).loadLabel(pm);
+            CharSequence app1 = BidiFormatter.getInstance().unicodeWrap(
+                    loadSafeLabel(pm, pm.getApplicationInfo(mCallingPkg, 0))
+                    .toString());
+            CharSequence app2 = BidiFormatter.getInstance().unicodeWrap(
+                    loadSafeLabel(pm, pm.getApplicationInfo(mProviderPkg, 0))
+                    .toString());
             AlertDialog dialog = new AlertDialog.Builder(this)
                     .setTitle(getString(R.string.abc_slice_permission_title, app1, app2))
                     .setView(R.layout.abc_slice_permission_request)
@@ -74,6 +85,45 @@
         }
     }
 
+    // Based on loadSafeLabel in PackageitemInfo
+    private CharSequence loadSafeLabel(PackageManager pm, ApplicationInfo appInfo) {
+        // loadLabel() always returns non-null
+        String label = appInfo.loadLabel(pm).toString();
+        // strip HTML tags to avoid <br> and other tags overwriting original message
+        String labelStr = Html.fromHtml(label).toString();
+
+        // If the label contains new line characters it may push the UI
+        // down to hide a part of it. Labels shouldn't have new line
+        // characters, so just truncate at the first time one is seen.
+        final int labelLength = labelStr.length();
+        int offset = 0;
+        while (offset < labelLength) {
+            final int codePoint = labelStr.codePointAt(offset);
+            final int type = Character.getType(codePoint);
+            if (type == Character.LINE_SEPARATOR
+                    || type == Character.CONTROL
+                    || type == Character.PARAGRAPH_SEPARATOR) {
+                labelStr = labelStr.substring(0, offset);
+                break;
+            }
+            // replace all non-break space to " " in order to be trimmed
+            if (type == Character.SPACE_SEPARATOR) {
+                labelStr = labelStr.substring(0, offset) + " " + labelStr.substring(offset
+                        + Character.charCount(codePoint));
+            }
+            offset += Character.charCount(codePoint);
+        }
+
+        labelStr = labelStr.trim();
+        if (labelStr.isEmpty()) {
+            return appInfo.packageName;
+        }
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(42);
+
+        return TextUtils.ellipsize(labelStr, paint, MAX_LABEL_SIZE_PX, TextUtils.TruncateAt.END);
+    }
+
     @Override
     public void onClick(DialogInterface dialog, int which) {
         if (which == DialogInterface.BUTTON_POSITIVE) {
diff --git a/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java b/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
index 2d06d8c..d62e9b5 100644
--- a/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
+++ b/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
@@ -25,6 +25,7 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
@@ -62,6 +63,7 @@
     private static final String TAG = "SliceProviderCompat";
     private static final String DATA_PREFIX = "slice_data_";
     private static final String PERMS_PREFIX = "slice_perms_";
+    private static final String ALL_FILES = DATA_PREFIX + "all_slice_files";
 
     private static final long SLICE_BIND_ANR = 2000;
 
@@ -97,8 +99,18 @@
 
     public SliceProviderCompat(SliceProvider provider) {
         mProvider = provider;
-        mPinnedList = new CompatPinnedList(provider.getContext(),
-                DATA_PREFIX + getClass().getName());
+        String prefsFile = DATA_PREFIX + getClass().getName();
+        SharedPreferences allFiles = provider.getContext().getSharedPreferences(ALL_FILES, 0);
+        Set<String> files = allFiles.getStringSet(ALL_FILES, Collections.<String>emptySet());
+        if (!files.contains(prefsFile)) {
+            // Make sure this is editable.
+            files = new ArraySet<>(files);
+            files.add(prefsFile);
+            allFiles.edit()
+                    .putStringSet(ALL_FILES, files)
+                    .commit();
+        }
+        mPinnedList = new CompatPinnedList(provider.getContext(), prefsFile);
         mPermissionManager = new CompatPermissionManager(provider.getContext(),
                 PERMS_PREFIX + getClass().getName());
     }
@@ -117,12 +129,6 @@
     public Bundle call(String method, String arg, Bundle extras) {
         if (method.equals(METHOD_SLICE)) {
             Uri uri = extras.getParcelable(EXTRA_BIND_URI);
-            if (Binder.getCallingUid() != Process.myUid()) {
-                getContext().enforceUriPermission(uri, Binder.getCallingPid(),
-                        Binder.getCallingUid(),
-                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
-                        "Slice binding requires write access to the uri");
-            }
             Set<SliceSpec> specs = getSpecs(extras);
 
             Slice s = handleBindSlice(uri, specs, getCallingPackage());
@@ -606,4 +612,17 @@
             Log.e(TAG, "Unable to get slice descendants", e);
         }
     }
+
+    /**
+     * Compat version of {@link android.app.slice.SliceManager#getPinnedSlices}.
+     */
+    public static List<Uri> getPinnedSlices(Context context) {
+        ArrayList<Uri> pinnedSlices = new ArrayList<>();
+        SharedPreferences prefs = context.getSharedPreferences(ALL_FILES, 0);
+        Set<String> prefSet = prefs.getStringSet(ALL_FILES, Collections.<String>emptySet());
+        for (String pref : prefSet) {
+            pinnedSlices.addAll(new CompatPinnedList(context, pref).getPinnedSlices());
+        }
+        return pinnedSlices;
+    }
 }
diff --git a/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java b/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
index 8641530..c35793e 100644
--- a/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
+++ b/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
@@ -29,11 +29,10 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
-import androidx.collection.ArraySet;
 import androidx.slice.SliceConvert;
 
 import java.util.Collection;
-import java.util.List;
+import java.util.Set;
 
 /**
  * @hide
@@ -64,8 +63,8 @@
         }
 
         @Override
-        public Slice onBindSlice(Uri sliceUri, List<SliceSpec> supportedVersions) {
-            androidx.slice.SliceProvider.setSpecs(new ArraySet<>(wrap(supportedVersions)));
+        public Slice onBindSlice(Uri sliceUri, Set<SliceSpec> supportedVersions) {
+            androidx.slice.SliceProvider.setSpecs(wrap(supportedVersions));
             try {
                 return SliceConvert.unwrap(mSliceProvider.onBindSlice(sliceUri));
             } finally {
diff --git a/slices/view/api/current.txt b/slices/view/api/current.txt
index 5a37826..aa95b81 100644
--- a/slices/view/api/current.txt
+++ b/slices/view/api/current.txt
@@ -5,6 +5,8 @@
     method public abstract androidx.slice.Slice bindSlice(android.content.Intent);
     method public abstract int checkSlicePermission(android.net.Uri, int, int);
     method public static androidx.slice.SliceManager getInstance(android.content.Context);
+    method public abstract java.util.List<android.net.Uri> getPinnedSlices();
+    method public abstract java.util.Set<androidx.slice.SliceSpec> getPinnedSpecs(android.net.Uri);
     method public abstract java.util.Collection<android.net.Uri> getSliceDescendants(android.net.Uri);
     method public abstract void grantSlicePermission(java.lang.String, android.net.Uri);
     method public abstract android.net.Uri mapIntentToUri(android.content.Intent);
diff --git a/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java b/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java
index b54335d..465db2b 100644
--- a/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java
+++ b/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java
@@ -17,6 +17,7 @@
 package androidx.slice;
 
 import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
@@ -44,6 +45,7 @@
 
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
 import java.util.concurrent.Executor;
 
 @RunWith(AndroidJUnit4.class)
@@ -88,6 +90,28 @@
     }
 
     @Test
+    public void testPinList() {
+        Uri uri = new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(mContext.getPackageName())
+                .build();
+        Uri longerUri = uri.buildUpon().appendPath("something").build();
+        try {
+            mManager.pinSlice(uri);
+            mManager.pinSlice(longerUri);
+            verify(mSliceProvider, timeout(2000)).onSlicePinned(eq(longerUri));
+
+            List<Uri> uris = mManager.getPinnedSlices();
+            assertEquals(2, uris.size());
+            assertTrue(uris.contains(uri));
+            assertTrue(uris.contains(longerUri));
+        } finally {
+            mManager.unpinSlice(uri);
+            mManager.unpinSlice(longerUri);
+        }
+    }
+
+    @Test
     public void testCallback() {
         if (BuildCompat.isAtLeastP()) {
             return;
diff --git a/slices/view/src/main/java/androidx/slice/SliceManager.java b/slices/view/src/main/java/androidx/slice/SliceManager.java
index 37a1960..83febcc 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManager.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManager.java
@@ -28,6 +28,7 @@
 import androidx.core.os.BuildCompat;
 
 import java.util.Collection;
+import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Executor;
 
@@ -125,10 +126,8 @@
      * <p>
      * This is the set of specs supported for a specific pinned slice. It will take
      * into account all clients and returns only specs supported by all.
-     * @hide
      * @see SliceSpec
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public abstract @NonNull Set<SliceSpec> getPinnedSpecs(@NonNull Uri uri);
 
     /**
@@ -233,6 +232,12 @@
     public abstract @NonNull Collection<Uri> getSliceDescendants(@NonNull Uri uri);
 
     /**
+     * Get the list of currently pinned slices for this app.
+     * @see SliceProvider#onSlicePinned
+     */
+    public abstract @NonNull List<Uri> getPinnedSlices();
+
+    /**
      * Class that listens to changes in {@link Slice}s.
      */
     public interface SliceCallback {
diff --git a/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java b/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
index f8cda4d..0018c13 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
@@ -29,6 +29,7 @@
 import androidx.slice.widget.SliceLiveData;
 
 import java.util.Collection;
+import java.util.List;
 import java.util.Set;
 
 
@@ -97,4 +98,9 @@
     public Collection<Uri> getSliceDescendants(Uri uri) {
         return SliceProviderCompat.getSliceDescendants(mContext, uri);
     }
+
+    @Override
+    public List<Uri> getPinnedSlices() {
+        return SliceProviderCompat.getPinnedSlices(mContext);
+    }
 }
diff --git a/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java b/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
index 4c19b84..ec49fe2 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
@@ -30,7 +30,6 @@
 import androidx.annotation.RestrictTo;
 import androidx.core.content.PermissionChecker;
 
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -44,7 +43,7 @@
 class SliceManagerWrapper extends SliceManagerBase {
 
     private final android.app.slice.SliceManager mManager;
-    private final List<SliceSpec> mSpecs;
+    private final Set<SliceSpec> mSpecs;
 
     SliceManagerWrapper(Context context) {
         this(context, context.getSystemService(android.app.slice.SliceManager.class));
@@ -53,7 +52,7 @@
     SliceManagerWrapper(Context context, android.app.slice.SliceManager manager) {
         super(context);
         mManager = manager;
-        mSpecs = new ArrayList<>(unwrap(SUPPORTED_SPECS));
+        mSpecs = unwrap(SUPPORTED_SPECS);
     }
 
     @Override
@@ -111,4 +110,9 @@
     public Uri mapIntentToUri(@NonNull Intent intent) {
         return mManager.mapIntentToUri(intent);
     }
+
+    @Override
+    public List<Uri> getPinnedSlices() {
+        return mManager.getPinnedSlices();
+    }
 }
diff --git a/slices/view/src/main/java/androidx/slice/SliceMetadata.java b/slices/view/src/main/java/androidx/slice/SliceMetadata.java
index 8a4ee03..0c4f614 100644
--- a/slices/view/src/main/java/androidx/slice/SliceMetadata.java
+++ b/slices/view/src/main/java/androidx/slice/SliceMetadata.java
@@ -23,9 +23,9 @@
 import static android.app.slice.Slice.SUBTYPE_MAX;
 import static android.app.slice.Slice.SUBTYPE_VALUE;
 import static android.app.slice.SliceItem.FORMAT_INT;
+import static android.app.slice.SliceItem.FORMAT_LONG;
 import static android.app.slice.SliceItem.FORMAT_SLICE;
 import static android.app.slice.SliceItem.FORMAT_TEXT;
-import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
 
 import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
 import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
@@ -118,11 +118,11 @@
     private SliceMetadata(@NonNull Context context, @NonNull Slice slice) {
         mSlice = slice;
         mContext = context;
-        SliceItem ttlItem = SliceQuery.find(slice, FORMAT_TIMESTAMP, HINT_TTL, null);
+        SliceItem ttlItem = SliceQuery.find(slice, FORMAT_LONG, HINT_TTL, null);
         if (ttlItem != null) {
             mExpiry = ttlItem.getTimestamp();
         }
-        SliceItem updatedItem = SliceQuery.find(slice, FORMAT_TIMESTAMP, HINT_LAST_UPDATED, null);
+        SliceItem updatedItem = SliceQuery.find(slice, FORMAT_LONG, HINT_LAST_UPDATED, null);
         if (updatedItem != null) {
             mLastUpdated = updatedItem.getTimestamp();
         }
diff --git a/slices/view/src/main/java/androidx/slice/widget/GridContent.java b/slices/view/src/main/java/androidx/slice/widget/GridContent.java
index 69d5c44..424143c 100644
--- a/slices/view/src/main/java/androidx/slice/widget/GridContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/GridContent.java
@@ -26,9 +26,9 @@
 import static android.app.slice.SliceItem.FORMAT_ACTION;
 import static android.app.slice.SliceItem.FORMAT_IMAGE;
 import static android.app.slice.SliceItem.FORMAT_INT;
+import static android.app.slice.SliceItem.FORMAT_LONG;
 import static android.app.slice.SliceItem.FORMAT_SLICE;
 import static android.app.slice.SliceItem.FORMAT_TEXT;
-import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
 
 import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
 import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
@@ -295,7 +295,7 @@
                     if (SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) {
                         mContentDescr = item;
                     } else if (mTextCount < 2 && (FORMAT_TEXT.equals(itemFormat)
-                            || FORMAT_TIMESTAMP.equals(itemFormat))) {
+                            || FORMAT_LONG.equals(itemFormat))) {
                         mTextCount++;
                         mCellItems.add(item);
                     } else if (imageCount < 1 && FORMAT_IMAGE.equals(item.getFormat())) {
@@ -340,7 +340,7 @@
                     || cellItem.hasAnyHints(HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED);
             return !isNonCellContent
                     && (FORMAT_TEXT.equals(format)
-                    || FORMAT_TIMESTAMP.equals(format)
+                    || FORMAT_LONG.equals(format)
                     || FORMAT_IMAGE.equals(format));
         }
 
diff --git a/slices/view/src/main/java/androidx/slice/widget/GridRowView.java b/slices/view/src/main/java/androidx/slice/widget/GridRowView.java
index 4b8078c..7423b02 100644
--- a/slices/view/src/main/java/androidx/slice/widget/GridRowView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/GridRowView.java
@@ -21,9 +21,9 @@
 import static android.app.slice.Slice.HINT_TITLE;
 import static android.app.slice.SliceItem.FORMAT_ACTION;
 import static android.app.slice.SliceItem.FORMAT_IMAGE;
+import static android.app.slice.SliceItem.FORMAT_LONG;
 import static android.app.slice.SliceItem.FORMAT_SLICE;
 import static android.app.slice.SliceItem.FORMAT_TEXT;
-import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
@@ -258,7 +258,7 @@
             SliceItem item = cellItems.get(i);
             final String itemFormat = item.getFormat();
             if (textCount < maxCellText && (FORMAT_TEXT.equals(itemFormat)
-                    || FORMAT_TIMESTAMP.equals(itemFormat))) {
+                    || FORMAT_LONG.equals(itemFormat))) {
                 if (textItems != null && !textItems.contains(item)) {
                     continue;
                 }
@@ -305,13 +305,13 @@
     private boolean addItem(SliceItem item, int color, ViewGroup container, boolean singleItem) {
         final String format = item.getFormat();
         View addedView = null;
-        if (FORMAT_TEXT.equals(format) || FORMAT_TIMESTAMP.equals(format)) {
+        if (FORMAT_TEXT.equals(format) || FORMAT_LONG.equals(format)) {
             boolean title = SliceQuery.hasAnyHints(item, HINT_LARGE, HINT_TITLE);
             TextView tv = (TextView) LayoutInflater.from(getContext()).inflate(title
                     ? TITLE_TEXT_LAYOUT : TEXT_LAYOUT, null);
             tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, title ? mTitleSize : mSubtitleSize);
             tv.setTextColor(title ? mTitleColor : mSubtitleColor);
-            CharSequence text = FORMAT_TIMESTAMP.equals(format)
+            CharSequence text = FORMAT_LONG.equals(format)
                     ? SliceViewUtil.getRelativeTimeString(item.getTimestamp())
                     : item.getText();
             tv.setText(text);
diff --git a/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java b/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java
index e09405c..9578e21 100644
--- a/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java
+++ b/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java
@@ -191,8 +191,6 @@
                         null);
                 break;
         }
-        int mode = mParent != null ? mParent.getMode() : MODE_LARGE;
-        ((SliceChildView) v).setMode(mode);
         return v;
     }
 
@@ -248,6 +246,8 @@
             mSliceChildView.setOnTouchListener(this);
 
             final boolean isHeader = position == HEADER_INDEX;
+            int mode = mParent != null ? mParent.getMode() : MODE_LARGE;
+            mSliceChildView.setMode(mode);
             mSliceChildView.setTint(mColor);
             mSliceChildView.setStyle(mAttrs, mDefStyleAttr, mDefStyleRes);
             mSliceChildView.setSliceItem(item, isHeader, position, mSliceObserver);
diff --git a/slices/view/src/main/java/androidx/slice/widget/RowContent.java b/slices/view/src/main/java/androidx/slice/widget/RowContent.java
index bc3826f..0c8d4a6 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RowContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RowContent.java
@@ -27,10 +27,10 @@
 import static android.app.slice.SliceItem.FORMAT_ACTION;
 import static android.app.slice.SliceItem.FORMAT_IMAGE;
 import static android.app.slice.SliceItem.FORMAT_INT;
+import static android.app.slice.SliceItem.FORMAT_LONG;
 import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
 import static android.app.slice.SliceItem.FORMAT_SLICE;
 import static android.app.slice.SliceItem.FORMAT_TEXT;
-import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
 
 import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
 import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
@@ -157,11 +157,11 @@
             }
             // Special rules for end items: only one timestamp
             boolean hasTimestamp = mStartItem != null
-                    && FORMAT_TIMESTAMP.equals(mStartItem.getFormat());
+                    && FORMAT_LONG.equals(mStartItem.getFormat());
             for (int i = 0; i < endItems.size(); i++) {
                 final SliceItem item = endItems.get(i);
                 boolean isAction = SliceQuery.find(item, FORMAT_ACTION) != null;
-                if (FORMAT_TIMESTAMP.equals(item.getFormat())) {
+                if (FORMAT_LONG.equals(item.getFormat())) {
                     if (!hasTimestamp) {
                         hasTimestamp = true;
                         mEndItems.add(item);
@@ -382,7 +382,7 @@
         return (FORMAT_TEXT.equals(itemFormat)
                 && !SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType()))
                 || FORMAT_IMAGE.equals(itemFormat)
-                || FORMAT_TIMESTAMP.equals(itemFormat)
+                || FORMAT_LONG.equals(itemFormat)
                 || FORMAT_REMOTE_INPUT.equals(itemFormat)
                 || (FORMAT_SLICE.equals(itemFormat) && item.hasHint(HINT_TITLE)
                 && !item.hasHint(HINT_SHORTCUT))
@@ -400,7 +400,7 @@
         final String type = item.getFormat();
         return (FORMAT_ACTION.equals(type) && (SliceQuery.find(item, FORMAT_IMAGE) != null))
                     || FORMAT_IMAGE.equals(type)
-                    || (FORMAT_TIMESTAMP.equals(type)
+                    || (FORMAT_LONG.equals(type)
                 && !item.hasAnyHints(HINT_TTL, HINT_LAST_UPDATED));
     }
 }
diff --git a/slices/view/src/main/java/androidx/slice/widget/RowView.java b/slices/view/src/main/java/androidx/slice/widget/RowView.java
index a7101fe..2840db9 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RowView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RowView.java
@@ -26,8 +26,8 @@
 import static android.app.slice.SliceItem.FORMAT_ACTION;
 import static android.app.slice.SliceItem.FORMAT_IMAGE;
 import static android.app.slice.SliceItem.FORMAT_INT;
+import static android.app.slice.SliceItem.FORMAT_LONG;
 import static android.app.slice.SliceItem.FORMAT_SLICE;
-import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
 
 import static androidx.slice.core.SliceHints.ICON_IMAGE;
 import static androidx.slice.core.SliceHints.SMALL_IMAGE;
@@ -278,7 +278,7 @@
 
         // If we're here we can can show end items; check for top level actions first
         List<SliceItem> endItems = mRowContent.getEndItems();
-        if (mIsHeader && mHeaderActions != null && mHeaderActions.size() > 0) {
+        if (mHeaderActions != null && mHeaderActions.size() > 0) {
             // Use these if we have them instead
             endItems = mHeaderActions;
         }
@@ -460,7 +460,7 @@
         if (FORMAT_IMAGE.equals(sliceItem.getFormat())) {
             icon = sliceItem.getIcon();
             imageMode = sliceItem.hasHint(HINT_NO_TINT) ? SMALL_IMAGE : ICON_IMAGE;
-        } else if (FORMAT_TIMESTAMP.equals(sliceItem.getFormat())) {
+        } else if (FORMAT_LONG.equals(sliceItem.getFormat())) {
             timeStamp = sliceItem;
         }
         View addedView = null;
diff --git a/slices/view/src/main/java/androidx/slice/widget/ShortcutView.java b/slices/view/src/main/java/androidx/slice/widget/ShortcutView.java
index 37ef17b..3afda5d 100644
--- a/slices/view/src/main/java/androidx/slice/widget/ShortcutView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/ShortcutView.java
@@ -195,7 +195,7 @@
                 if (mActionItem == null) {
                     mActionItem = new SliceItem(PendingIntent.getActivity(context, 0,
                             pm.getLaunchIntentForPackage(appInfo.packageName), 0),
-                            new Slice.Builder(slice.getUri()).build(), FORMAT_SLICE,
+                            new Slice.Builder(slice.getUri()).build(), FORMAT_ACTION,
                             null /* subtype */, null);
                 }
             }
diff --git a/webkit/api/current.txt b/webkit/api/current.txt
index 7084a21..60dbb9c 100644
--- a/webkit/api/current.txt
+++ b/webkit/api/current.txt
@@ -33,6 +33,10 @@
     method public abstract int getErrorCode();
   }
 
+  public class WebResourceRequestCompat {
+    method public static boolean isRedirect(android.webkit.WebResourceRequest);
+  }
+
   public class WebSettingsCompat {
     method public static int getDisabledActionModeMenuItems(android.webkit.WebSettings);
     method public static boolean getOffscreenPreRaster(android.webkit.WebSettings);
@@ -81,6 +85,7 @@
     field public static final java.lang.String SHOULD_OVERRIDE_WITH_REDIRECTS = "SHOULD_OVERRIDE_WITH_REDIRECTS";
     field public static final java.lang.String START_SAFE_BROWSING = "START_SAFE_BROWSING";
     field public static final java.lang.String VISUAL_STATE_CALLBACK = "VISUAL_STATE_CALLBACK";
+    field public static final java.lang.String WEB_RESOURCE_REQUEST_IS_REDIRECT = "WEB_RESOURCE_REQUEST_IS_REDIRECT";
   }
 
 }
diff --git a/webkit/src/main/java/androidx/webkit/WebResourceRequestCompat.java b/webkit/src/main/java/androidx/webkit/WebResourceRequestCompat.java
new file mode 100644
index 0000000..783efca
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/WebResourceRequestCompat.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.annotation.SuppressLint;
+import android.webkit.WebResourceRequest;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresFeature;
+import androidx.webkit.internal.WebResourceRequestAdapter;
+import androidx.webkit.internal.WebViewFeatureInternal;
+import androidx.webkit.internal.WebViewGlueCommunicator;
+
+// TODO(gsennton) add a test for this class
+
+/**
+ * Compatibility version of {@link WebResourceRequest}.
+ */
+public class WebResourceRequestCompat {
+
+    // Developers should not be able to instantiate this class.
+    private WebResourceRequestCompat() {}
+
+    /**
+     * Gets whether the request was a result of a server-side redirect.
+     *
+     * @return whether the request was a result of a server-side redirect.
+     */
+    @SuppressLint("NewApi")
+    @RequiresFeature(name = WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public static boolean isRedirect(@NonNull WebResourceRequest request) {
+        WebViewFeatureInternal feature = WebViewFeatureInternal.WEB_RESOURCE_REQUEST_IS_REDIRECT;
+        if (feature.isSupportedByFramework()) {
+            return request.isRedirect();
+        } else if (feature.isSupportedByWebView()) {
+            return getAdapter(request).isRedirect();
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    private static WebResourceRequestAdapter getAdapter(WebResourceRequest request) {
+        return WebViewGlueCommunicator.getCompatConverter().convertWebResourceRequest(request);
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/WebViewCompat.java b/webkit/src/main/java/androidx/webkit/WebViewCompat.java
index 6e82a69..bb1c4fc 100644
--- a/webkit/src/main/java/androidx/webkit/WebViewCompat.java
+++ b/webkit/src/main/java/androidx/webkit/WebViewCompat.java
@@ -344,7 +344,7 @@
                 throw new RuntimeException("A WebView method was called on thread '"
                         + Thread.currentThread().getName() + "'. "
                         + "All WebView methods must be called on the same thread. "
-                        + "(Expected Looper " + webview.getLooper() + " called on "
+                        + "(Expected Looper " + webview.getWebViewLooper() + " called on "
                         + Looper.myLooper() + ", FYI main Looper is " + Looper.getMainLooper()
                         + ")");
             }
diff --git a/webkit/src/main/java/androidx/webkit/WebViewFeature.java b/webkit/src/main/java/androidx/webkit/WebViewFeature.java
index b2715df..5ee4c2c 100644
--- a/webkit/src/main/java/androidx/webkit/WebViewFeature.java
+++ b/webkit/src/main/java/androidx/webkit/WebViewFeature.java
@@ -62,7 +62,8 @@
             RECEIVE_WEB_RESOURCE_ERROR,
             RECEIVE_HTTP_ERROR,
             SHOULD_OVERRIDE_WITH_REDIRECTS,
-            SAFE_BROWSING_HIT
+            SAFE_BROWSING_HIT,
+            WEB_RESOURCE_REQUEST_IS_REDIRECT
     })
     @Retention(RetentionPolicy.SOURCE)
     @Target({ElementType.PARAMETER, ElementType.METHOD})
@@ -208,6 +209,14 @@
     public static final String SAFE_BROWSING_HIT = Features.SAFE_BROWSING_HIT;
 
     /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link WebResourceRequestCompat#isRedirect(WebResourceRequest)}.
+     */
+    public static final String WEB_RESOURCE_REQUEST_IS_REDIRECT =
+            Features.WEB_RESOURCE_REQUEST_IS_REDIRECT;
+
+    /**
      * Return whether a feature is supported at run-time. This depends on the Android version of the
      * device and the WebView APK on the device.
      */
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebResourceRequestAdapter.java b/webkit/src/main/java/androidx/webkit/internal/WebResourceRequestAdapter.java
new file mode 100644
index 0000000..0d6a05d
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/WebResourceRequestAdapter.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit.internal;
+
+import android.webkit.WebResourceRequest;
+
+import org.chromium.support_lib_boundary.WebResourceRequestBoundaryInterface;
+
+/**
+ * Adapter between {@link androidx.webkit.WebResourceRequestCompat} and
+ * {@link org.chromium.support_lib_boundary.WebResourceRequestBoundaryInterface}.
+ */
+public class WebResourceRequestAdapter {
+    private final WebResourceRequestBoundaryInterface mBoundaryInterface;
+
+    public WebResourceRequestAdapter(WebResourceRequestBoundaryInterface boundaryInterface) {
+        mBoundaryInterface = boundaryInterface;
+    }
+
+    /**
+     * Adapter method for
+     * {@link androidx.webkit.WebResourceRequestCompat#isRedirect(WebResourceRequest)}.
+     */
+    public boolean isRedirect() {
+        return mBoundaryInterface.isRedirect();
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java b/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
index d2ff4a4..07c6b5f 100644
--- a/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
+++ b/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
@@ -23,6 +23,7 @@
 import android.webkit.WebSettings;
 
 import androidx.webkit.ServiceWorkerClientCompat;
+import androidx.webkit.WebResourceRequestCompat;
 import androidx.webkit.WebViewClientCompat;
 import androidx.webkit.WebViewCompat;
 import androidx.webkit.WebViewFeature;
@@ -154,7 +155,14 @@
      * {@link WebViewClientCompat#onSafeBrowsingHit(android.webkit.WebView,
      * WebResourceRequest, int, SafeBrowsingResponseCompat)}.
      */
-    SAFE_BROWSING_HIT(WebViewFeature.SAFE_BROWSING_HIT, Build.VERSION_CODES.O_MR1);
+    SAFE_BROWSING_HIT(WebViewFeature.SAFE_BROWSING_HIT, Build.VERSION_CODES.O_MR1),
+
+    /**
+     * This feature covers
+     * {@link WebResourceRequestCompat#isRedirect(WebResourceRequest)}.
+     */
+    WEB_RESOURCE_REQUEST_IS_REDIRECT(WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT,
+            Build.VERSION_CODES.N);
 
     private final String mFeatureValue;
     private final int mOsVersion;
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java b/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java
index 4d264a3..e333480 100644
--- a/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java
+++ b/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java
@@ -17,9 +17,11 @@
 package androidx.webkit.internal;
 
 import android.webkit.ServiceWorkerWebSettings;
+import android.webkit.WebResourceRequest;
 import android.webkit.WebSettings;
 
 import org.chromium.support_lib_boundary.ServiceWorkerWebSettingsBoundaryInterface;
+import org.chromium.support_lib_boundary.WebResourceRequestBoundaryInterface;
 import org.chromium.support_lib_boundary.WebSettingsBoundaryInterface;
 import org.chromium.support_lib_boundary.WebkitToCompatConverterBoundaryInterface;
 import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
@@ -56,4 +58,14 @@
                 ServiceWorkerWebSettingsBoundaryInterface.class,
                 mImpl.convertServiceWorkerSettings(settings));
     }
+
+    /**
+     * Return a {@link WebResourceRequestAdapter} linked to the given {@link WebResourceRequest} so
+     * that calls on either of those objects affect the other object.
+     */
+    public WebResourceRequestAdapter convertWebResourceRequest(WebResourceRequest request) {
+        return new WebResourceRequestAdapter(BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+                WebResourceRequestBoundaryInterface.class,
+                mImpl.convertWebResourceRequest(request)));
+    }
 }