Merge "[WebView Support Library] Add WebViewClientCompat feature flags" into pi-androidx-dev
diff --git a/car/res/drawable/car_borderless_button_background.xml b/car/res/drawable/car_borderless_button_background.xml
index 0ef4bea..a8add07 100644
--- a/car/res/drawable/car_borderless_button_background.xml
+++ b/car/res/drawable/car_borderless_button_background.xml
@@ -18,6 +18,6 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:color="?attr/listItemBackgroundColor">
     <item android:id="@android:id/mask">
-        <color android:color="#42ffffff "/>
+        <color android:color="#42ffffff"/>
     </item>
 </ripple>
diff --git a/car/res/layout/car_alert_dialog.xml b/car/res/layout/car_alert_dialog.xml
index 46da691..704c8ad 100644
--- a/car/res/layout/car_alert_dialog.xml
+++ b/car/res/layout/car_alert_dialog.xml
@@ -38,7 +38,7 @@
 
         <TextView
             android:id="@+id/title"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="@dimen/car_dialog_header_height"
             android:gravity="center_vertical|start"
             android:visibility="gone"
diff --git a/car/res/layout/car_list_item_seekbar_content.xml b/car/res/layout/car_list_item_seekbar_content.xml
index eedbe73..6e3e33a 100644
--- a/car/res/layout/car_list_item_seekbar_content.xml
+++ b/car/res/layout/car_list_item_seekbar_content.xml
@@ -33,6 +33,8 @@
         android:id="@+id/seek_bar_container"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/car_padding_1"
+        android:layout_marginBottom="@dimen/car_padding_1"
         android:orientation="vertical">
         <TextView
             android:id="@+id/text"
diff --git a/car/res/layout/car_list_item_text_content.xml b/car/res/layout/car_list_item_text_content.xml
index 0e5d4b9..b5f1a03 100644
--- a/car/res/layout/car_list_item_text_content.xml
+++ b/car/res/layout/car_list_item_text_content.xml
@@ -59,11 +59,11 @@
     <View
         android:id="@+id/switch_divider"
         android:layout_centerVertical="true"
-        android:layout_toStartOf="@id/switch_widget"
+        android:layout_toStartOf="@+id/switch_widget"
         android:layout_marginEnd="@dimen/car_padding_4"
         style="@style/CarListVerticalDivider"/>
     <Switch
-        android:id="@+id/switch_widget"
+        android:id="@id/switch_widget"
         android:layout_centerVertical="true"
         android:layout_width="@dimen/car_primary_icon_size"
         android:layout_height="@dimen/car_primary_icon_size"
@@ -75,28 +75,28 @@
     <View
         android:id="@+id/action2_divider"
         android:layout_centerVertical="true"
-        android:layout_toStartOf="@id/action2"
+        android:layout_toStartOf="@+id/action2"
         android:layout_marginEnd="@dimen/car_padding_4"
         style="@style/CarListVerticalDivider"/>
     <Button
-        android:id="@+id/action2"
+        android:id="@id/action2"
         android:layout_centerVertical="true"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_toStartOf="@id/action1_divider"
+        android:layout_toStartOf="@+id/action1_divider"
         android:layout_marginEnd="@dimen/car_padding_4"
         android:ellipsize="end"
         android:maxLength="@integer/car_borderless_button_text_length_limit"
         android:maxLines="1"
         style="?android:attr/borderlessButtonStyle"/>
     <View
-        android:id="@+id/action1_divider"
+        android:id="@id/action1_divider"
         android:layout_centerVertical="true"
-        android:layout_toStartOf="@id/action1"
+        android:layout_toStartOf="@+id/action1"
         android:layout_marginEnd="@dimen/car_padding_4"
         style="@style/CarListVerticalDivider"/>
     <Button
-        android:id="@+id/action1"
+        android:id="@id/action1"
         android:layout_centerVertical="true"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
diff --git a/car/res/values/dimens.xml b/car/res/values/dimens.xml
index c310ec7..51810ad 100644
--- a/car/res/values/dimens.xml
+++ b/car/res/values/dimens.xml
@@ -144,7 +144,7 @@
 
     <!-- Seekbar -->
     <dimen name="car_seekbar_height">6dp</dimen>
-    <dimen name="car_seekbar_thumb_size">20dp</dimen>
+    <dimen name="car_seekbar_thumb_size">24dp</dimen>
     <dimen name="car_seekbar_thumb_stroke">1dp</dimen>
 
     <!-- Scroll Bar Thumb -->
diff --git a/car/res/values/styles.xml b/car/res/values/styles.xml
index 7e32c39..4b547ad 100644
--- a/car/res/values/styles.xml
+++ b/car/res/values/styles.xml
@@ -328,7 +328,6 @@
         <item name="android:maxLines">1</item>
         <item name="android:textAppearance">@style/TextAppearance.Car.Title2</item>
         <item name="android:ellipsize">end</item>
-        <item name="android:textAlignment">viewStart</item>
     </style>
 
     <!-- Title text for a dialog that is fixed in a light color. -->
diff --git a/car/src/main/java/androidx/car/app/CarAlertDialog.java b/car/src/main/java/androidx/car/app/CarAlertDialog.java
index 453ad4e..58262b6 100644
--- a/car/src/main/java/androidx/car/app/CarAlertDialog.java
+++ b/car/src/main/java/androidx/car/app/CarAlertDialog.java
@@ -23,6 +23,7 @@
 import android.os.Bundle;
 import android.text.TextUtils;
 import android.util.TypedValue;
+import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.TouchDelegate;
 import android.view.View;
@@ -90,17 +91,24 @@
 
     private void setTitleInternal(CharSequence title) {
         boolean hasTitle = !TextUtils.isEmpty(title);
+        boolean hasBody = mBodyView.getVisibility() == View.VISIBLE;
+        boolean hasButton = mButtonPanel.getVisibility() == View.VISIBLE;
 
         mTitleView.setText(title);
         mTitleView.setVisibility(hasTitle ? View.VISIBLE : View.GONE);
 
+        // Center title if there is no button.
+        mTitleView.setGravity(hasButton ? Gravity.CENTER_VERTICAL | Gravity.START : Gravity.CENTER);
+
         // If there's a title, then remove the padding at the top of the content view.
         int topPadding = hasTitle ? 0 : mTopPadding;
+        // If there is only title, also remove the padding at the bottom so title is centered.
+        int bottomPadding = !hasButton && !hasBody ? 0 : mContentView.getPaddingBottom();
         mContentView.setPaddingRelative(
                 mContentView.getPaddingStart(),
                 topPadding,
                 mContentView.getPaddingEnd(),
-                mContentView.getPaddingBottom());
+                bottomPadding);
     }
 
     private void setBody(CharSequence body) {
@@ -226,10 +234,12 @@
      * contents based on what data is present.
      */
     private void initializeDialogWithData() {
-        setTitleInternal(mData.mTitle);
         setBody(mData.mBody);
         setPositiveButton(mData.mPositiveButtonText);
         setNegativeButton(mData.mNegativeButtonText);
+        // setTitleInternal() should be called last because we want to center title and adjust
+        // padding depending on body/button configuration.
+        setTitleInternal(mData.mTitle);
     }
 
     /**
diff --git a/compat/src/main/java/androidx/core/graphics/drawable/IconCompat.java b/compat/src/main/java/androidx/core/graphics/drawable/IconCompat.java
index 366ddf3..4c296ea 100644
--- a/compat/src/main/java/androidx/core/graphics/drawable/IconCompat.java
+++ b/compat/src/main/java/androidx/core/graphics/drawable/IconCompat.java
@@ -569,6 +569,58 @@
         return bundle;
     }
 
+    @Override
+    public String toString() {
+        if (mType == TYPE_UNKOWN) {
+            return String.valueOf(mObj1);
+        }
+        final StringBuilder sb = new StringBuilder("Icon(typ=").append(typeToString(mType));
+        switch (mType) {
+            case TYPE_BITMAP:
+            case TYPE_ADAPTIVE_BITMAP:
+                sb.append(" size=")
+                        .append(((Bitmap) mObj1).getWidth())
+                        .append("x")
+                        .append(((Bitmap) mObj1).getHeight());
+                break;
+            case TYPE_RESOURCE:
+                sb.append(" pkg=")
+                        .append(getResPackage())
+                        .append(" id=")
+                        .append(String.format("0x%08x", getResId()));
+                break;
+            case TYPE_DATA:
+                sb.append(" len=").append(mInt1);
+                if (mInt2 != 0) {
+                    sb.append(" off=").append(mInt2);
+                }
+                break;
+            case TYPE_URI:
+                sb.append(" uri=").append(mObj1);
+                break;
+        }
+        if (mTintList != null) {
+            sb.append(" tint=");
+            sb.append(mTintList);
+        }
+        if (mTintMode != DEFAULT_TINT_MODE) {
+            sb.append(" mode=").append(mTintMode);
+        }
+        sb.append(")");
+        return sb.toString();
+    }
+
+    private static String typeToString(int x) {
+        switch (x) {
+            case TYPE_BITMAP: return "BITMAP";
+            case TYPE_ADAPTIVE_BITMAP: return "BITMAP_MASKABLE";
+            case TYPE_DATA: return "DATA";
+            case TYPE_RESOURCE: return "RESOURCE";
+            case TYPE_URI: return "URI";
+            default: return "UNKNOWN";
+        }
+    }
+
     /**
      * Extracts an icon from a bundle that was added using {@link #toBundle()}.
      */
diff --git a/jetifier/jetifier/core/src/main/resources/default.generated.config b/jetifier/jetifier/core/src/main/resources/default.generated.config
index 66dfa5a..3acc5fc 100644
--- a/jetifier/jetifier/core/src/main/resources/default.generated.config
+++ b/jetifier/jetifier/core/src/main/resources/default.generated.config
@@ -2124,7 +2124,7 @@
       "to": [
         {
           "groupId": "androidx.slice",
-          "artifactId": "slices-core",
+          "artifactId": "slice-core",
           "version": "1.0.0"
         }
       ]
@@ -2138,7 +2138,7 @@
       "to": [
         {
           "groupId": "androidx.slice",
-          "artifactId": "slices-builders",
+          "artifactId": "slice-builders",
           "version": "1.0.0"
         }
       ]
@@ -2152,7 +2152,7 @@
       "to": [
         {
           "groupId": "androidx.slice",
-          "artifactId": "slices-view",
+          "artifactId": "slice-view",
           "version": "1.0.0"
         }
       ]
diff --git a/media/api/current.txt b/media/api/current.txt
index d7df744..a9e2161 100644
--- a/media/api/current.txt
+++ b/media/api/current.txt
@@ -565,6 +565,41 @@
     method public androidx.media.AudioAttributesCompat.Builder setUsage(int);
   }
 
+  public final class DataSourceDesc {
+    method public long getEndPosition();
+    method public java.io.FileDescriptor getFileDescriptor();
+    method public long getFileDescriptorLength();
+    method public long getFileDescriptorOffset();
+    method public androidx.media.Media2DataSource getMedia2DataSource();
+    method public java.lang.String getMediaId();
+    method public long getStartPosition();
+    method public int getType();
+    method public android.net.Uri getUri();
+    method public android.content.Context getUriContext();
+    method public java.util.List<java.net.HttpCookie> getUriCookies();
+    method public java.util.Map<java.lang.String, java.lang.String> getUriHeaders();
+    field public static final long FD_LENGTH_UNKNOWN = 576460752303423487L; // 0x7ffffffffffffffL
+    field public static final long POSITION_UNKNOWN = 576460752303423487L; // 0x7ffffffffffffffL
+    field public static final int TYPE_CALLBACK = 1; // 0x1
+    field public static final int TYPE_FD = 2; // 0x2
+    field public static final int TYPE_NONE = 0; // 0x0
+    field public static final int TYPE_URI = 3; // 0x3
+  }
+
+  public static class DataSourceDesc.Builder {
+    ctor public DataSourceDesc.Builder();
+    ctor public DataSourceDesc.Builder(androidx.media.DataSourceDesc);
+    method public androidx.media.DataSourceDesc build();
+    method public androidx.media.DataSourceDesc.Builder setDataSource(androidx.media.Media2DataSource);
+    method public androidx.media.DataSourceDesc.Builder setDataSource(java.io.FileDescriptor);
+    method public androidx.media.DataSourceDesc.Builder setDataSource(java.io.FileDescriptor, long, long);
+    method public androidx.media.DataSourceDesc.Builder setDataSource(android.content.Context, android.net.Uri);
+    method public androidx.media.DataSourceDesc.Builder setDataSource(android.content.Context, android.net.Uri, java.util.Map<java.lang.String, java.lang.String>, java.util.List<java.net.HttpCookie>);
+    method public androidx.media.DataSourceDesc.Builder setEndPosition(long);
+    method public androidx.media.DataSourceDesc.Builder setMediaId(java.lang.String);
+    method public androidx.media.DataSourceDesc.Builder setStartPosition(long);
+  }
+
   public abstract class MediaBrowserServiceCompat extends android.app.Service {
     ctor public MediaBrowserServiceCompat();
     method public void dump(java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]);
@@ -600,6 +635,487 @@
     method public void sendResult(T);
   }
 
+  public class MediaController2 implements java.lang.AutoCloseable {
+    ctor public MediaController2(android.content.Context, androidx.media.SessionToken2, java.util.concurrent.Executor, androidx.media.MediaController2.ControllerCallback);
+    method public void addPlaylistItem(int, androidx.media.MediaItem2);
+    method public void adjustVolume(int, int);
+    method public void close();
+    method public void fastForward();
+    method public long getBufferedPosition();
+    method public int getBufferingState();
+    method public androidx.media.MediaItem2 getCurrentMediaItem();
+    method public long getCurrentPosition();
+    method public long getDuration();
+    method public androidx.media.MediaController2.PlaybackInfo getPlaybackInfo();
+    method public float getPlaybackSpeed();
+    method public int getPlayerState();
+    method public java.util.List<androidx.media.MediaItem2> getPlaylist();
+    method public androidx.media.MediaMetadata2 getPlaylistMetadata();
+    method public int getRepeatMode();
+    method public android.app.PendingIntent getSessionActivity();
+    method public androidx.media.SessionToken2 getSessionToken();
+    method public int getShuffleMode();
+    method public boolean isConnected();
+    method public void pause();
+    method public void play();
+    method public void playFromMediaId(java.lang.String, android.os.Bundle);
+    method public void playFromSearch(java.lang.String, android.os.Bundle);
+    method public void playFromUri(android.net.Uri, android.os.Bundle);
+    method public void prepare();
+    method public void prepareFromMediaId(java.lang.String, android.os.Bundle);
+    method public void prepareFromSearch(java.lang.String, android.os.Bundle);
+    method public void prepareFromUri(android.net.Uri, android.os.Bundle);
+    method public void removePlaylistItem(androidx.media.MediaItem2);
+    method public void replacePlaylistItem(int, androidx.media.MediaItem2);
+    method public void reset();
+    method public void rewind();
+    method public void seekTo(long);
+    method public void selectRoute(android.os.Bundle);
+    method public void sendCustomCommand(androidx.media.SessionCommand2, android.os.Bundle, android.os.ResultReceiver);
+    method public void setPlaybackSpeed(float);
+    method public void setPlaylist(java.util.List<androidx.media.MediaItem2>, androidx.media.MediaMetadata2);
+    method public void setRating(java.lang.String, androidx.media.Rating2);
+    method public void setRepeatMode(int);
+    method public void setShuffleMode(int);
+    method public void setVolumeTo(int, int);
+    method public void skipToNextItem();
+    method public void skipToPlaylistItem(androidx.media.MediaItem2);
+    method public void skipToPreviousItem();
+    method public void subscribeRoutesInfo();
+    method public void unsubscribeRoutesInfo();
+    method public void updatePlaylistMetadata(androidx.media.MediaMetadata2);
+  }
+
+  public static abstract class MediaController2.ControllerCallback {
+    ctor public MediaController2.ControllerCallback();
+    method public void onAllowedCommandsChanged(androidx.media.MediaController2, androidx.media.SessionCommandGroup2);
+    method public void onBufferingStateChanged(androidx.media.MediaController2, androidx.media.MediaItem2, int);
+    method public void onConnected(androidx.media.MediaController2, androidx.media.SessionCommandGroup2);
+    method public void onCurrentMediaItemChanged(androidx.media.MediaController2, androidx.media.MediaItem2);
+    method public void onCustomCommand(androidx.media.MediaController2, androidx.media.SessionCommand2, android.os.Bundle, android.os.ResultReceiver);
+    method public void onCustomLayoutChanged(androidx.media.MediaController2, java.util.List<androidx.media.MediaSession2.CommandButton>);
+    method public void onDisconnected(androidx.media.MediaController2);
+    method public void onError(androidx.media.MediaController2, int, android.os.Bundle);
+    method public void onPlaybackInfoChanged(androidx.media.MediaController2, androidx.media.MediaController2.PlaybackInfo);
+    method public void onPlaybackSpeedChanged(androidx.media.MediaController2, float);
+    method public void onPlayerStateChanged(androidx.media.MediaController2, int);
+    method public void onPlaylistChanged(androidx.media.MediaController2, java.util.List<androidx.media.MediaItem2>, androidx.media.MediaMetadata2);
+    method public void onPlaylistMetadataChanged(androidx.media.MediaController2, androidx.media.MediaMetadata2);
+    method public void onRepeatModeChanged(androidx.media.MediaController2, int);
+    method public void onRoutesInfoChanged(androidx.media.MediaController2, java.util.List<android.os.Bundle>);
+    method public void onSeekCompleted(androidx.media.MediaController2, long);
+    method public void onShuffleModeChanged(androidx.media.MediaController2, int);
+  }
+
+  public static final class MediaController2.PlaybackInfo {
+    method public androidx.media.AudioAttributesCompat getAudioAttributes();
+    method public int getControlType();
+    method public int getCurrentVolume();
+    method public int getMaxVolume();
+    method public int getPlaybackType();
+    field public static final int PLAYBACK_TYPE_LOCAL = 1; // 0x1
+    field public static final int PLAYBACK_TYPE_REMOTE = 2; // 0x2
+  }
+
+  public class MediaItem2 {
+    method public static androidx.media.MediaItem2 fromBundle(android.os.Bundle);
+    method public androidx.media.DataSourceDesc getDataSourceDesc();
+    method public int getFlags();
+    method public java.lang.String getMediaId();
+    method public androidx.media.MediaMetadata2 getMetadata();
+    method public boolean isBrowsable();
+    method public boolean isPlayable();
+    method public void setMetadata(androidx.media.MediaMetadata2);
+    method public android.os.Bundle toBundle();
+    field public static final int FLAG_BROWSABLE = 1; // 0x1
+    field public static final int FLAG_PLAYABLE = 2; // 0x2
+  }
+
+  public static final class MediaItem2.Builder {
+    ctor public MediaItem2.Builder(int);
+    method public androidx.media.MediaItem2 build();
+    method public androidx.media.MediaItem2.Builder setDataSourceDesc(androidx.media.DataSourceDesc);
+    method public androidx.media.MediaItem2.Builder setMediaId(java.lang.String);
+    method public androidx.media.MediaItem2.Builder setMetadata(androidx.media.MediaMetadata2);
+  }
+
+  public final class MediaMetadata2 {
+    method public boolean containsKey(java.lang.String);
+    method public static androidx.media.MediaMetadata2 fromBundle(android.os.Bundle);
+    method public android.graphics.Bitmap getBitmap(java.lang.String);
+    method public android.os.Bundle getExtras();
+    method public float getFloat(java.lang.String);
+    method public long getLong(java.lang.String);
+    method public java.lang.String getMediaId();
+    method public androidx.media.Rating2 getRating(java.lang.String);
+    method public java.lang.String getString(java.lang.String);
+    method public java.lang.CharSequence getText(java.lang.String);
+    method public java.util.Set<java.lang.String> keySet();
+    method public int size();
+    method public android.os.Bundle toBundle();
+    field public static final long BT_FOLDER_TYPE_ALBUMS = 2L; // 0x2L
+    field public static final long BT_FOLDER_TYPE_ARTISTS = 3L; // 0x3L
+    field public static final long BT_FOLDER_TYPE_GENRES = 4L; // 0x4L
+    field public static final long BT_FOLDER_TYPE_MIXED = 0L; // 0x0L
+    field public static final long BT_FOLDER_TYPE_PLAYLISTS = 5L; // 0x5L
+    field public static final long BT_FOLDER_TYPE_TITLES = 1L; // 0x1L
+    field public static final long BT_FOLDER_TYPE_YEARS = 6L; // 0x6L
+    field public static final java.lang.String METADATA_KEY_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT";
+    field public static final java.lang.String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM";
+    field public static final java.lang.String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART";
+    field public static final java.lang.String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
+    field public static final java.lang.String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI";
+    field public static final java.lang.String METADATA_KEY_ART = "android.media.metadata.ART";
+    field public static final java.lang.String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST";
+    field public static final java.lang.String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI";
+    field public static final java.lang.String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR";
+    field public static final java.lang.String METADATA_KEY_BT_FOLDER_TYPE = "android.media.metadata.BT_FOLDER_TYPE";
+    field public static final java.lang.String METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION";
+    field public static final java.lang.String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER";
+    field public static final java.lang.String METADATA_KEY_DATE = "android.media.metadata.DATE";
+    field public static final java.lang.String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
+    field public static final java.lang.String METADATA_KEY_DISPLAY_DESCRIPTION = "android.media.metadata.DISPLAY_DESCRIPTION";
+    field public static final java.lang.String METADATA_KEY_DISPLAY_ICON = "android.media.metadata.DISPLAY_ICON";
+    field public static final java.lang.String METADATA_KEY_DISPLAY_ICON_URI = "android.media.metadata.DISPLAY_ICON_URI";
+    field public static final java.lang.String METADATA_KEY_DISPLAY_SUBTITLE = "android.media.metadata.DISPLAY_SUBTITLE";
+    field public static final java.lang.String METADATA_KEY_DISPLAY_TITLE = "android.media.metadata.DISPLAY_TITLE";
+    field public static final java.lang.String METADATA_KEY_DOWNLOAD_STATUS = "android.media.metadata.DOWNLOAD_STATUS";
+    field public static final java.lang.String METADATA_KEY_DURATION = "android.media.metadata.DURATION";
+    field public static final java.lang.String METADATA_KEY_EXTRAS = "android.media.metadata.EXTRAS";
+    field public static final java.lang.String METADATA_KEY_GENRE = "android.media.metadata.GENRE";
+    field public static final java.lang.String METADATA_KEY_MEDIA_ID = "android.media.metadata.MEDIA_ID";
+    field public static final java.lang.String METADATA_KEY_MEDIA_URI = "android.media.metadata.MEDIA_URI";
+    field public static final java.lang.String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS";
+    field public static final java.lang.String METADATA_KEY_RATING = "android.media.metadata.RATING";
+    field public static final java.lang.String METADATA_KEY_TITLE = "android.media.metadata.TITLE";
+    field public static final java.lang.String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
+    field public static final java.lang.String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING";
+    field public static final java.lang.String METADATA_KEY_WRITER = "android.media.metadata.WRITER";
+    field public static final java.lang.String METADATA_KEY_YEAR = "android.media.metadata.YEAR";
+    field public static final long STATUS_DOWNLOADED = 2L; // 0x2L
+    field public static final long STATUS_DOWNLOADING = 1L; // 0x1L
+    field public static final long STATUS_NOT_DOWNLOADED = 0L; // 0x0L
+  }
+
+  public static final class MediaMetadata2.Builder {
+    ctor public MediaMetadata2.Builder();
+    ctor public MediaMetadata2.Builder(androidx.media.MediaMetadata2);
+    method public androidx.media.MediaMetadata2 build();
+    method public androidx.media.MediaMetadata2.Builder putBitmap(java.lang.String, android.graphics.Bitmap);
+    method public androidx.media.MediaMetadata2.Builder putFloat(java.lang.String, float);
+    method public androidx.media.MediaMetadata2.Builder putLong(java.lang.String, long);
+    method public androidx.media.MediaMetadata2.Builder putRating(java.lang.String, androidx.media.Rating2);
+    method public androidx.media.MediaMetadata2.Builder putString(java.lang.String, java.lang.String);
+    method public androidx.media.MediaMetadata2.Builder putText(java.lang.String, java.lang.CharSequence);
+    method public androidx.media.MediaMetadata2.Builder setExtras(android.os.Bundle);
+  }
+
+  public abstract class MediaPlayerBase implements java.lang.AutoCloseable {
+    ctor public MediaPlayerBase();
+    method public abstract androidx.media.AudioAttributesCompat getAudioAttributes();
+    method public long getBufferedPosition();
+    method public abstract int getBufferingState();
+    method public abstract androidx.media.DataSourceDesc getCurrentDataSource();
+    method public long getCurrentPosition();
+    method public long getDuration();
+    method public float getMaxPlayerVolume();
+    method public float getPlaybackSpeed();
+    method public abstract int getPlayerState();
+    method public abstract float getPlayerVolume();
+    method public boolean isReversePlaybackSupported();
+    method public abstract void loopCurrent(boolean);
+    method public abstract void pause();
+    method public abstract void play();
+    method public abstract void prepare();
+    method public abstract void registerPlayerEventCallback(java.util.concurrent.Executor, androidx.media.MediaPlayerBase.PlayerEventCallback);
+    method public abstract void reset();
+    method public abstract void seekTo(long);
+    method public abstract void setAudioAttributes(androidx.media.AudioAttributesCompat);
+    method public abstract void setDataSource(androidx.media.DataSourceDesc);
+    method public abstract void setNextDataSource(androidx.media.DataSourceDesc);
+    method public abstract void setNextDataSources(java.util.List<androidx.media.DataSourceDesc>);
+    method public abstract void setPlaybackSpeed(float);
+    method public abstract void setPlayerVolume(float);
+    method public abstract void skipToNext();
+    method public abstract void unregisterPlayerEventCallback(androidx.media.MediaPlayerBase.PlayerEventCallback);
+    field public static final int BUFFERING_STATE_BUFFERING_AND_PLAYABLE = 1; // 0x1
+    field public static final int BUFFERING_STATE_BUFFERING_AND_STARVED = 2; // 0x2
+    field public static final int BUFFERING_STATE_BUFFERING_COMPLETE = 3; // 0x3
+    field public static final int BUFFERING_STATE_UNKNOWN = 0; // 0x0
+    field public static final int PLAYER_STATE_ERROR = 3; // 0x3
+    field public static final int PLAYER_STATE_IDLE = 0; // 0x0
+    field public static final int PLAYER_STATE_PAUSED = 1; // 0x1
+    field public static final int PLAYER_STATE_PLAYING = 2; // 0x2
+    field public static final long UNKNOWN_TIME = -1L; // 0xffffffffffffffffL
+  }
+
+  public static abstract class MediaPlayerBase.PlayerEventCallback {
+    ctor public MediaPlayerBase.PlayerEventCallback();
+    method public void onBufferingStateChanged(androidx.media.MediaPlayerBase, androidx.media.DataSourceDesc, int);
+    method public void onCurrentDataSourceChanged(androidx.media.MediaPlayerBase, androidx.media.DataSourceDesc);
+    method public void onMediaPrepared(androidx.media.MediaPlayerBase, androidx.media.DataSourceDesc);
+    method public void onPlaybackSpeedChanged(androidx.media.MediaPlayerBase, float);
+    method public void onPlayerStateChanged(androidx.media.MediaPlayerBase, int);
+    method public void onSeekCompleted(androidx.media.MediaPlayerBase, long);
+  }
+
+  public abstract class MediaPlaylistAgent {
+    ctor public MediaPlaylistAgent();
+    method public abstract void addPlaylistItem(int, androidx.media.MediaItem2);
+    method public abstract androidx.media.MediaItem2 getCurrentMediaItem();
+    method public androidx.media.MediaItem2 getMediaItem(androidx.media.DataSourceDesc);
+    method public abstract java.util.List<androidx.media.MediaItem2> getPlaylist();
+    method public abstract androidx.media.MediaMetadata2 getPlaylistMetadata();
+    method public abstract int getRepeatMode();
+    method public abstract int getShuffleMode();
+    method public final void notifyPlaylistChanged();
+    method public final void notifyPlaylistMetadataChanged();
+    method public final void notifyRepeatModeChanged();
+    method public final void notifyShuffleModeChanged();
+    method public final void registerPlaylistEventCallback(java.util.concurrent.Executor, androidx.media.MediaPlaylistAgent.PlaylistEventCallback);
+    method public abstract void removePlaylistItem(androidx.media.MediaItem2);
+    method public abstract void replacePlaylistItem(int, androidx.media.MediaItem2);
+    method public abstract void setPlaylist(java.util.List<androidx.media.MediaItem2>, androidx.media.MediaMetadata2);
+    method public abstract void setRepeatMode(int);
+    method public abstract void setShuffleMode(int);
+    method public abstract void skipToNextItem();
+    method public abstract void skipToPlaylistItem(androidx.media.MediaItem2);
+    method public abstract void skipToPreviousItem();
+    method public final void unregisterPlaylistEventCallback(androidx.media.MediaPlaylistAgent.PlaylistEventCallback);
+    method public abstract void updatePlaylistMetadata(androidx.media.MediaMetadata2);
+    field public static final int REPEAT_MODE_ALL = 2; // 0x2
+    field public static final int REPEAT_MODE_GROUP = 3; // 0x3
+    field public static final int REPEAT_MODE_NONE = 0; // 0x0
+    field public static final int REPEAT_MODE_ONE = 1; // 0x1
+    field public static final int SHUFFLE_MODE_ALL = 1; // 0x1
+    field public static final int SHUFFLE_MODE_GROUP = 2; // 0x2
+    field public static final int SHUFFLE_MODE_NONE = 0; // 0x0
+  }
+
+  public static abstract class MediaPlaylistAgent.PlaylistEventCallback {
+    ctor public MediaPlaylistAgent.PlaylistEventCallback();
+    method public void onPlaylistChanged(androidx.media.MediaPlaylistAgent, java.util.List<androidx.media.MediaItem2>, androidx.media.MediaMetadata2);
+    method public void onPlaylistMetadataChanged(androidx.media.MediaPlaylistAgent, androidx.media.MediaMetadata2);
+    method public void onRepeatModeChanged(androidx.media.MediaPlaylistAgent, int);
+    method public void onShuffleModeChanged(androidx.media.MediaPlaylistAgent, int);
+  }
+
+  public class MediaSession2 implements java.lang.AutoCloseable {
+    method public void addPlaylistItem(int, androidx.media.MediaItem2);
+    method public void clearOnDataSourceMissingHelper();
+    method public void close();
+    method public long getBufferedPosition();
+    method public int getBufferingState();
+    method public java.util.List<androidx.media.MediaSession2.ControllerInfo> getConnectedControllers();
+    method public androidx.media.MediaItem2 getCurrentMediaItem();
+    method public long getCurrentPosition();
+    method public long getDuration();
+    method public float getPlaybackSpeed();
+    method public androidx.media.MediaPlayerBase getPlayer();
+    method public int getPlayerState();
+    method public java.util.List<androidx.media.MediaItem2> getPlaylist();
+    method public androidx.media.MediaPlaylistAgent getPlaylistAgent();
+    method public androidx.media.MediaMetadata2 getPlaylistMetadata();
+    method public int getRepeatMode();
+    method public int getShuffleMode();
+    method public androidx.media.SessionToken2 getToken();
+    method public androidx.media.VolumeProviderCompat getVolumeProvider();
+    method public void notifyError(int, android.os.Bundle);
+    method public void notifyRoutesInfoChanged(androidx.media.MediaSession2.ControllerInfo, java.util.List<android.os.Bundle>);
+    method public void pause();
+    method public void play();
+    method public void prepare();
+    method public void removePlaylistItem(androidx.media.MediaItem2);
+    method public void replacePlaylistItem(int, androidx.media.MediaItem2);
+    method public void reset();
+    method public void seekTo(long);
+    method public void sendCustomCommand(androidx.media.SessionCommand2, android.os.Bundle);
+    method public void sendCustomCommand(androidx.media.MediaSession2.ControllerInfo, androidx.media.SessionCommand2, android.os.Bundle, android.os.ResultReceiver);
+    method public void setAllowedCommands(androidx.media.MediaSession2.ControllerInfo, androidx.media.SessionCommandGroup2);
+    method public void setAudioFocusRequest(android.media.AudioFocusRequest);
+    method public void setCustomLayout(androidx.media.MediaSession2.ControllerInfo, java.util.List<androidx.media.MediaSession2.CommandButton>);
+    method public void setOnDataSourceMissingHelper(androidx.media.MediaSession2.OnDataSourceMissingHelper);
+    method public void setPlaybackSpeed(float);
+    method public void setPlaylist(java.util.List<androidx.media.MediaItem2>, androidx.media.MediaMetadata2);
+    method public void setRepeatMode(int);
+    method public void setShuffleMode(int);
+    method public void skipToNextItem();
+    method public void skipToPlaylistItem(androidx.media.MediaItem2);
+    method public void skipToPreviousItem();
+    method public void updatePlayer(androidx.media.MediaPlayerBase, androidx.media.MediaPlaylistAgent, androidx.media.VolumeProviderCompat);
+    method public void updatePlaylistMetadata(androidx.media.MediaMetadata2);
+    field public static final int ERROR_CODE_ACTION_ABORTED = 10; // 0xa
+    field public static final int ERROR_CODE_APP_ERROR = 1; // 0x1
+    field public static final int ERROR_CODE_AUTHENTICATION_EXPIRED = 3; // 0x3
+    field public static final int ERROR_CODE_CONCURRENT_STREAM_LIMIT = 5; // 0x5
+    field public static final int ERROR_CODE_CONTENT_ALREADY_PLAYING = 8; // 0x8
+    field public static final int ERROR_CODE_END_OF_QUEUE = 11; // 0xb
+    field public static final int ERROR_CODE_NOT_AVAILABLE_IN_REGION = 7; // 0x7
+    field public static final int ERROR_CODE_NOT_SUPPORTED = 2; // 0x2
+    field public static final int ERROR_CODE_PARENTAL_CONTROL_RESTRICTED = 6; // 0x6
+    field public static final int ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED = 4; // 0x4
+    field public static final int ERROR_CODE_SETUP_REQUIRED = 12; // 0xc
+    field public static final int ERROR_CODE_SKIP_LIMIT_REACHED = 9; // 0x9
+    field public static final int ERROR_CODE_UNKNOWN_ERROR = 0; // 0x0
+  }
+
+  public static final class MediaSession2.Builder {
+    ctor public MediaSession2.Builder(android.content.Context);
+    method public androidx.media.MediaSession2 build();
+    method public androidx.media.MediaSession2.Builder setId(java.lang.String);
+    method public androidx.media.MediaSession2.Builder setPlayer(androidx.media.MediaPlayerBase);
+    method public androidx.media.MediaSession2.Builder setPlaylistAgent(androidx.media.MediaPlaylistAgent);
+    method public androidx.media.MediaSession2.Builder setSessionActivity(android.app.PendingIntent);
+    method public androidx.media.MediaSession2.Builder setSessionCallback(java.util.concurrent.Executor, androidx.media.MediaSession2.SessionCallback);
+    method public androidx.media.MediaSession2.Builder setVolumeProvider(androidx.media.VolumeProviderCompat);
+  }
+
+  public static final class MediaSession2.CommandButton {
+    method public androidx.media.SessionCommand2 getCommand();
+    method public java.lang.String getDisplayName();
+    method public android.os.Bundle getExtras();
+    method public int getIconResId();
+    method public boolean isEnabled();
+  }
+
+  public static final class MediaSession2.CommandButton.Builder {
+    ctor public MediaSession2.CommandButton.Builder();
+    method public androidx.media.MediaSession2.CommandButton build();
+    method public androidx.media.MediaSession2.CommandButton.Builder setCommand(androidx.media.SessionCommand2);
+    method public androidx.media.MediaSession2.CommandButton.Builder setDisplayName(java.lang.String);
+    method public androidx.media.MediaSession2.CommandButton.Builder setEnabled(boolean);
+    method public androidx.media.MediaSession2.CommandButton.Builder setExtras(android.os.Bundle);
+    method public androidx.media.MediaSession2.CommandButton.Builder setIconResId(int);
+  }
+
+  public static final class MediaSession2.ControllerInfo {
+    method public java.lang.String getPackageName();
+    method public int getUid();
+  }
+
+  public static abstract interface MediaSession2.OnDataSourceMissingHelper {
+    method public abstract androidx.media.DataSourceDesc onDataSourceMissing(androidx.media.MediaSession2, androidx.media.MediaItem2);
+  }
+
+  public static abstract class MediaSession2.SessionCallback {
+    ctor public MediaSession2.SessionCallback();
+    method public void onBufferingStateChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerBase, androidx.media.MediaItem2, int);
+    method public boolean onCommandRequest(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, androidx.media.SessionCommand2);
+    method public androidx.media.SessionCommandGroup2 onConnect(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo);
+    method public void onCurrentMediaItemChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerBase, androidx.media.MediaItem2);
+    method public void onCustomCommand(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, androidx.media.SessionCommand2, android.os.Bundle, android.os.ResultReceiver);
+    method public void onDisconnected(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo);
+    method public void onFastForward(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo);
+    method public void onMediaPrepared(androidx.media.MediaSession2, androidx.media.MediaPlayerBase, androidx.media.MediaItem2);
+    method public void onPlayFromMediaId(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, java.lang.String, android.os.Bundle);
+    method public void onPlayFromSearch(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, java.lang.String, android.os.Bundle);
+    method public void onPlayFromUri(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, android.net.Uri, android.os.Bundle);
+    method public void onPlaybackSpeedChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerBase, float);
+    method public void onPlayerStateChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerBase, int);
+    method public void onPlaylistChanged(androidx.media.MediaSession2, androidx.media.MediaPlaylistAgent, java.util.List<androidx.media.MediaItem2>, androidx.media.MediaMetadata2);
+    method public void onPlaylistMetadataChanged(androidx.media.MediaSession2, androidx.media.MediaPlaylistAgent, androidx.media.MediaMetadata2);
+    method public void onPrepareFromMediaId(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, java.lang.String, android.os.Bundle);
+    method public void onPrepareFromSearch(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, java.lang.String, android.os.Bundle);
+    method public void onPrepareFromUri(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, android.net.Uri, android.os.Bundle);
+    method public void onRepeatModeChanged(androidx.media.MediaSession2, androidx.media.MediaPlaylistAgent, int);
+    method public void onRewind(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo);
+    method public void onSeekCompleted(androidx.media.MediaSession2, androidx.media.MediaPlayerBase, long);
+    method public void onSelectRoute(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, android.os.Bundle);
+    method public void onSetRating(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, java.lang.String, androidx.media.Rating2);
+    method public void onShuffleModeChanged(androidx.media.MediaSession2, androidx.media.MediaPlaylistAgent, int);
+    method public void onSubscribeRoutesInfo(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo);
+    method public void onUnsubscribeRoutesInfo(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo);
+  }
+
+  public final class Rating2 {
+    method public static androidx.media.Rating2 fromBundle(android.os.Bundle);
+    method public float getPercentRating();
+    method public int getRatingStyle();
+    method public float getStarRating();
+    method public boolean hasHeart();
+    method public boolean isRated();
+    method public boolean isThumbUp();
+    method public static androidx.media.Rating2 newHeartRating(boolean);
+    method public static androidx.media.Rating2 newPercentageRating(float);
+    method public static androidx.media.Rating2 newStarRating(int, float);
+    method public static androidx.media.Rating2 newThumbRating(boolean);
+    method public static androidx.media.Rating2 newUnratedRating(int);
+    method public android.os.Bundle toBundle();
+    field public static final int RATING_3_STARS = 3; // 0x3
+    field public static final int RATING_4_STARS = 4; // 0x4
+    field public static final int RATING_5_STARS = 5; // 0x5
+    field public static final int RATING_HEART = 1; // 0x1
+    field public static final int RATING_NONE = 0; // 0x0
+    field public static final int RATING_PERCENTAGE = 6; // 0x6
+    field public static final int RATING_THUMB_UP_DOWN = 2; // 0x2
+  }
+
+  public final class SessionCommand2 {
+    ctor public SessionCommand2(int);
+    ctor public SessionCommand2(java.lang.String, android.os.Bundle);
+    method public int getCommandCode();
+    method public java.lang.String getCustomCommand();
+    method public android.os.Bundle getExtras();
+    field public static final int COMMAND_CODE_CUSTOM = 0; // 0x0
+    field public static final int COMMAND_CODE_PLAYBACK_PAUSE = 2; // 0x2
+    field public static final int COMMAND_CODE_PLAYBACK_PLAY = 1; // 0x1
+    field public static final int COMMAND_CODE_PLAYBACK_PREPARE = 6; // 0x6
+    field public static final int COMMAND_CODE_PLAYBACK_RESET = 3; // 0x3
+    field public static final int COMMAND_CODE_PLAYBACK_SEEK_TO = 9; // 0x9
+    field public static final int COMMAND_CODE_PLAYBACK_SET_SPEED = 39; // 0x27
+    field public static final int COMMAND_CODE_PLAYLIST_ADD_ITEM = 15; // 0xf
+    field public static final int COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM = 20; // 0x14
+    field public static final int COMMAND_CODE_PLAYLIST_GET_LIST = 18; // 0x12
+    field public static final int COMMAND_CODE_PLAYLIST_GET_LIST_METADATA = 20; // 0x14
+    field public static final int COMMAND_CODE_PLAYLIST_REMOVE_ITEM = 16; // 0x10
+    field public static final int COMMAND_CODE_PLAYLIST_REPLACE_ITEM = 17; // 0x11
+    field public static final int COMMAND_CODE_PLAYLIST_SET_LIST = 19; // 0x13
+    field public static final int COMMAND_CODE_PLAYLIST_SET_LIST_METADATA = 21; // 0x15
+    field public static final int COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE = 14; // 0xe
+    field public static final int COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE = 13; // 0xd
+    field public static final int COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM = 4; // 0x4
+    field public static final int COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM = 12; // 0xc
+    field public static final int COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM = 5; // 0x5
+    field public static final int COMMAND_CODE_SESSION_FAST_FORWARD = 7; // 0x7
+    field public static final int COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID = 22; // 0x16
+    field public static final int COMMAND_CODE_SESSION_PLAY_FROM_SEARCH = 24; // 0x18
+    field public static final int COMMAND_CODE_SESSION_PLAY_FROM_URI = 23; // 0x17
+    field public static final int COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID = 25; // 0x19
+    field public static final int COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH = 27; // 0x1b
+    field public static final int COMMAND_CODE_SESSION_PREPARE_FROM_URI = 26; // 0x1a
+    field public static final int COMMAND_CODE_SESSION_REWIND = 8; // 0x8
+    field public static final int COMMAND_CODE_SESSION_SELECT_ROUTE = 38; // 0x26
+    field public static final int COMMAND_CODE_SESSION_SET_RATING = 28; // 0x1c
+    field public static final int COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO = 36; // 0x24
+    field public static final int COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO = 37; // 0x25
+    field public static final int COMMAND_CODE_VOLUME_ADJUST_VOLUME = 11; // 0xb
+    field public static final int COMMAND_CODE_VOLUME_SET_VOLUME = 10; // 0xa
+  }
+
+  public final class SessionCommandGroup2 {
+    ctor public SessionCommandGroup2();
+    ctor public SessionCommandGroup2(androidx.media.SessionCommandGroup2);
+    method public void addAllPredefinedCommands();
+    method public void addCommand(androidx.media.SessionCommand2);
+    method public void addCommand(int);
+    method public java.util.Set<androidx.media.SessionCommand2> getCommands();
+    method public boolean hasCommand(androidx.media.SessionCommand2);
+    method public boolean hasCommand(int);
+    method public void removeCommand(androidx.media.SessionCommand2);
+    method public void removeCommand(int);
+  }
+
+  public final class SessionToken2 {
+    method public static androidx.media.SessionToken2 fromBundle(android.os.Bundle);
+    method public java.lang.String getId();
+    method public java.lang.String getPackageName();
+    method public java.lang.String getServiceName();
+    method public int getType();
+    method public int getUid();
+    method public android.os.Bundle toBundle();
+    field public static final int TYPE_SESSION = 0; // 0x0
+  }
+
   public abstract class VolumeProviderCompat {
     ctor public VolumeProviderCompat(int, int, int);
     method public final int getCurrentVolume();
diff --git a/media/src/androidTest/java/androidx/media/MediaPlayer2Test.java b/media/src/androidTest/java/androidx/media/MediaPlayer2Test.java
index cc409b7..04cc270 100644
--- a/media/src/androidTest/java/androidx/media/MediaPlayer2Test.java
+++ b/media/src/androidTest/java/androidx/media/MediaPlayer2Test.java
@@ -29,6 +29,7 @@
 import android.media.MediaRecorder;
 import android.media.MediaTimestamp;
 import android.media.PlaybackParams;
+import android.media.SubtitleData;
 import android.media.SyncParams;
 import android.media.audiofx.AudioEffect;
 import android.media.audiofx.Visualizer;
@@ -36,12 +37,15 @@
 import android.os.Build;
 import android.os.Environment;
 import android.support.test.filters.LargeTest;
+import android.support.test.filters.MediumTest;
 import android.support.test.filters.SdkSuppress;
 import android.support.test.runner.AndroidJUnit4;
 import android.util.Log;
 
 import androidx.media.test.R;
 
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -52,9 +56,11 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Vector;
+import java.util.concurrent.BlockingDeque;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingDeque;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
@@ -77,14 +83,16 @@
     private File mOutFile;
     private Camera mCamera;
 
+    @Before
     @Override
-    public void setUp() throws Exception {
+    public void setUp() throws Throwable {
         super.setUp();
         mRecordedFilePath = new File(Environment.getExternalStorageDirectory(),
                 "mediaplayer_record.out").getAbsolutePath();
         mOutFile = new File(mRecordedFilePath);
     }
 
+    @After
     @Override
     public void tearDown() throws Exception {
         super.tearDown();
@@ -93,61 +101,8 @@
         }
     }
 
-    // Bug 13652927
-    public void testVorbisCrash() throws Exception {
-        MediaPlayer2 mp = mPlayer;
-        MediaPlayer2 mp2 = mPlayer2;
-        AssetFileDescriptor afd2 = mResources.openRawResourceFd(R.raw.testmp3_2);
-        mp2.setDataSource(new DataSourceDesc.Builder()
-                .setDataSource(afd2.getFileDescriptor(), afd2.getStartOffset(), afd2.getLength())
-                .build());
-        final Monitor onPrepareCalled = new Monitor();
-        final Monitor onErrorCalled = new Monitor();
-        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
-            @Override
-            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
-                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
-                    onPrepareCalled.signal();
-                }
-            }
-
-            @Override
-            public void onError(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
-                onErrorCalled.signal();
-            }
-        };
-        mp2.setMediaPlayer2EventCallback(mExecutor, ecb);
-        mp2.prepare();
-        onPrepareCalled.waitForSignal();
-        afd2.close();
-        mp2.clearMediaPlayer2EventCallback();
-
-        mp2.loopCurrent(true);
-        mp2.play();
-
-        for (int i = 0; i < 20; i++) {
-            try {
-                AssetFileDescriptor afd = mResources.openRawResourceFd(R.raw.bug13652927);
-                mp.setDataSource(new DataSourceDesc.Builder()
-                        .setDataSource(afd.getFileDescriptor(), afd.getStartOffset(),
-                            afd.getLength())
-                        .build());
-                mp.setMediaPlayer2EventCallback(mExecutor, ecb);
-                onPrepareCalled.reset();
-                mp.prepare();
-                onErrorCalled.waitForSignal();
-                afd.close();
-            } catch (Exception e) {
-                // expected to fail
-                Log.i("@@@", "failed: " + e);
-            }
-            Thread.sleep(500);
-            assertTrue("media player died",
-                    mp2.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
-            mp.reset();
-        }
-    }
-
+    @Test
+    @MediumTest
     public void testPlayNullSourcePath() throws Exception {
         final Monitor onSetDataSourceCalled = new Monitor();
         MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
@@ -168,6 +123,8 @@
         onSetDataSourceCalled.waitForSignal();
     }
 
+    @Test
+    @LargeTest
     public void testPlayAudioFromDataURI() throws Exception {
         final int mp3Duration = 34909;
         final int tolerance = 70;
@@ -216,9 +173,6 @@
                     .setLegacyStreamType(AudioManager.STREAM_MUSIC)
                     .build();
             mp.setAudioAttributes(attributes);
-            /* FIXME: ensure screen is on while testing.
-            mp.setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK);
-            */
 
             assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
             onPlayCalled.reset();
@@ -280,6 +234,8 @@
         }
     }
 
+    @Test
+    @LargeTest
     public void testPlayAudio() throws Exception {
         final int resid = R.raw.testmp3_2;
         final int mp3Duration = 34909;
@@ -410,7 +366,6 @@
                         .setInternalLegacyStreamType(AudioManager.STREAM_MUSIC)
                         .build();
                 mp.setAudioAttributes(attributes);
-                mp.setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK);
 
                 assertFalse(mp.isPlaying());
                 onPlayCalled.reset();
@@ -441,6 +396,8 @@
     }
     */
 
+    @Test
+    @LargeTest
     public void testPlayAudioLooping() throws Exception {
         final int resid = R.raw.testmp3;
 
@@ -458,8 +415,10 @@
                         @Override
                         public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd,
                                 int what, int extra) {
-                            Log.i("@@@", "got oncompletion");
-                            onCompletionCalled.signal();
+                            if (what == MediaPlayer2.MEDIA_INFO_PLAYBACK_COMPLETE) {
+                                Log.i("@@@", "got oncompletion");
+                                onCompletionCalled.signal();
+                            }
                         }
 
                         @Override
@@ -496,6 +455,8 @@
         }
     }
 
+    @Test
+    @LargeTest
     public void testPlayMidi() throws Exception {
         final int resid = R.raw.midi8sec;
         final int midiDuration = 8000;
@@ -1448,14 +1409,14 @@
         }
     }
 
+    @Test
+    @LargeTest
     public void testDeselectTrackForSubtitleTracks() throws Throwable {
         if (!checkLoadResource(R.raw.testvideo_with_2_subtitle_tracks)) {
             return; // skip;
         }
 
-        /* FIXME: find out counter part of waitForIdleSync.
-        getInstrumentation().waitForIdleSync();
-        */
+        mInstrumentation.waitForIdleSync();
 
         MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
             @Override
@@ -1478,21 +1439,18 @@
                     mOnDeselectTrackCalled.signal();
                 }
             }
-        };
-        synchronized (mEventCbLock) {
-            mEventCallbacks.add(ecb);
-        }
 
-        /* TODO: uncomment once API is available in supportlib.
-        mPlayer.setOnSubtitleDataListener(new MediaPlayer2.OnSubtitleDataListener() {
             @Override
-            public void onSubtitleData(MediaPlayer2 mp, SubtitleData data) {
+            public void onSubtitleData(
+                    MediaPlayer2 mp, DataSourceDesc dsd, SubtitleData data) {
                 if (data != null && data.getData() != null) {
                     mOnSubtitleDataCalled.signal();
                 }
             }
-        });
-        */
+        };
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(ecb);
+        }
 
         mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
 
@@ -1535,22 +1493,13 @@
         mPlayer.reset();
     }
 
+    @Test
+    @LargeTest
     public void testChangeSubtitleTrack() throws Throwable {
         if (!checkLoadResource(R.raw.testvideo_with_2_subtitle_tracks)) {
             return; // skip;
         }
 
-        /* TODO: uncomment once API is available in supportlib.
-        mPlayer.setOnSubtitleDataListener(new MediaPlayer2.OnSubtitleDataListener() {
-            @Override
-            public void onSubtitleData(MediaPlayer2 mp, SubtitleData data) {
-                if (data != null && data.getData() != null) {
-                    mOnSubtitleDataCalled.signal();
-                }
-            }
-        });
-        */
-
         MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
             @Override
             public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
@@ -1567,6 +1516,14 @@
                     mOnPlayCalled.signal();
                 }
             }
+
+            @Override
+            public void onSubtitleData(
+                    MediaPlayer2 mp, DataSourceDesc dsd, SubtitleData data) {
+                if (data != null && data.getData() != null) {
+                    mOnSubtitleDataCalled.signal();
+                }
+            }
         };
         synchronized (mEventCbLock) {
             mEventCallbacks.add(ecb);
@@ -1656,10 +1613,77 @@
         mPlayer.reset();
     }
 
+    @Test
+    @LargeTest
+    public void testMediaTimeDiscontinuity() throws Exception {
+        if (!checkLoadResource(
+                R.raw.bbb_s1_320x240_mp4_h264_mp2_800kbps_30fps_aac_lc_5ch_240kbps_44100hz)) {
+            return; // skip
+        }
+
+        final BlockingDeque<MediaTimestamp> timestamps = new LinkedBlockingDeque<>();
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+                    mOnSeekCompleteCalled.signal();
+                }
+            }
+            @Override
+            public void onMediaTimeDiscontinuity(
+                    MediaPlayer2 mp, DataSourceDesc dsd, MediaTimestamp timestamp) {
+                timestamps.add(timestamp);
+                mOnMediaTimeDiscontinuityCalled.signal();
+            }
+        };
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(ecb);
+        }
+
+        mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+        mPlayer.prepare();
+
+        // Timestamp needs to be reported when playback starts.
+        mOnMediaTimeDiscontinuityCalled.reset();
+        mPlayer.play();
+        do {
+            assertTrue(mOnMediaTimeDiscontinuityCalled.waitForSignal(1000));
+        } while (Math.abs(timestamps.getLast().getMediaClockRate() - 1.0f) > 0.01f);
+
+        // Timestamp needs to be reported when seeking is done.
+        mOnSeekCompleteCalled.reset();
+        mOnMediaTimeDiscontinuityCalled.reset();
+        mPlayer.seekTo(3000);
+        mOnSeekCompleteCalled.waitForSignal();
+        do {
+            assertTrue(mOnMediaTimeDiscontinuityCalled.waitForSignal(1000));
+        } while (Math.abs(timestamps.getLast().getMediaClockRate() - 1.0f) > 0.01f);
+
+        // Timestamp needs to be updated when playback rate changes.
+        mOnMediaTimeDiscontinuityCalled.reset();
+        mPlayer.setPlaybackParams(new PlaybackParams().setSpeed(0.5f));
+        mOnMediaTimeDiscontinuityCalled.waitForSignal();
+        do {
+            assertTrue(mOnMediaTimeDiscontinuityCalled.waitForSignal(1000));
+        } while (Math.abs(timestamps.getLast().getMediaClockRate() - 0.5f) > 0.01f);
+
+        // Timestamp needs to be updated when player is paused.
+        mOnMediaTimeDiscontinuityCalled.reset();
+        mPlayer.pause();
+        mOnMediaTimeDiscontinuityCalled.waitForSignal();
+        do {
+            assertTrue(mOnMediaTimeDiscontinuityCalled.waitForSignal(1000));
+        } while (Math.abs(timestamps.getLast().getMediaClockRate() - 0.0f) > 0.01f);
+
+        mPlayer.reset();
+    }
+
     /*
      *  This test assumes the resources being tested are between 8 and 14 seconds long
      *  The ones being used here are 10 seconds long.
      */
+    @Test
+    @LargeTest
     public void testResumeAtEnd() throws Throwable {
         int testsRun = testResumeAtEnd(R.raw.loudsoftmp3)
                 + testResumeAtEnd(R.raw.loudsoftwav)
diff --git a/media/src/androidTest/java/androidx/media/MediaPlayer2TestBase.java b/media/src/androidTest/java/androidx/media/MediaPlayer2TestBase.java
index 34c464b..41fef64 100644
--- a/media/src/androidTest/java/androidx/media/MediaPlayer2TestBase.java
+++ b/media/src/androidTest/java/androidx/media/MediaPlayer2TestBase.java
@@ -15,20 +15,31 @@
  */
 package androidx.media;
 
+import static android.content.Context.KEYGUARD_SERVICE;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import android.app.Instrumentation;
+import android.app.KeyguardManager;
 import android.content.Context;
 import android.content.res.AssetFileDescriptor;
 import android.content.res.Resources;
 import android.media.AudioManager;
 import android.media.MediaTimestamp;
+import android.media.SubtitleData;
 import android.media.TimedMetaData;
 import android.net.Uri;
+import android.os.PersistableBundle;
+import android.os.PowerManager;
+import android.support.test.InstrumentationRegistry;
 import android.support.test.rule.ActivityTestRule;
 import android.view.SurfaceHolder;
+import android.view.WindowManager;
+
+import androidx.annotation.CallSuper;
 
 import org.junit.After;
 import org.junit.Before;
@@ -39,6 +50,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.logging.Logger;
@@ -63,6 +75,7 @@
     protected Monitor mOnCompletionCalled = new Monitor();
     protected Monitor mOnInfoCalled = new Monitor();
     protected Monitor mOnErrorCalled = new Monitor();
+    protected Monitor mOnMediaTimeDiscontinuityCalled = new Monitor();
     protected int mCallStatus;
 
     protected Context mContext;
@@ -73,34 +86,35 @@
     protected MediaPlayer2 mPlayer = null;
     protected MediaPlayer2 mPlayer2 = null;
     protected MediaStubActivity mActivity;
+    protected Instrumentation mInstrumentation;
 
     protected final Object mEventCbLock = new Object();
-    protected List<MediaPlayer2.MediaPlayer2EventCallback> mEventCallbacks =
-            new ArrayList<MediaPlayer2.MediaPlayer2EventCallback>();
+    protected List<MediaPlayer2.MediaPlayer2EventCallback> mEventCallbacks = new ArrayList<>();
     protected final Object mEventCbLock2 = new Object();
-    protected List<MediaPlayer2.MediaPlayer2EventCallback> mEventCallbacks2 =
-            new ArrayList<MediaPlayer2.MediaPlayer2EventCallback>();
+    protected List<MediaPlayer2.MediaPlayer2EventCallback> mEventCallbacks2 = new ArrayList<>();
 
     @Rule
     public ActivityTestRule<MediaStubActivity> mActivityRule =
             new ActivityTestRule<>(MediaStubActivity.class);
+    public PowerManager.WakeLock mScreenLock;
+    private KeyguardManager mKeyguardManager;
 
     // convenience functions to create MediaPlayer2
-    protected static MediaPlayer2 createMediaPlayer2(Context context, Uri uri) {
+    protected MediaPlayer2 createMediaPlayer2(Context context, Uri uri) {
         return createMediaPlayer2(context, uri, null);
     }
 
-    protected static MediaPlayer2 createMediaPlayer2(Context context, Uri uri,
+    protected MediaPlayer2 createMediaPlayer2(Context context, Uri uri,
             SurfaceHolder holder) {
         AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
         int s = am.generateAudioSessionId();
         return createMediaPlayer2(context, uri, holder, null, s > 0 ? s : 0);
     }
 
-    protected static MediaPlayer2 createMediaPlayer2(Context context, Uri uri, SurfaceHolder holder,
+    protected MediaPlayer2 createMediaPlayer2(Context context, Uri uri, SurfaceHolder holder,
             AudioAttributesCompat audioAttributes, int audioSessionId) {
         try {
-            MediaPlayer2 mp = MediaPlayer2.create();
+            MediaPlayer2 mp = createMediaPlayer2OnUiThread();
             final AudioAttributesCompat aa = audioAttributes != null ? audioAttributes :
                     new AudioAttributesCompat.Builder().build();
             mp.setAudioAttributes(aa);
@@ -142,13 +156,13 @@
         return null;
     }
 
-    protected static MediaPlayer2 createMediaPlayer2(Context context, int resid) {
+    protected MediaPlayer2 createMediaPlayer2(Context context, int resid) {
         AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
         int s = am.generateAudioSessionId();
         return createMediaPlayer2(context, resid, null, s > 0 ? s : 0);
     }
 
-    protected static MediaPlayer2 createMediaPlayer2(Context context, int resid,
+    protected MediaPlayer2 createMediaPlayer2(Context context, int resid,
             AudioAttributesCompat audioAttributes, int audioSessionId) {
         try {
             AssetFileDescriptor afd = context.getResources().openRawResourceFd(resid);
@@ -156,7 +170,7 @@
                 return null;
             }
 
-            MediaPlayer2 mp = MediaPlayer2.create();
+            MediaPlayer2 mp = createMediaPlayer2OnUiThread();
 
             final AudioAttributesCompat aa = audioAttributes != null ? audioAttributes :
                     new AudioAttributesCompat.Builder().build();
@@ -202,6 +216,20 @@
         return null;
     }
 
+    private MediaPlayer2 createMediaPlayer2OnUiThread() {
+        final MediaPlayer2[] mp = new MediaPlayer2[1];
+        try {
+            mActivityRule.runOnUiThread(new Runnable() {
+                public void run() {
+                    mp[0] = MediaPlayer2.create();
+                }
+            });
+        } catch (Throwable throwable) {
+            fail("Failed to create MediaPlayer2 instance on UI thread.");
+        }
+        return mp[0];
+    }
+
     public static class Monitor {
         private int mNumSignal;
 
@@ -255,8 +283,24 @@
     }
 
     @Before
-    public void setUp() throws Exception {
+    @CallSuper
+    public void setUp() throws Throwable {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mKeyguardManager = (KeyguardManager)
+                mInstrumentation.getTargetContext().getSystemService(KEYGUARD_SERVICE);
         mActivity = mActivityRule.getActivity();
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                // Keep screen on while testing.
+                mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+                mActivity.setTurnScreenOn(true);
+                mActivity.setShowWhenLocked(true);
+                mKeyguardManager.requestDismissKeyguard(mActivity, null);
+            }
+        });
+        mInstrumentation.waitForIdleSync();
+
         try {
             mActivityRule.runOnUiThread(new Runnable() {
                 public void run() {
@@ -277,6 +321,7 @@
     }
 
     @After
+    @CallSuper
     public void tearDown() throws Exception {
         if (mPlayer != null) {
             mPlayer.close();
@@ -340,11 +385,11 @@
             }
 
             @Override
-            public void onMediaTimeChanged(MediaPlayer2 mp, DataSourceDesc dsd,
+            public void onMediaTimeDiscontinuity(MediaPlayer2 mp, DataSourceDesc dsd,
                     MediaTimestamp timestamp) {
                 synchronized (cbLock) {
                     for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
-                        ecb.onMediaTimeChanged(mp, dsd, timestamp);
+                        ecb.onMediaTimeDiscontinuity(mp, dsd, timestamp);
                     }
                 }
             }
@@ -357,6 +402,15 @@
                     }
                 }
             }
+            @Override
+            public  void onSubtitleData(MediaPlayer2 mp, DataSourceDesc dsd,
+                    final SubtitleData data) {
+                synchronized (cbLock) {
+                    for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
+                        ecb.onSubtitleData(mp, dsd, data);
+                    }
+                }
+            }
         });
     }
 
@@ -486,11 +540,7 @@
 
         boolean audioOnly = (width != null && width.intValue() == -1)
                 || (height != null && height.intValue() == -1);
-
         mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
-        /* FIXME: ensure that screen is on in activity level.
-        mPlayer.setScreenOnWhilePlaying(true);
-        */
 
         synchronized (mEventCbLock) {
             mEventCallbacks.add(new MediaPlayer2.MediaPlayer2EventCallback() {
@@ -562,6 +612,42 @@
             Thread.sleep(playTime);
         }
 
+        // validate a few MediaMetrics.
+        PersistableBundle metrics = mPlayer.getMetrics();
+        if (metrics == null) {
+            fail("MediaPlayer.getMetrics() returned null metrics");
+        } else if (metrics.isEmpty()) {
+            fail("MediaPlayer.getMetrics() returned empty metrics");
+        } else {
+
+            int size = metrics.size();
+            Set<String> keys = metrics.keySet();
+
+            if (keys == null) {
+                fail("MediaMetricsSet returned no keys");
+            } else if (keys.size() != size) {
+                fail("MediaMetricsSet.keys().size() mismatch MediaMetricsSet.size()");
+            }
+
+            // we played something; so one of these should be non-null
+            String vmime = metrics.getString(MediaPlayer2.MetricsConstants.MIME_TYPE_VIDEO, null);
+            String amime = metrics.getString(MediaPlayer2.MetricsConstants.MIME_TYPE_AUDIO, null);
+            if (vmime == null && amime == null) {
+                fail("getMetrics() returned neither video nor audio mime value");
+            }
+
+            long duration = metrics.getLong(MediaPlayer2.MetricsConstants.DURATION, -2);
+            if (duration == -2) {
+                fail("getMetrics() didn't return a duration");
+            }
+            long playing = metrics.getLong(MediaPlayer2.MetricsConstants.PLAYING, -2);
+            if (playing == -2) {
+                fail("getMetrics() didn't return a playing time");
+            }
+            if (!keys.contains(MediaPlayer2.MetricsConstants.PLAYING)) {
+                fail("MediaMetricsSet.keys() missing: " + MediaPlayer2.MetricsConstants.PLAYING);
+            }
+        }
         mPlayer.reset();
     }
 
diff --git a/media/src/main/java/androidx/media/DataSourceDesc.java b/media/src/main/java/androidx/media/DataSourceDesc.java
index 7a4fe5c..f76f651 100644
--- a/media/src/main/java/androidx/media/DataSourceDesc.java
+++ b/media/src/main/java/androidx/media/DataSourceDesc.java
@@ -16,14 +16,11 @@
 
 package androidx.media;
 
-import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
 import android.content.Context;
 import android.net.Uri;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
 import androidx.core.util.Preconditions;
 
 import java.io.FileDescriptor;
@@ -36,16 +33,12 @@
 import java.util.Map;
 
 /**
- * @hide
- * Structure for data source descriptor.
+ * Structure for data source descriptor. Used by {@link MediaItem2}.
+ * <p>
+ * Users should use {@link Builder} to change {@link DataSourceDesc}.
  *
- * Used by {@link MediaPlayer2#setDataSource(DataSourceDesc)}
- * to set data source for playback.
- *
- * <p>Users should use {@link Builder} to change {@link DataSourceDesc}.
- *
+ * @see MediaItem2
  */
-@RestrictTo(LIBRARY_GROUP)
 public final class DataSourceDesc {
     /* No data source has been set yet */
     public static final int TYPE_NONE     = 0;
@@ -56,8 +49,23 @@
     /* data source is type of Uri */
     public static final int TYPE_URI      = 3;
 
-    // intentionally less than long.MAX_VALUE
-    public static final long LONG_MAX = 0x7ffffffffffffffL;
+    // intentionally less than long.MAX_VALUE.
+    // Declare this first to avoid 'illegal forward reference'.
+    private static final long LONG_MAX = 0x7ffffffffffffffL;
+
+    /**
+     * Used when a position is unknown.
+     *
+     * @see #getEndPosition()
+     */
+    public static final long POSITION_UNKNOWN = LONG_MAX;
+
+    /**
+     * Used when the length of file descriptor is unknown.
+     *
+     * @see #getFileDescriptorLength()
+     */
+    public static final long FD_LENGTH_UNKNOWN = LONG_MAX;
 
     private int mType = TYPE_NONE;
 
@@ -65,7 +73,7 @@
 
     private FileDescriptor mFD;
     private long mFDOffset = 0;
-    private long mFDLength = LONG_MAX;
+    private long mFDLength = FD_LENGTH_UNKNOWN;
 
     private Uri mUri;
     private Map<String, String> mUriHeader;
@@ -74,7 +82,7 @@
 
     private String mMediaId;
     private long mStartPositionMs = 0;
-    private long mEndPositionMs = LONG_MAX;
+    private long mEndPositionMs = POSITION_UNKNOWN;
 
     private DataSourceDesc() {
     }
@@ -83,7 +91,7 @@
      * Return the media Id of data source.
      * @return the media Id of data source
      */
-    public String getMediaId() {
+    public @Nullable String getMediaId() {
         return mMediaId;
     }
 
@@ -97,7 +105,7 @@
 
     /**
      * Return the position in milliseconds at which the playback will end.
-     * -1 means ending at the end of source content.
+     * {@link #POSITION_UNKNOWN} means ending at the end of source content.
      * @return the position in milliseconds at which the playback will end
      */
     public long getEndPosition() {
@@ -117,7 +125,7 @@
      * It's meaningful only when {@code getType} returns {@link #TYPE_CALLBACK}.
      * @return the Media2DataSource of this data source
      */
-    public Media2DataSource getMedia2DataSource() {
+    public @Nullable Media2DataSource getMedia2DataSource() {
         return mMedia2DataSource;
     }
 
@@ -126,7 +134,7 @@
      * It's meaningful only when {@code getType} returns {@link #TYPE_FD}.
      * @return the FileDescriptor of this data source
      */
-    public FileDescriptor getFileDescriptor() {
+    public @Nullable FileDescriptor getFileDescriptor() {
         return mFD;
     }
 
@@ -143,7 +151,7 @@
     /**
      * Return the content length associated with the FileDescriptor of this data source.
      * It's meaningful only when {@code getType} returns {@link #TYPE_FD}.
-     * -1 means same as the length of source content.
+     * {@link #FD_LENGTH_UNKNOWN} means same as the length of source content.
      * @return the content length associated with the FileDescriptor of this data source
      */
     public long getFileDescriptorLength() {
@@ -155,7 +163,7 @@
      * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
      * @return the Uri of this data source
      */
-    public Uri getUri() {
+    public @Nullable Uri getUri() {
         return mUri;
     }
 
@@ -164,7 +172,7 @@
      * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
      * @return the Uri headers of this data source
      */
-    public Map<String, String> getUriHeaders() {
+    public @Nullable Map<String, String> getUriHeaders() {
         if (mUriHeader == null) {
             return null;
         }
@@ -176,7 +184,7 @@
      * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
      * @return the Uri cookies of this data source
      */
-    public List<HttpCookie> getUriCookies() {
+    public @Nullable List<HttpCookie> getUriCookies() {
         if (mUriCookies == null) {
             return null;
         }
@@ -188,23 +196,12 @@
      * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
      * @return the Context used for resolving the Uri of this data source
      */
-    public Context getUriContext() {
+    public @Nullable Context getUriContext() {
         return mUriContext;
     }
 
     /**
      * Builder class for {@link DataSourceDesc} objects.
-     * <p> Here is an example where <code>Builder</code> is used to define the
-     * {@link DataSourceDesc} to be used by a {@link MediaPlayer2} instance:
-     *
-     * <pre class="prettyprint">
-     * DataSourceDesc oldDSD = mediaplayer2.getDataSourceDesc();
-     * DataSourceDesc newDSD = new DataSourceDesc.Builder(oldDSD)
-     *         .setStartPosition(1000)
-     *         .setEndPosition(15000)
-     *         .build();
-     * mediaplayer2.setDataSourceDesc(newDSD);
-     * </pre>
      */
     public static class Builder {
         private int mType = TYPE_NONE;
@@ -213,7 +210,7 @@
 
         private FileDescriptor mFD;
         private long mFDOffset = 0;
-        private long mFDLength = LONG_MAX;
+        private long mFDLength = FD_LENGTH_UNKNOWN;
 
         private Uri mUri;
         private Map<String, String> mUriHeader;
@@ -222,7 +219,7 @@
 
         private String mMediaId;
         private long mStartPositionMs = 0;
-        private long mEndPositionMs = LONG_MAX;
+        private long mEndPositionMs = POSITION_UNKNOWN;
 
         /**
          * Constructs a new Builder with the defaults.
@@ -235,7 +232,7 @@
          * @param dsd the {@link DataSourceDesc} object whose data will be reused
          * in the new Builder.
          */
-        public Builder(DataSourceDesc dsd) {
+        public Builder(@NonNull DataSourceDesc dsd) {
             mType = dsd.mType;
             mMedia2DataSource = dsd.mMedia2DataSource;
             mFD = dsd.mFD;
@@ -258,7 +255,7 @@
          *
          * @return a new {@link DataSourceDesc} object
          */
-        public DataSourceDesc build() {
+        public @NonNull DataSourceDesc build() {
             if (mType != TYPE_CALLBACK
                     && mType != TYPE_FD
                     && mType != TYPE_URI) {
@@ -293,7 +290,7 @@
          * @param mediaId the media Id of this data source
          * @return the same Builder instance.
          */
-        public Builder setMediaId(String mediaId) {
+        public @NonNull Builder setMediaId(String mediaId) {
             mMediaId = mediaId;
             return this;
         }
@@ -306,7 +303,7 @@
          * @return the same Builder instance.
          *
          */
-        public Builder setStartPosition(long position) {
+        public @NonNull Builder setStartPosition(long position) {
             if (position < 0) {
                 position = 0;
             }
@@ -321,9 +318,9 @@
          * @param position the end position in milliseconds at which the playback will end
          * @return the same Builder instance.
          */
-        public Builder setEndPosition(long position) {
+        public @NonNull Builder setEndPosition(long position) {
             if (position < 0) {
-                position = LONG_MAX;
+                position = POSITION_UNKNOWN;
             }
             mEndPositionMs = position;
             return this;
@@ -336,7 +333,7 @@
          * @return the same Builder instance.
          * @throws NullPointerException if m2ds is null.
          */
-        public Builder setDataSource(@NonNull Media2DataSource m2ds) {
+        public @NonNull Builder setDataSource(@NonNull Media2DataSource m2ds) {
             Preconditions.checkNotNull(m2ds);
             resetDataSource();
             mType = TYPE_CALLBACK;
@@ -353,7 +350,7 @@
          * @return the same Builder instance.
          * @throws NullPointerException if fd is null.
          */
-        public Builder setDataSource(FileDescriptor fd) {
+        public @NonNull Builder setDataSource(@NonNull FileDescriptor fd) {
             Preconditions.checkNotNull(fd);
             resetDataSource();
             mType = TYPE_FD;
@@ -375,13 +372,14 @@
          * @return the same Builder instance.
          * @throws NullPointerException if fd is null.
          */
-        public Builder setDataSource(FileDescriptor fd, long offset, long length) {
+        public @NonNull Builder setDataSource(@NonNull FileDescriptor fd, long offset,
+                long length) {
             Preconditions.checkNotNull(fd);
             if (offset < 0) {
                 offset = 0;
             }
             if (length < 0) {
-                length = LONG_MAX;
+                length = FD_LENGTH_UNKNOWN;
             }
             resetDataSource();
             mType = TYPE_FD;
@@ -399,7 +397,7 @@
          * @return the same Builder instance.
          * @throws NullPointerException if context or uri is null.
          */
-        public Builder setDataSource(@NonNull Context context, @NonNull Uri uri) {
+        public @NonNull Builder setDataSource(@NonNull Context context, @NonNull Uri uri) {
             Preconditions.checkNotNull(context, "context cannot be null");
             Preconditions.checkNotNull(uri, "uri cannot be null");
             resetDataSource();
@@ -413,13 +411,7 @@
          * Sets the data source as a content Uri.
          *
          * To provide cookies for the subsequent HTTP requests, you can install your own default
-         * cookie handler and use other variants of setDataSource APIs instead. Alternatively, you
-         * can use this API to pass the cookies as a list of HttpCookie. If the app has not
-         * installed a CookieHandler already, {@link MediaPlayer2} will create a CookieManager
-         * and populates its CookieStore with the provided cookies when this data source is passed
-         * to {@link MediaPlayer2}. If the app has installed its own handler already, the handler
-         * is required to be of CookieManager type such that {@link MediaPlayer2} can update the
-         * manager’s CookieStore.
+         * cookie handler and use other variants of setDataSource APIs instead.
          *
          *  <p><strong>Note</strong> that the cross domain redirection is allowed by default,
          * but that can be changed with key/value pairs through the headers parameter with
@@ -436,7 +428,7 @@
          * @throws IllegalArgumentException if the cookie handler is not of CookieManager type
          *                                  when cookies are provided.
          */
-        public Builder setDataSource(@NonNull Context context, @NonNull Uri uri,
+        public @NonNull Builder setDataSource(@NonNull Context context, @NonNull Uri uri,
                 @Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies) {
             Preconditions.checkNotNull(context, "context cannot be null");
             Preconditions.checkNotNull(uri);
@@ -467,7 +459,7 @@
             mMedia2DataSource = null;
             mFD = null;
             mFDOffset = 0;
-            mFDLength = LONG_MAX;
+            mFDLength = FD_LENGTH_UNKNOWN;
             mUri = null;
             mUriHeader = null;
             mUriCookies = null;
diff --git a/media/src/main/java/androidx/media/MediaController2.java b/media/src/main/java/androidx/media/MediaController2.java
index 05d61ef..1da552d 100644
--- a/media/src/main/java/androidx/media/MediaController2.java
+++ b/media/src/main/java/androidx/media/MediaController2.java
@@ -108,7 +108,6 @@
 import android.app.PendingIntent;
 import android.content.Context;
 import android.media.AudioManager;
-import android.media.session.MediaSessionManager;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -127,6 +126,7 @@
 import android.util.Log;
 
 import androidx.annotation.GuardedBy;
+import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
@@ -136,14 +136,14 @@
 import androidx.media.MediaSession2.ControllerInfo;
 import androidx.media.MediaSession2.ErrorCode;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
- * @hide
- * Allows an app to interact with an active {@link MediaSession2} or a
- * {@link MediaSessionService2} in any status. Media buttons and other commands can be sent to
- * the session.
+ * Allows an app to interact with an active {@link MediaSession2} in any status. Media buttons and
+ * other commands can be sent to the session.
  * <p>
  * When you're done, use {@link #close()} to clean up resources. This also helps session service
  * to be destroyed when there's no controller associated with it.
@@ -151,26 +151,32 @@
  * When controlling {@link MediaSession2}, the controller will be available immediately after
  * the creation.
  * <p>
- * When controlling {@link MediaSessionService2}, the {@link MediaController2} would be
- * available only if the session service allows this controller by
- * {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)} for the service.
- * Wait {@link ControllerCallback#onConnected(MediaController2, SessionCommandGroup2)} or
- * {@link ControllerCallback#onDisconnected(MediaController2)} for the result.
- * <p>
- * A controller can be created through token from {@link MediaSessionManager} if you hold the
- * signature|privileged permission "android.permission.MEDIA_CONTENT_CONTROL" permission or are
- * an enabled notification listener or by getting a {@link SessionToken2} directly the
- * the session owner.
- * <p>
  * MediaController2 objects are thread-safe.
  * <p>
  * @see MediaSession2
- * @see MediaSessionService2
  */
 @TargetApi(Build.VERSION_CODES.KITKAT)
-@RestrictTo(LIBRARY_GROUP)
 public class MediaController2 implements AutoCloseable {
     /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @IntDef({AudioManager.ADJUST_LOWER, AudioManager.ADJUST_RAISE, AudioManager.ADJUST_SAME,
+            AudioManager.ADJUST_MUTE, AudioManager.ADJUST_UNMUTE, AudioManager.ADJUST_TOGGLE_MUTE})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface VolumeDirection {}
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @IntDef(value = {AudioManager.FLAG_SHOW_UI, AudioManager.FLAG_ALLOW_RINGER_MODES,
+            AudioManager.FLAG_PLAY_SOUND, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE,
+            AudioManager.FLAG_VIBRATE}, flag = true)
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface VolumeFlags {}
+
+    /**
      * Interface for listening to change in activeness of the {@link MediaSession2}.  It's
      * active if and only if it has set a player.
      */
@@ -297,7 +303,6 @@
          * @param item new item
          * @see #onBufferingStateChanged(MediaController2, MediaItem2, int)
          */
-        // TODO(jaewan): Use this (b/74316764)
         public void onCurrentMediaItemChanged(@NonNull MediaController2 controller,
                 @NonNull MediaItem2 item) { }
 
@@ -525,7 +530,6 @@
 
         @Override
         public void onSessionEvent(String event, Bundle extras) {
-            // TODO: Call callbacks on the executor
             switch (event) {
                 case SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED: {
                     SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
@@ -666,7 +670,7 @@
     }
 
     private static final String TAG = "MediaController2";
-    private static final boolean DEBUG = true; // TODO(jaewan): Change
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     // Note: Using {@code null} doesn't helpful here because MediaBrowserServiceCompat always wraps
     //       the rootHints so it becomes non-null.
@@ -1090,7 +1094,7 @@
      * @param flags flags from {@link AudioManager} to include with the volume request for local
      *              playback
      */
-    public void setVolumeTo(int value, int flags) {
+    public void setVolumeTo(int value, @VolumeFlags int flags) {
         synchronized (mLock) {
             if (!mConnected) {
                 Log.w(TAG, "Session isn't active", new IllegalStateException());
@@ -1107,6 +1111,7 @@
      * Adjust the volume of the output this session is playing on. The direction
      * must be one of {@link AudioManager#ADJUST_LOWER},
      * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}.
+     * <p>
      * The command will be ignored if the session does not support
      * {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} or
      * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}.
@@ -1122,7 +1127,7 @@
      * @param flags flags from {@link AudioManager} to include with the volume request for local
      *              playback
      */
-    public void adjustVolume(int direction, int flags) {
+    public void adjustVolume(@VolumeDirection int direction, @VolumeFlags int flags) {
         synchronized (mLock) {
             if (!mConnected) {
                 Log.w(TAG, "Session isn't active", new IllegalStateException());
@@ -1274,7 +1279,6 @@
      */
     public @Nullable PlaybackInfo getPlaybackInfo() {
         synchronized (mLock) {
-            // TODO: update mPlaybackInfo via MediaControllerCompat.Callback.onAudioInfoChanged().
             return mPlaybackInfo;
         }
     }
@@ -1333,7 +1337,9 @@
      * implementation. Use media items returned here for other playlist agent APIs such as
      * {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
      *
-     * @return playlist. Can be {@code null} if the controller doesn't have enough permission.
+     * @return playlist. Can be {@code null} if the playlist hasn't set nor controller doesn't have
+     *      enough permission.
+     * @see SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST
      */
     public @Nullable List<MediaItem2> getPlaylist() {
         synchronized (mLock) {
@@ -1389,7 +1395,8 @@
 
     /**
      * Adds the media item to the playlist at position index. Index equals or greater than
-     * the current playlist size will add the item at the end of the playlist.
+     * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
+     * the playlist.
      * <p>
      * This will not change the currently playing media item.
      * If index is less than or equal to the current index of the playlist,
@@ -1501,7 +1508,6 @@
      * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
      */
     public void setRepeatMode(@RepeatMode int repeatMode) {
-        // TODO: check permission
         Bundle args = new Bundle();
         args.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
         sendCommand(COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE, args);
@@ -1530,7 +1536,6 @@
      * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
      */
     public void setShuffleMode(@ShuffleMode int shuffleMode) {
-        // TODO: check permission
         Bundle args = new Bundle();
         args.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
         sendCommand(COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE, args);
@@ -1570,7 +1575,6 @@
 
     // Should be used without a lock to prevent potential deadlock.
     void onConnectedNotLocked(Bundle data) {
-        // TODO: Getting mPlaybackInfo via MediaControllerCompat.Callback.onAudioInfoChanged()
         // is enough or should we pass it while connecting?
         final SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
                 data.getBundle(ARGUMENT_ALLOWED_COMMANDS));
@@ -1580,13 +1584,14 @@
                 ARGUMENT_PLAYBACK_STATE_COMPAT);
         final int repeatMode = data.getInt(ARGUMENT_REPEAT_MODE);
         final int shuffleMode = data.getInt(ARGUMENT_SHUFFLE_MODE);
-        // TODO: Set mMediaMetadataCompat from the data.
         final List<MediaItem2> playlist = MediaUtils2.fromMediaItem2ParcelableArray(
                 data.getParcelableArray(ARGUMENT_PLAYLIST));
         final MediaItem2 currentMediaItem = MediaItem2.fromBundle(
                 data.getBundle(ARGUMENT_MEDIA_ITEM));
         final PlaybackInfo playbackInfo =
                 PlaybackInfo.fromBundle(data.getBundle(ARGUMENT_PLAYBACK_INFO));
+        final MediaMetadata2 metadata = MediaMetadata2.fromBundle(
+                data.getBundle(ARGUMENT_PLAYLIST_METADATA));
         if (DEBUG) {
             Log.d(TAG, "onConnectedNotLocked sessionCompatToken=" + mToken.getSessionCompatToken()
                     + ", allowedCommands=" + allowedCommands);
@@ -1611,10 +1616,10 @@
                 mShuffleMode = shuffleMode;
                 mPlaylist = playlist;
                 mCurrentMediaItem = currentMediaItem;
+                mPlaylistMetadata = metadata;
                 mConnected = true;
                 mPlaybackInfo = playbackInfo;
             }
-            // TODO(jaewan): Keep commands to prevents illegal API calls.
             mCallbackExecutor.execute(new Runnable() {
                 @Override
                 public void run() {
@@ -1634,8 +1639,6 @@
     }
 
     private void initialize() {
-        // TODO(jaewan): More sanity checks.
-        // TODO: Check the connection between 1.0 and 2.0 APIs
         if (mToken.getType() == SessionToken2.TYPE_SESSION) {
             synchronized (mLock) {
                 mBrowserCompat = null;
diff --git a/media/src/main/java/androidx/media/MediaItem2.java b/media/src/main/java/androidx/media/MediaItem2.java
index 20fbf06..b8c44c1 100644
--- a/media/src/main/java/androidx/media/MediaItem2.java
+++ b/media/src/main/java/androidx/media/MediaItem2.java
@@ -31,7 +31,6 @@
 import java.util.UUID;
 
 /**
- * @hide
  * A class with information on a single media item with the metadata information.
  * Media item are application dependent so we cannot guarantee that they contain the right values.
  * <p>
@@ -39,7 +38,6 @@
  * <p>
  * This object isn't a thread safe.
  */
-@RestrictTo(LIBRARY_GROUP)
 public class MediaItem2 {
     /** @hide */
     @RestrictTo(LIBRARY_GROUP)
@@ -298,7 +296,6 @@
             String id = (mMetadata != null)
                     ? mMetadata.getString(MediaMetadata2.METADATA_KEY_MEDIA_ID) : null;
             if (id == null) {
-                //  TODO(jaewan): Double check if its sufficient (e.g. Use UUID instead?)
                 id = (mMediaId != null) ? mMediaId : toString();
             }
             return new MediaItem2(id, mDataSourceDesc, mMetadata, mFlags);
diff --git a/media/src/main/java/androidx/media/MediaMetadata2.java b/media/src/main/java/androidx/media/MediaMetadata2.java
index f622bfc..0cfd237 100644
--- a/media/src/main/java/androidx/media/MediaMetadata2.java
+++ b/media/src/main/java/androidx/media/MediaMetadata2.java
@@ -34,14 +34,12 @@
 import java.util.Set;
 
 /**
- * @hide
  * Contains metadata about an item, such as the title, artist, etc.
  */
 // New version of MediaMetadata with following changes
 //   - Don't implement Parcelable for updatable support.
 //   - Also support MediaDescription features. MediaDescription is deprecated instead because
 //     it was insufficient for controller to display media contents.
-@RestrictTo(LIBRARY_GROUP)
 public final class MediaMetadata2 {
     private static final String TAG = "MediaMetadata2";
 
@@ -339,8 +337,7 @@
      * service providing the content. If used, this should be a persistent
      * unique key for the underlying content.  It may be used with
      * {@link MediaController2#playFromMediaId(String, Bundle)}
-     * to initiate playback when provided by a {@link MediaBrowser2} connected to
-     * the same app.
+     * to initiate playback.
      *
      * @see Builder#putText(String, CharSequence)
      * @see Builder#putString(String, String)
@@ -353,7 +350,7 @@
      * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
      * information about the Uri of the content. This value is specific to the service providing the
      * content. It may be used with {@link MediaController2#playFromUri(Uri, Bundle)}
-     * to initiate playback when provided by a {@link MediaBrowser2} connected to the same app.
+     * to initiate playback.
      *
      * @see Builder#putText(String, CharSequence)
      * @see Builder#putString(String, String)
@@ -714,7 +711,6 @@
         if (key == null) {
             throw new IllegalArgumentException("key shouldn't be null");
         }
-        // TODO(jaewan): Add backward compatibility
         Rating2 rating = null;
         try {
             rating = Rating2.fromBundle(mBundle.getBundle(key));
@@ -887,7 +883,6 @@
          * <li>{@link #METADATA_KEY_DISPLAY_ICON_URI}</li>
          * <li>{@link #METADATA_KEY_MEDIA_ID}</li>
          * <li>{@link #METADATA_KEY_MEDIA_URI}</li>
-         * <li>{@link #METADATA_KEY_RADIO_PROGRAM_NAME}</li>
          * </ul>
          *
          * @param key The key for referencing this value
@@ -932,7 +927,6 @@
          * <li>{@link #METADATA_KEY_DISPLAY_ICON_URI}</li>
          * <li>{@link #METADATA_KEY_MEDIA_ID}</li>
          * <li>{@link #METADATA_KEY_MEDIA_URI}</li>
-         * <li>{@link #METADATA_KEY_RADIO_PROGRAM_NAME}</li>
          * </ul>
          *
          * @param key The key for referencing this value
diff --git a/media/src/main/java/androidx/media/MediaPlayer2.java b/media/src/main/java/androidx/media/MediaPlayer2.java
index 1864d72..3802b9f 100644
--- a/media/src/main/java/androidx/media/MediaPlayer2.java
+++ b/media/src/main/java/androidx/media/MediaPlayer2.java
@@ -28,6 +28,7 @@
 import android.media.MediaTimestamp;
 import android.media.PlaybackParams;
 import android.media.ResourceBusyException;
+import android.media.SubtitleData;
 import android.media.SyncParams;
 import android.media.TimedMetaData;
 import android.media.UnsupportedSchemeException;
@@ -47,7 +48,6 @@
 import java.util.UUID;
 import java.util.concurrent.Executor;
 
-
 /**
  * @hide
  * MediaPlayer2 class can be used to control playback
@@ -832,10 +832,7 @@
      *
      *  Additional vendor-specific fields may also be present in
      *  the return value.
-     *  @hide
-     *  TODO: This method is not ready for public. Currently returns metrics data in MediaPlayer1.
      */
-    @RestrictTo(LIBRARY_GROUP)
     public abstract PersistableBundle getMetrics();
 
     /**
@@ -1229,13 +1226,26 @@
                 @CallStatus int status) { }
 
         /**
-         * Called to indicate media clock has changed.
+         * Called when a discontinuity in the normal progression of the media time is detected.
+         * The "normal progression" of media time is defined as the expected increase of the
+         * playback position when playing media, relative to the playback speed (for instance every
+         * second, media time increases by two seconds when playing at 2x).<br>
+         * Discontinuities are encountered in the following cases:
+         * <ul>
+         * <li>when the player is starved for data and cannot play anymore</li>
+         * <li>when the player encounters a playback error</li>
+         * <li>when the a seek operation starts, and when it's completed</li>
+         * <li>when the playback speed changes</li>
+         * <li>when the playback state changes</li>
+         * <li>when the player is reset</li>
+         * </ul>
          *
          * @param mp the MediaPlayer2 the media time pertains to.
          * @param dsd the DataSourceDesc of this data source
-         * @param timestamp the new media clock.
+         * @param timestamp the timestamp that correlates media time, system time and clock rate,
+         *     or {@link MediaTimestamp#TIMESTAMP_UNKNOWN} in an error case.
          */
-        public void onMediaTimeChanged(
+        public void onMediaTimeDiscontinuity(
                 MediaPlayer2 mp, DataSourceDesc dsd, MediaTimestamp timestamp) { }
 
         /**
@@ -1247,12 +1257,14 @@
          */
         public void onCommandLabelReached(MediaPlayer2 mp, @NonNull Object label) { }
 
-        /* TODO : uncomment below once API is available in supportlib.
+        /**
          * Called when when a player subtitle track has new subtitle data available.
          * @param mp the player that reports the new subtitle data
+         * @param dsd the DataSourceDesc of this data source
          * @param data the subtitle data
          */
-        // public void onSubtitleData(MediaPlayer2 mp, @NonNull SubtitleData data) { }
+        public void onSubtitleData(
+                MediaPlayer2 mp, DataSourceDesc dsd, @NonNull SubtitleData data) { }
     }
 
     /**
@@ -2008,4 +2020,100 @@
             super(detailMessage);
         }
     }
+
+    /**
+     * Definitions for the metrics that are reported via the {@link #getMetrics} call.
+     */
+    public static final class MetricsConstants {
+        private MetricsConstants() {}
+
+        /**
+         * Key to extract the MIME type of the video track
+         * from the {@link MediaPlayer2#getMetrics} return value.
+         * The value is a String.
+         */
+        public static final String MIME_TYPE_VIDEO = "android.media.mediaplayer.video.mime";
+
+        /**
+         * Key to extract the codec being used to decode the video track
+         * from the {@link MediaPlayer2#getMetrics} return value.
+         * The value is a String.
+         */
+        public static final String CODEC_VIDEO = "android.media.mediaplayer.video.codec";
+
+        /**
+         * Key to extract the width (in pixels) of the video track
+         * from the {@link MediaPlayer2#getMetrics} return value.
+         * The value is an integer.
+         */
+        public static final String WIDTH = "android.media.mediaplayer.width";
+
+        /**
+         * Key to extract the height (in pixels) of the video track
+         * from the {@link MediaPlayer2#getMetrics} return value.
+         * The value is an integer.
+         */
+        public static final String HEIGHT = "android.media.mediaplayer.height";
+
+        /**
+         * Key to extract the count of video frames played
+         * from the {@link MediaPlayer2#getMetrics} return value.
+         * The value is an integer.
+         */
+        public static final String FRAMES = "android.media.mediaplayer.frames";
+
+        /**
+         * Key to extract the count of video frames dropped
+         * from the {@link MediaPlayer2#getMetrics} return value.
+         * The value is an integer.
+         */
+        public static final String FRAMES_DROPPED = "android.media.mediaplayer.dropped";
+
+        /**
+         * Key to extract the MIME type of the audio track
+         * from the {@link MediaPlayer2#getMetrics} return value.
+         * The value is a String.
+         */
+        public static final String MIME_TYPE_AUDIO = "android.media.mediaplayer.audio.mime";
+
+        /**
+         * Key to extract the codec being used to decode the audio track
+         * from the {@link MediaPlayer2#getMetrics} return value.
+         * The value is a String.
+         */
+        public static final String CODEC_AUDIO = "android.media.mediaplayer.audio.codec";
+
+        /**
+         * Key to extract the duration (in milliseconds) of the
+         * media being played
+         * from the {@link MediaPlayer2#getMetrics} return value.
+         * The value is a long.
+         */
+        public static final String DURATION = "android.media.mediaplayer.durationMs";
+
+        /**
+         * Key to extract the playing time (in milliseconds) of the
+         * media being played
+         * from the {@link MediaPlayer2#getMetrics} return value.
+         * The value is a long.
+         */
+        public static final String PLAYING = "android.media.mediaplayer.playingMs";
+
+        /**
+         * Key to extract the count of errors encountered while
+         * playing the media
+         * from the {@link MediaPlayer2#getMetrics} return value.
+         * The value is an integer.
+         */
+        public static final String ERRORS = "android.media.mediaplayer.err";
+
+        /**
+         * Key to extract an (optional) error code detected while
+         * playing the media
+         * from the {@link MediaPlayer2#getMetrics} return value.
+         * The value is an integer.
+         */
+        public static final String ERROR_CODE = "android.media.mediaplayer.errcode";
+
+    }
 }
diff --git a/media/src/main/java/androidx/media/MediaPlayer2Impl.java b/media/src/main/java/androidx/media/MediaPlayer2Impl.java
index a4bb01e..50c490f 100644
--- a/media/src/main/java/androidx/media/MediaPlayer2Impl.java
+++ b/media/src/main/java/androidx/media/MediaPlayer2Impl.java
@@ -28,6 +28,7 @@
 import android.media.MediaTimestamp;
 import android.media.PlaybackParams;
 import android.media.ResourceBusyException;
+import android.media.SubtitleData;
 import android.media.SyncParams;
 import android.media.TimedMetaData;
 import android.media.UnsupportedSchemeException;
@@ -75,13 +76,53 @@
     private static final int NEXT_SOURCE_STATE_PREPARING = 1;
     private static final int NEXT_SOURCE_STATE_PREPARED = 2;
 
-    // TODO: This class has too many locks. Use one single lock to protect internal variables.
-    private MediaPlayer mPlayer;
-    @PlayerState
-    private int mPlayerState;
-    @BuffState
-    private int mBufferingState;
-    private AudioAttributesCompat mAudioAttributes;
+    private static ArrayMap<Integer, Integer> sInfoEventMap;
+    private static ArrayMap<Integer, Integer> sErrorEventMap;
+    private static ArrayMap<Integer, Integer> sPrepareDrmStatusMap;
+
+    static {
+        sInfoEventMap = new ArrayMap<>();
+        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_UNKNOWN, MEDIA_INFO_UNKNOWN);
+        sInfoEventMap.put(2 /*MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT*/, MEDIA_INFO_STARTED_AS_NEXT);
+        sInfoEventMap.put(
+                MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START, MEDIA_INFO_VIDEO_RENDERING_START);
+        sInfoEventMap.put(
+                MediaPlayer.MEDIA_INFO_VIDEO_TRACK_LAGGING, MEDIA_INFO_VIDEO_TRACK_LAGGING);
+        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_BUFFERING_START, MEDIA_INFO_BUFFERING_START);
+        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_BUFFERING_END, MEDIA_INFO_BUFFERING_END);
+        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_BAD_INTERLEAVING, MEDIA_INFO_BAD_INTERLEAVING);
+        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_NOT_SEEKABLE, MEDIA_INFO_NOT_SEEKABLE);
+        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_METADATA_UPDATE, MEDIA_INFO_METADATA_UPDATE);
+        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_AUDIO_NOT_PLAYING, MEDIA_INFO_AUDIO_NOT_PLAYING);
+        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_VIDEO_NOT_PLAYING, MEDIA_INFO_VIDEO_NOT_PLAYING);
+        sInfoEventMap.put(
+                MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, MEDIA_INFO_UNSUPPORTED_SUBTITLE);
+        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_SUBTITLE_TIMED_OUT, MEDIA_INFO_SUBTITLE_TIMED_OUT);
+
+        sErrorEventMap = new ArrayMap<>();
+        sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNKNOWN);
+        sErrorEventMap.put(
+                MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK,
+                MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK);
+        sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_IO, MEDIA_ERROR_IO);
+        sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_MALFORMED, MEDIA_ERROR_MALFORMED);
+        sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_UNSUPPORTED, MEDIA_ERROR_UNSUPPORTED);
+        sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_TIMED_OUT, MEDIA_ERROR_TIMED_OUT);
+
+        sPrepareDrmStatusMap.put(
+                MediaPlayer.PREPARE_DRM_STATUS_SUCCESS, PREPARE_DRM_STATUS_SUCCESS);
+        sPrepareDrmStatusMap.put(
+                MediaPlayer.PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR,
+                PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR);
+        sPrepareDrmStatusMap.put(
+                MediaPlayer.PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR,
+                PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR);
+        sPrepareDrmStatusMap.put(
+                MediaPlayer.PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR,
+                PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR);
+    }
+
+    private MediaPlayer mPlayer;  // MediaPlayer is thread-safe.
 
     private final Object mSrcLock = new Object();
     //--- guarded by |mSrcLock| start
@@ -106,18 +147,18 @@
     @GuardedBy("mTaskLock")
     private Task mCurrentTask;
 
-    private final Object mEventCbLock = new Object();
-    @GuardedBy("mEventCbLock")
+    private final Object mLock = new Object();
+    //--- guarded by |mLock| start
+    @PlayerState private int mPlayerState;
+    @BuffState private int mBufferingState;
+    private AudioAttributesCompat mAudioAttributes;
     private ArrayList<Pair<Executor, MediaPlayer2EventCallback>> mMp2EventCallbackRecords =
             new ArrayList<>();
-    @GuardedBy("mEventCbLock")
     private ArrayMap<PlayerEventCallback, Executor> mPlayerEventCallbackMap =
             new ArrayMap<>();
-
-    private final Object mDrmEventCbLock = new Object();
-    @GuardedBy("mDrmEventCbLock")
     private ArrayList<Pair<Executor, DrmEventCallback>> mDrmEventCallbackRecords =
             new ArrayList<>();
+    //--- guarded by |mLock| end
 
     /**
      * Default constructor.
@@ -130,6 +171,9 @@
         mHandlerThread.start();
         Looper looper = mHandlerThread.getLooper();
         mTaskHandler = new Handler(looper);
+
+        // TODO: To make sure MediaPlayer1 listeners work, the caller thread should have a looper.
+        // Fix the framework or document this behavior.
         mPlayer = new MediaPlayer();
         mPlayerState = PLAYER_STATE_IDLE;
         mBufferingState = BUFFERING_STATE_UNKNOWN;
@@ -273,7 +317,9 @@
 
     @Override
     public @PlayerState int getPlayerState() {
-        return mPlayerState;
+        synchronized (mLock) {
+            return mPlayerState;
+        }
     }
 
     /**
@@ -283,7 +329,9 @@
      */
     @Override
     public @BuffState int getBufferingState() {
-        return mBufferingState;
+        synchronized (mLock) {
+            return mBufferingState;
+        }
     }
 
     /**
@@ -299,15 +347,21 @@
         addTask(new Task(CALL_COMPLETED_SET_AUDIO_ATTRIBUTES, false) {
             @Override
             void process() {
-                mAudioAttributes = attributes;
-                mPlayer.setAudioAttributes((AudioAttributes) mAudioAttributes.unwrap());
+                AudioAttributes attr;
+                synchronized (mLock) {
+                    mAudioAttributes = attributes;
+                    attr = (AudioAttributes) mAudioAttributes.unwrap();
+                }
+                mPlayer.setAudioAttributes(attr);
             }
         });
     }
 
     @Override
     public @NonNull AudioAttributesCompat getAudioAttributes() {
-        return mAudioAttributes;
+        synchronized (mLock) {
+            return mAudioAttributes;
+        }
     }
 
     /**
@@ -523,7 +577,7 @@
             throw new IllegalArgumentException(
                     "Illegal null Executor for the PlayerEventCallback");
         }
-        synchronized (mEventCbLock) {
+        synchronized (mLock) {
             mPlayerEventCallbackMap.put(cb, e);
         }
     }
@@ -537,7 +591,7 @@
         if (cb == null) {
             throw new IllegalArgumentException("Illegal null PlayerEventCallback");
         }
-        synchronized (mEventCbLock) {
+        synchronized (mLock) {
             mPlayerEventCallbackMap.remove(cb);
         }
     }
@@ -547,18 +601,12 @@
         addTask(new Task(CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED, false) {
             @Override
             void process() {
-                synchronized (mEventCbLock) {
-                    for (final Pair<Executor, MediaPlayer2EventCallback> cb
-                            : mMp2EventCallbackRecords) {
-                        cb.first.execute(new Runnable() {
-                            @Override
-                            public void run() {
-                                cb.second.onCommandLabelReached(
-                                        MediaPlayer2Impl.this, label);
-                            }
-                        });
+                notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                    @Override
+                    public void notify(MediaPlayer2EventCallback cb) {
+                        cb.onCommandLabelReached(MediaPlayer2Impl.this, label);
                     }
-                }
+                });
             }
         });
     }
@@ -840,9 +888,20 @@
     @Override
     public void reset() {
         mPlayer.reset();
+
+        mBufferedPercentageCurrent.set(0);
+        mBufferedPercentageNext.set(0);
+        mVolume = 1.0f;
+
+        synchronized (mLock) {
+            mAudioAttributes = null;
+            mMp2EventCallbackRecords.clear();
+            mPlayerEventCallbackMap.clear();
+            mDrmEventCallbackRecords.clear();
+        }
         setPlayerState(PLAYER_STATE_IDLE);
         setBufferingState(BUFFERING_STATE_UNKNOWN);
-        /* FIXME: reset other internal variables. */
+        setUpListeners();
     }
 
     /**
@@ -1169,7 +1228,7 @@
             throw new IllegalArgumentException(
                     "Illegal null Executor for the MediaPlayer2EventCallback");
         }
-        synchronized (mEventCbLock) {
+        synchronized (mLock) {
             mMp2EventCallbackRecords.add(new Pair(executor, eventCallback));
         }
     }
@@ -1179,7 +1238,7 @@
      */
     @Override
     public void clearMediaPlayer2EventCallback() {
-        synchronized (mEventCbLock) {
+        synchronized (mLock) {
             mMp2EventCallbackRecords.clear();
         }
     }
@@ -1199,8 +1258,7 @@
         mPlayer.setOnDrmConfigHelper(new MediaPlayer.OnDrmConfigHelper() {
             @Override
             public void onDrmConfig(MediaPlayer mp) {
-                /** FIXME: pass the right DSD. */
-                listener.onDrmConfig(MediaPlayer2Impl.this, null);
+                listener.onDrmConfig(MediaPlayer2Impl.this, mCurrentDSD);
             }
         });
     }
@@ -1222,7 +1280,7 @@
             throw new IllegalArgumentException(
                     "Illegal null Executor for the MediaPlayer2EventCallback");
         }
-        synchronized (mDrmEventCbLock) {
+        synchronized (mLock) {
             mDrmEventCallbackRecords.add(new Pair(executor, eventCallback));
         }
     }
@@ -1232,7 +1290,7 @@
      */
     @Override
     public void clearDrmEventCallback() {
-        synchronized (mDrmEventCbLock) {
+        synchronized (mLock) {
             mDrmEventCallbackRecords.clear();
         }
     }
@@ -1472,60 +1530,105 @@
 
     private void setPlaybackParamsInternal(final PlaybackParams params) {
         PlaybackParams current = mPlayer.getPlaybackParams();
-        if (Math.abs(current.getSpeed() - params.getSpeed()) > 0.0001f) {
-            synchronized (mEventCbLock) {
-                final int callbackCount = mPlayerEventCallbackMap.size();
-                for (int i = 0; i < callbackCount; i++) {
-                    final Executor executor = mPlayerEventCallbackMap.valueAt(i);
-                    final PlayerEventCallback cb = mPlayerEventCallbackMap.keyAt(i);
-                    executor.execute(new Runnable() {
-                        @Override
-                        public void run() {
-                            cb.onPlaybackSpeedChanged(MediaPlayer2Impl.this, params.getSpeed());
-                        }
-                    });
-                }
-            }
-        }
         mPlayer.setPlaybackParams(params);
+        if (current.getSpeed() != params.getSpeed()) {
+            notifyPlayerEvent(new PlayerEventNotifier() {
+                @Override
+                public void notify(PlayerEventCallback cb) {
+                    cb.onPlaybackSpeedChanged(MediaPlayer2Impl.this, params.getSpeed());
+                }
+            });
+        }
     }
 
     private void setPlayerState(@PlayerState final int state) {
-        if (mPlayerState != state) {
-            synchronized (mEventCbLock) {
-                final int callbackCount = mPlayerEventCallbackMap.size();
-                for (int i = 0; i < callbackCount; i++) {
-                    final Executor executor = mPlayerEventCallbackMap.valueAt(i);
-                    final PlayerEventCallback cb = mPlayerEventCallbackMap.keyAt(i);
-                    executor.execute(new Runnable() {
-                        @Override
-                        public void run() {
-                            cb.onPlayerStateChanged(MediaPlayer2Impl.this, state);
-                        }
-                    });
-                }
+        synchronized (mLock) {
+            if (mPlayerState == state) {
+                return;
             }
             mPlayerState = state;
         }
+        notifyPlayerEvent(new PlayerEventNotifier() {
+            @Override
+            public void notify(PlayerEventCallback cb) {
+                cb.onPlayerStateChanged(MediaPlayer2Impl.this, state);
+            }
+        });
     }
 
     private void setBufferingState(@BuffState final int state) {
-        if (mBufferingState != state) {
-            synchronized (mEventCbLock) {
-                final int callbackCount = mPlayerEventCallbackMap.size();
-                for (int i = 0; i < callbackCount; i++) {
-                    final Executor executor = mPlayerEventCallbackMap.valueAt(i);
-                    final PlayerEventCallback cb = mPlayerEventCallbackMap.keyAt(i);
-                    executor.execute(new Runnable() {
-                        @Override
-                        public void run() {
-                            cb.onBufferingStateChanged(MediaPlayer2Impl.this, mCurrentDSD, state);
-                        }
-                    });
-                }
+        synchronized (mLock) {
+            if (mBufferingState == state) {
+                return;
             }
             mBufferingState = state;
         }
+        notifyPlayerEvent(new PlayerEventNotifier() {
+            @Override
+            public void notify(PlayerEventCallback cb) {
+                cb.onBufferingStateChanged(MediaPlayer2Impl.this, mCurrentDSD, state);
+            }
+        });
+    }
+
+    private void notifyMediaPlayer2Event(final Mp2EventNotifier notifier) {
+        List<Pair<Executor, MediaPlayer2EventCallback>> records;
+        synchronized (mLock) {
+            records = new ArrayList<>(mMp2EventCallbackRecords);
+        }
+        for (final Pair<Executor, MediaPlayer2EventCallback> record : records) {
+            record.first.execute(new Runnable() {
+                @Override
+                public void run() {
+                    notifier.notify(record.second);
+                }
+            });
+        }
+    }
+
+    private void notifyPlayerEvent(final PlayerEventNotifier notifier) {
+        ArrayMap<PlayerEventCallback, Executor> map;
+        synchronized (mLock) {
+            map = new ArrayMap<>(mPlayerEventCallbackMap);
+        }
+        final int callbackCount = map.size();
+        for (int i = 0; i < callbackCount; i++) {
+            final Executor executor = map.valueAt(i);
+            final PlayerEventCallback cb = map.keyAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    notifier.notify(cb);
+                }
+            });
+        }
+    }
+
+    private void notifyDrmEvent(final DrmEventNotifier notifier) {
+        List<Pair<Executor, DrmEventCallback>> records;
+        synchronized (mLock) {
+            records = new ArrayList<>(mDrmEventCallbackRecords);
+        }
+        for (final Pair<Executor, DrmEventCallback> record : records) {
+            record.first.execute(new Runnable() {
+                @Override
+                public void run() {
+                    notifier.notify(record.second);
+                }
+            });
+        }
+    }
+
+    private interface Mp2EventNotifier {
+        void notify(MediaPlayer2EventCallback callback);
+    }
+
+    private interface PlayerEventNotifier {
+        void notify(PlayerEventCallback callback);
+    }
+
+    private interface DrmEventNotifier {
+        void notify(DrmEventCallback callback);
     }
 
     private void setUpListeners() {
@@ -1534,29 +1637,18 @@
             public void onPrepared(MediaPlayer mp) {
                 setPlayerState(PLAYER_STATE_PAUSED);
                 setBufferingState(BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
-                synchronized (mEventCbLock) {
-                    for (final Pair<Executor, MediaPlayer2EventCallback> cb :
-                            mMp2EventCallbackRecords) {
-                        cb.first.execute(new Runnable() {
-                            @Override
-                            public void run() {
-                                cb.second.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
-                                        MEDIA_INFO_PREPARED, 0);
-                            }
-                        });
+                notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                    @Override
+                    public void notify(MediaPlayer2EventCallback callback) {
+                        callback.onInfo(MediaPlayer2Impl.this, mCurrentDSD, MEDIA_INFO_PREPARED, 0);
                     }
-                    final int callbackCount = mPlayerEventCallbackMap.size();
-                    for (int i = 0; i < callbackCount; i++) {
-                        final Executor executor = mPlayerEventCallbackMap.valueAt(i);
-                        final PlayerEventCallback cb = mPlayerEventCallbackMap.keyAt(i);
-                        executor.execute(new Runnable() {
-                            @Override
-                            public void run() {
-                                cb.onMediaPrepared(MediaPlayer2Impl.this, mCurrentDSD);
-                            }
-                        });
+                });
+                notifyPlayerEvent(new PlayerEventNotifier() {
+                    @Override
+                    public void notify(PlayerEventCallback cb) {
+                        cb.onMediaPrepared(MediaPlayer2Impl.this, mCurrentDSD);
                     }
-                }
+                });
                 synchronized (mTaskLock) {
                     if (mCurrentTask != null
                             && mCurrentTask.mMediaCallType == CALL_COMPLETED_PREPARE
@@ -1572,18 +1664,12 @@
         mPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
             @Override
             public void onVideoSizeChanged(MediaPlayer mp, final int width, final int height) {
-                synchronized (mEventCbLock) {
-                    for (final Pair<Executor, MediaPlayer2EventCallback> cb :
-                            mMp2EventCallbackRecords) {
-                        cb.first.execute(new Runnable() {
-                            @Override
-                            public void run() {
-                                cb.second.onVideoSizeChanged(MediaPlayer2Impl.this, mCurrentDSD,
-                                        width, height);
-                            }
-                        });
+                notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                    @Override
+                    public void notify(MediaPlayer2EventCallback cb) {
+                        cb.onVideoSizeChanged(MediaPlayer2Impl.this, mCurrentDSD, width, height);
                     }
-                }
+                });
             }
         });
         mPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() {
@@ -1591,18 +1677,13 @@
             public boolean onInfo(MediaPlayer mp, int what, int extra) {
                 switch (what) {
                     case MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START:
-                        synchronized (mEventCbLock) {
-                            for (final Pair<Executor, MediaPlayer2EventCallback> cb :
-                                    mMp2EventCallbackRecords) {
-                                cb.first.execute(new Runnable() {
-                                    @Override
-                                    public void run() {
-                                        cb.second.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
-                                                MEDIA_INFO_VIDEO_RENDERING_START, 0);
-                                    }
-                                });
+                        notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                            @Override
+                            public void notify(MediaPlayer2EventCallback cb) {
+                                cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
+                                        MEDIA_INFO_VIDEO_RENDERING_START, 0);
                             }
-                        }
+                        });
                         break;
                     case MediaPlayer.MEDIA_INFO_BUFFERING_START:
                         setBufferingState(BUFFERING_STATE_BUFFERING_AND_STARVED);
@@ -1618,18 +1699,13 @@
             @Override
             public void onCompletion(MediaPlayer mp) {
                 setPlayerState(PLAYER_STATE_PAUSED);
-                synchronized (mEventCbLock) {
-                    for (final Pair<Executor, MediaPlayer2EventCallback> cb :
-                            mMp2EventCallbackRecords) {
-                        cb.first.execute(new Runnable() {
-                            @Override
-                            public void run() {
-                                cb.second.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
-                                        MEDIA_INFO_PLAYBACK_COMPLETE, 0);
-                            }
-                        });
+                notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                    @Override
+                    public void notify(MediaPlayer2EventCallback cb) {
+                        cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD, MEDIA_INFO_PLAYBACK_COMPLETE,
+                                0);
                     }
-                }
+                });
             }
         });
         mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@@ -1637,19 +1713,13 @@
             public boolean onError(MediaPlayer mp, final int what, final int extra) {
                 setPlayerState(PLAYER_STATE_ERROR);
                 setBufferingState(BUFFERING_STATE_UNKNOWN);
-                synchronized (mEventCbLock) {
-                    for (final Pair<Executor, MediaPlayer2EventCallback> cb :
-                            mMp2EventCallbackRecords) {
-                        cb.first.execute(new Runnable() {
-                            @Override
-                            public void run() {
-                                // TODO: translate what value to MP2's definition.
-                                cb.second.onError(MediaPlayer2Impl.this, mCurrentDSD,
-                                        what, extra);
-                            }
-                        });
+                notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                    @Override
+                    public void notify(MediaPlayer2EventCallback cb) {
+                        int w = sErrorEventMap.getOrDefault(what, MEDIA_ERROR_UNKNOWN);
+                        cb.onError(MediaPlayer2Impl.this, mCurrentDSD, w, extra);
                     }
-                }
+                });
                 return true;
             }
         });
@@ -1665,57 +1735,40 @@
                         processPendingTask_l();
                     }
                 }
-                synchronized (mEventCbLock) {
-                    final long seekPos = getCurrentPosition();
-                    final int callbackCount = mPlayerEventCallbackMap.size();
-                    for (int i = 0; i < callbackCount; i++) {
-                        final Executor executor = mPlayerEventCallbackMap.valueAt(i);
-                        final PlayerEventCallback cb = mPlayerEventCallbackMap.keyAt(i);
-                        executor.execute(new Runnable() {
-                            @Override
-                            public void run() {
-                                // TODO: The actual seeked position might be different from the
-                                // requested position. Clarify which one is expected here.
-                                cb.onSeekCompleted(MediaPlayer2Impl.this, seekPos);
-                            }
-                        });
+                final long seekPos = getCurrentPosition();
+                notifyPlayerEvent(new PlayerEventNotifier() {
+                    @Override
+                    public void notify(PlayerEventCallback cb) {
+                        // TODO: The actual seeked position might be different from the
+                        // requested position. Clarify which one is expected here.
+                        cb.onSeekCompleted(MediaPlayer2Impl.this, seekPos);
                     }
-                }
+                });
             }
         });
         mPlayer.setOnTimedMetaDataAvailableListener(
                 new MediaPlayer.OnTimedMetaDataAvailableListener() {
                     @Override
                     public void onTimedMetaDataAvailable(MediaPlayer mp, final TimedMetaData data) {
-                        synchronized (mEventCbLock) {
-                            for (final Pair<Executor, MediaPlayer2EventCallback> cb :
-                                    mMp2EventCallbackRecords) {
-                                cb.first.execute(new Runnable() {
-                                    @Override
-                                    public void run() {
-                                        cb.second.onTimedMetaDataAvailable(MediaPlayer2Impl.this,
-                                                mCurrentDSD, data);
-                                    }
-                                });
+                        notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                            @Override
+                            public void notify(MediaPlayer2EventCallback cb) {
+                                cb.onTimedMetaDataAvailable(
+                                        MediaPlayer2Impl.this, mCurrentDSD, data);
                             }
-                        }
+                        });
                     }
                 });
         mPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() {
             @Override
             public boolean onInfo(MediaPlayer mp, final int what, final int extra) {
-                synchronized (mEventCbLock) {
-                    for (final Pair<Executor, MediaPlayer2EventCallback> cb :
-                            mMp2EventCallbackRecords) {
-                        cb.first.execute(new Runnable() {
-                            @Override
-                            public void run() {
-                                // TODO: translate what value to MP2's definition.
-                                cb.second.onInfo(MediaPlayer2Impl.this, mCurrentDSD, what, extra);
-                            }
-                        });
+                notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                    @Override
+                    public void notify(MediaPlayer2EventCallback cb) {
+                        int w = sInfoEventMap.getOrDefault(what, MEDIA_INFO_UNKNOWN);
+                        cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD, w, extra);
                     }
-                }
+                });
                 return true;
             }
         });
@@ -1725,19 +1778,64 @@
                 if (percent >= 100) {
                     setBufferingState(BUFFERING_STATE_BUFFERING_COMPLETE);
                 }
-                synchronized (mEventCbLock) {
-                    mBufferedPercentageCurrent.set(percent);
-                    for (final Pair<Executor, MediaPlayer2EventCallback> cb :
-                            mMp2EventCallbackRecords) {
-                        cb.first.execute(new Runnable() {
+                mBufferedPercentageCurrent.set(percent);
+                notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                    @Override
+                    public void notify(MediaPlayer2EventCallback cb) {
+                        cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
+                                MEDIA_INFO_BUFFERING_UPDATE, percent);
+                    }
+                });
+            }
+        });
+        mPlayer.setOnMediaTimeDiscontinuityListener(
+                new MediaPlayer.OnMediaTimeDiscontinuityListener() {
+                    @Override
+                    public void onMediaTimeDiscontinuity(
+                            MediaPlayer mp, final MediaTimestamp timestamp) {
+                        notifyMediaPlayer2Event(new Mp2EventNotifier() {
                             @Override
-                            public void run() {
-                                cb.second.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
-                                        MEDIA_INFO_BUFFERING_UPDATE, percent);
+                            public void notify(MediaPlayer2EventCallback cb) {
+                                cb.onMediaTimeDiscontinuity(
+                                        MediaPlayer2Impl.this, mCurrentDSD, timestamp);
                             }
                         });
                     }
-                }
+                });
+        mPlayer.setOnSubtitleDataListener(new MediaPlayer.OnSubtitleDataListener() {
+            @Override
+            public  void onSubtitleData(MediaPlayer mp, final SubtitleData data) {
+                notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                    @Override
+                    public void notify(MediaPlayer2EventCallback cb) {
+                        cb.onSubtitleData(MediaPlayer2Impl.this, mCurrentDSD, data);
+                    }
+                });
+            }
+        });
+        mPlayer.setOnDrmInfoListener(new MediaPlayer.OnDrmInfoListener() {
+            @Override
+            public void onDrmInfo(MediaPlayer mp, final MediaPlayer.DrmInfo drmInfo) {
+                notifyDrmEvent(new DrmEventNotifier() {
+                    @Override
+                    public void notify(DrmEventCallback cb) {
+                        cb.onDrmInfo(MediaPlayer2Impl.this, mCurrentDSD,
+                                new DrmInfoImpl(drmInfo.getPssh(), drmInfo.getSupportedSchemes()));
+                    }
+                });
+            }
+        });
+        mPlayer.setOnDrmPreparedListener(new MediaPlayer.OnDrmPreparedListener() {
+            @Override
+            public void onDrmPrepared(MediaPlayer mp, final int status) {
+                notifyDrmEvent(new DrmEventNotifier() {
+                    @Override
+                    public void notify(DrmEventCallback cb) {
+                        int s = sPrepareDrmStatusMap.getOrDefault(
+                                status, PREPARE_DRM_STATUS_PREPARATION_ERROR);
+                        cb.onDrmPrepared(MediaPlayer2Impl.this, mCurrentDSD, s);
+                    }
+                });
             }
         });
     }
@@ -1968,18 +2066,13 @@
             if (mMediaCallType == CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED) {
                 return;
             }
-            synchronized (mEventCbLock) {
-                for (final Pair<Executor, MediaPlayer2EventCallback> cb :
-                        mMp2EventCallbackRecords) {
-                    cb.first.execute(new Runnable() {
-                        @Override
-                        public void run() {
-                            cb.second.onCallCompleted(
-                                    MediaPlayer2Impl.this, mDSD, mMediaCallType, status);
-                        }
-                    });
+            notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                @Override
+                public void notify(MediaPlayer2EventCallback cb) {
+                    cb.onCallCompleted(
+                            MediaPlayer2Impl.this, mDSD, mMediaCallType, status);
                 }
-            }
+            });
         }
     };
 }
diff --git a/media/src/main/java/androidx/media/MediaPlayerBase.java b/media/src/main/java/androidx/media/MediaPlayerBase.java
index 6854ae0..de0e128 100644
--- a/media/src/main/java/androidx/media/MediaPlayerBase.java
+++ b/media/src/main/java/androidx/media/MediaPlayerBase.java
@@ -32,11 +32,9 @@
 import java.util.concurrent.Executor;
 
 /**
- * @hide
  * Base class for all media players that want media session.
  */
 @TargetApi(Build.VERSION_CODES.KITKAT)
-@RestrictTo(LIBRARY_GROUP)
 public abstract class MediaPlayerBase implements AutoCloseable {
     /**
      * @hide
diff --git a/media/src/main/java/androidx/media/MediaPlaylistAgent.java b/media/src/main/java/androidx/media/MediaPlaylistAgent.java
index 802512d..07838e8 100644
--- a/media/src/main/java/androidx/media/MediaPlaylistAgent.java
+++ b/media/src/main/java/androidx/media/MediaPlaylistAgent.java
@@ -33,7 +33,6 @@
 import java.util.concurrent.Executor;
 
 /**
- * @hide
  * MediaPlaylistAgent is the abstract class an application needs to derive from to pass an object
  * to a MediaSession2 that will override default playlist handling behaviors. It contains a set of
  * notify methods to signal MediaSession2 that playlist-related state has changed.
@@ -43,7 +42,6 @@
  * Used by {@link MediaSession2} and {@link MediaController2}.
  */
 // This class only includes methods that contain {@link MediaItem2}.
-@RestrictTo(LIBRARY_GROUP)
 public abstract class MediaPlaylistAgent {
     private static final String TAG = "MediaPlaylistAgent";
 
@@ -151,7 +149,11 @@
     }
 
     /**
-     * TODO: add javadoc
+     * Notifies the current playlist and playlist metadata. Call this API when the playlist is
+     * changed.
+     * <p>
+     * Registered {@link PlaylistEventCallback} would receive this event through the
+     * {@link PlaylistEventCallback#onPlaylistChanged(MediaPlaylistAgent, List, MediaMetadata2)}.
      */
     public final void notifyPlaylistChanged() {
         SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
@@ -171,7 +173,10 @@
     }
 
     /**
-     * TODO: add javadoc
+     * Notifies the current playlist metadata. Call this API when the playlist metadata is changed.
+     * <p>
+     * Registered {@link PlaylistEventCallback} would receive this event through the
+     * {@link PlaylistEventCallback#onPlaylistMetadataChanged(MediaPlaylistAgent, MediaMetadata2)}.
      */
     public final void notifyPlaylistMetadataChanged() {
         SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
@@ -189,7 +194,10 @@
     }
 
     /**
-     * TODO: add javadoc
+     * Notifies the current shuffle mode. Call this API when the shuffle mode is changed.
+     * <p>
+     * Registered {@link PlaylistEventCallback} would receive this event through the
+     * {@link PlaylistEventCallback#onShuffleModeChanged(MediaPlaylistAgent, int)}.
      */
     public final void notifyShuffleModeChanged() {
         SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
@@ -207,7 +215,10 @@
     }
 
     /**
-     * TODO: add javadoc
+     * Notifies the current repeat mode. Call this API when the repeat mode is changed.
+     * <p>
+     * Registered {@link PlaylistEventCallback} would receive this event through the
+     * {@link PlaylistEventCallback#onRepeatModeChanged(MediaPlaylistAgent, int)}.
      */
     public final void notifyRepeatModeChanged() {
         SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
@@ -232,10 +243,14 @@
     public abstract @Nullable List<MediaItem2> getPlaylist();
 
     /**
-     * Sets the playlist.
+     * Sets the playlist with the metadata.
+     * <p>
+     * When the playlist is changed, call {@link #notifyPlaylistChanged()} to notify changes to the
+     * registered callbacks.
      *
      * @param list playlist
      * @param metadata metadata of the playlist
+     * @see #notifyPlaylistChanged()
      */
     public abstract void setPlaylist(@NonNull List<MediaItem2> list,
             @Nullable MediaMetadata2 metadata);
@@ -248,9 +263,13 @@
     public abstract @Nullable MediaMetadata2 getPlaylistMetadata();
 
     /**
-     * Updates the playlist metadata
+     * Updates the playlist metadata.
+     * <p>
+     * When the playlist metadata is changed, call {@link #notifyPlaylistMetadataChanged()} to
+     * notify changes to the registered callbacks.
      *
      * @param metadata metadata of the playlist
+     * @see #notifyPlaylistMetadataChanged()
      */
     public abstract void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata);
 
@@ -261,7 +280,8 @@
 
     /**
      * Adds the media item to the playlist at position index. Index equals or greater than
-     * the current playlist size will add the item at the end of the playlist.
+     * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
+     * the playlist.
      * <p>
      * This will not change the currently playing media item.
      * If index is less than or equal to the current index of the playlist,
@@ -317,13 +337,17 @@
     public abstract @RepeatMode int getRepeatMode();
 
     /**
-     * Sets the repeat mode
+     * Sets the repeat mode.
+     * <p>
+     * When the repeat mode is changed, call {@link #notifyRepeatModeChanged()} to notify changes
+     * to the registered callbacks.
      *
      * @param repeatMode repeat mode
      * @see #REPEAT_MODE_NONE
      * @see #REPEAT_MODE_ONE
      * @see #REPEAT_MODE_ALL
      * @see #REPEAT_MODE_GROUP
+     * @see #notifyRepeatModeChanged()
      */
     public abstract void setRepeatMode(@RepeatMode int repeatMode);
 
@@ -338,12 +362,16 @@
     public abstract @ShuffleMode int getShuffleMode();
 
     /**
-     * Sets the shuffle mode
+     * Sets the shuffle mode.
+     * <p>
+     * When the shuffle mode is changed, call {@link #notifyShuffleModeChanged()} to notify changes
+     * to the registered callbacks.
      *
      * @param shuffleMode The shuffle mode
      * @see #SHUFFLE_MODE_NONE
      * @see #SHUFFLE_MODE_ALL
      * @see #SHUFFLE_MODE_GROUP
+     * @see #notifyShuffleModeChanged()
      */
     public abstract void setShuffleMode(@ShuffleMode int shuffleMode);
 
diff --git a/media/src/main/java/androidx/media/MediaSession2.java b/media/src/main/java/androidx/media/MediaSession2.java
index 2ecb7ce..909e979 100644
--- a/media/src/main/java/androidx/media/MediaSession2.java
+++ b/media/src/main/java/androidx/media/MediaSession2.java
@@ -48,7 +48,6 @@
 import java.util.concurrent.Executor;
 
 /**
- * @hide
  * Allows a media app to expose its transport controls and playback information in a process to
  * other processes including the Android framework and other apps. Common use cases are as follows.
  * <ul>
@@ -61,10 +60,6 @@
  * handle media keys. In general an app only needs one session for all playback, though multiple
  * sessions can be created to provide finer grain controls of media.
  * <p>
- * If you want to support background playback, {@link MediaSessionService2} is preferred
- * instead. With it, your playback can be revived even after playback is finished. See
- * {@link MediaSessionService2} for details.
- * <p>
  * A session can be obtained by {@link Builder}. The owner of the session may pass its session token
  * to other processes to allow them to create a {@link MediaController2} to interact with the
  * session.
@@ -77,11 +72,8 @@
  * and notify any controllers.
  * <p>
  * {@link MediaSession2} objects should be used on the thread on the looper.
- *
- * @see MediaSessionService2
  */
 @TargetApi(Build.VERSION_CODES.KITKAT)
-@RestrictTo(LIBRARY_GROUP)
 public class MediaSession2 extends MediaInterface2.SessionPlayer implements AutoCloseable {
     /**
      * @hide
@@ -163,21 +155,18 @@
     public static final int ERROR_CODE_SETUP_REQUIRED = 12;
 
     /**
-     * TODO: Fix {link DataSourceDesc}
      * Interface definition of a callback to be invoked when a {@link MediaItem2} in the playlist
-     * didn't have a {link DataSourceDesc} but it's needed now for preparing or playing it.
+     * didn't have a {@link DataSourceDesc} but it's needed now for preparing or playing it.
      *
      * #see #setOnDataSourceMissingHelper
      */
     public interface OnDataSourceMissingHelper {
         /**
-         * TODO: Fix {link DataSourceDesc}
-         * Called when a {@link MediaItem2} in the playlist didn't have a {link DataSourceDesc}
+         * Called when a {@link MediaItem2} in the playlist didn't have a {@link DataSourceDesc}
          * but it's needed now for preparing or playing it. Returned data source descriptor will be
          * sent to the player directly to prepare or play the contents.
          * <p>
-         * TODO: Fix {link DataSourceDesc}
-         * An exception may be thrown if the returned {link DataSourceDesc} is duplicated in the
+         * An exception may be thrown if the returned {@link DataSourceDesc} is duplicated in the
          * playlist, so items cannot be differentiated.
          *
          * @param session the session for this event
@@ -195,7 +184,6 @@
      * If it's not set, the session will accept all controllers and all incoming commands by
      * default.
      */
-    // TODO(jaewan): Move this to updatable for default implementation (b/74091963)
     public abstract static class SessionCallback {
         /**
          * Called when a controller is created for this session. Return allowed commands for
@@ -434,7 +422,7 @@
         /**
          * Called when a controller called {@link MediaController2#subscribeRoutesInfo()}
          * Session app should notify the routes information by calling
-         * {@link MediaSession2#notifyRoutesInfoChanged(ControllerInfo, List<Bundle>)}.
+         * {@link MediaSession2#notifyRoutesInfoChanged(ControllerInfo, List)}.
          *
          * @param session the session for this event
          * @param controller controller information
@@ -472,7 +460,6 @@
          * @param player the player for this event
          * @param item new item
          */
-        // TODO(jaewan): Use this (b/74316764)
         public void onCurrentMediaItemChanged(@NonNull MediaSession2 session,
                 @NonNull MediaPlayerBase player, @NonNull MediaItem2 item) { }
 
@@ -788,7 +775,7 @@
     public static final class ControllerInfo {
         private final int mUid;
         private final String mPackageName;
-        // TODO: IMediaControllerCallback should be used only for MediaSession2ImplBase
+        // Note: IMediaControllerCallback should be used only for MediaSession2ImplBase
         private final IMediaControllerCallback mIControllerCallback;
         private final boolean mIsTrusted;
 
@@ -801,7 +788,7 @@
             mUid = uid;
             mPackageName = packageName;
             mIControllerCallback = callback;
-            mIsTrusted = isTrusted();
+            mIsTrusted = false;
         }
 
         /**
@@ -824,11 +811,11 @@
          * command request.
          *
          * @return {@code true} if the controller is trusted.
+         * @hide
          */
-        // TODO: Remove this API
+        @RestrictTo(LIBRARY_GROUP)
         public boolean isTrusted() {
-            //return mProvider.isTrusted_impl();
-            return false;
+            return mIsTrusted;
         }
 
         IBinder getId() {
@@ -860,7 +847,6 @@
          */
         @RestrictTo(LIBRARY_GROUP)
         public @NonNull Bundle toBundle() {
-            // TODO: Fill here.
             return new Bundle();
         }
 
@@ -870,7 +856,6 @@
          */
         @RestrictTo(LIBRARY_GROUP)
         public static @NonNull ControllerInfo fromBundle(@NonNull Context context, Bundle bundle) {
-            // TODO: Fill here.
             return new ControllerInfo(context, -1, -1, "TODO", null);
         }
 
@@ -951,7 +936,7 @@
         }
 
         /**
-         * Return whether it's enabled
+         * Return whether it's enabled.
          *
          * @return {@code true} if enabled. {@code false} otherwise.
          */
@@ -1008,7 +993,10 @@
             private boolean mEnabled;
 
             /**
-             * TODO: javadoc
+             * Sets the {@link SessionCommand2} that would be sent to the session when the button
+             * is clicked.
+             *
+             * @param command session command
              */
             public @NonNull Builder setCommand(@Nullable SessionCommand2 command) {
                 mCommand = command;
@@ -1016,7 +1004,13 @@
             }
 
             /**
-             * TODO: javadoc
+             * Sets the bitmap-type (e.g. PNG) icon resource id of the button.
+             * <p>
+             * None bitmap type (e.g. VectorDrawabale) may cause unexpected behavior when it's sent
+             * to {@link MediaController2} app, so please avoid using it especially for the older
+             * platform (API < 21).
+             *
+             * @param resId resource id of the button
              */
             public @NonNull Builder setIconResId(int resId) {
                 mIconResId = resId;
@@ -1024,7 +1018,9 @@
             }
 
             /**
-             * TODO: javadoc
+             * Sets the display name of the button.
+             *
+             * @param displayName display name of the button
              */
             public @NonNull Builder setDisplayName(@Nullable String displayName) {
                 mDisplayName = displayName;
@@ -1032,7 +1028,11 @@
             }
 
             /**
-             * TODO: javadoc
+             * Sets whether the button is enabled. Can be {@code false} to indicate that the button
+             * should be shown but isn't clickable.
+             *
+             * @param enabled {@code true} if the button is enabled and ready.
+             *          {@code false} otherwise.
              */
             public @NonNull Builder setEnabled(boolean enabled) {
                 mEnabled = enabled;
@@ -1040,7 +1040,9 @@
             }
 
             /**
-             * TODO: javadoc
+             * Sets the extras of the button.
+             *
+             * @param extras extras information of the button
              */
             public @NonNull Builder setExtras(@Nullable Bundle extras) {
                 mExtras = extras;
@@ -1048,7 +1050,9 @@
             }
 
             /**
-             * TODO: javadoc
+             * Builds the {@link CommandButton}.
+             *
+             * @return a new {@link CommandButton}
              */
             public @NonNull CommandButton build() {
                 return new CommandButton(mCommand, mIconResId, mDisplayName, mExtras, mEnabled);
@@ -1170,7 +1174,9 @@
     }
 
     /**
-     * TODO: add javadoc
+     * Returns the list of connected controller.
+     *
+     * @return list of {@link ControllerInfo}
      */
     public @NonNull List<ControllerInfo> getConnectedControllers() {
         return mImpl.getConnectedControllers();
@@ -1409,14 +1415,12 @@
      * Sets the data source missing helper. Helper will be used to provide default implementation of
      * {@link MediaPlaylistAgent} when it isn't set by developer.
      * <p>
-     * TODO: Fix {link DataSourceDesc}
      * Default implementation of the {@link MediaPlaylistAgent} will call helper when a
-     * {@link MediaItem2} in the playlist doesn't have a {link DataSourceDesc}. This may happen
+     * {@link MediaItem2} in the playlist doesn't have a {@link DataSourceDesc}. This may happen
      * when
      * <ul>
-     * TODO: Fix {link DataSourceDesc}
      *      <li>{@link MediaItem2} specified by {@link #setPlaylist(List, MediaMetadata2)} doesn't
-     *          have {link DataSourceDesc}</li>
+     *          have {@link DataSourceDesc}</li>
      *      <li>{@link MediaController2#addPlaylistItem(int, MediaItem2)} is called and accepted
      *          by {@link SessionCallback#onCommandRequest(
      *          MediaSession2, ControllerInfo, SessionCommand2)}.
@@ -1477,8 +1481,7 @@
      * list. Wait for {@link SessionCallback#onPlaylistChanged(MediaSession2, MediaPlaylistAgent,
      * List, MediaMetadata2)} to know the operation finishes.
      * <p>
-     * TODO: Fix {link DataSourceDesc}
-     * You may specify a {@link MediaItem2} without {link DataSourceDesc}. In that case,
+     * You may specify a {@link MediaItem2} without {@link DataSourceDesc}. In that case,
      * {@link MediaPlaylistAgent} has responsibility to dynamically query {link DataSourceDesc}
      * when such media item is ready for preparation or play. Default implementation needs
      * {@link OnDataSourceMissingHelper} for such case.
@@ -1551,7 +1554,8 @@
 
     /**
      * Adds the media item to the playlist at position index. Index equals or greater than
-     * the current playlist size will add the item at the end of the playlist.
+     * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
+     * the playlist.
      * <p>
      * This will not change the currently playing media item.
      * If index is less than or equal to the current index of the play list,
diff --git a/media/src/main/java/androidx/media/MediaSession2ImplBase.java b/media/src/main/java/androidx/media/MediaSession2ImplBase.java
index 989475a..e474b45 100644
--- a/media/src/main/java/androidx/media/MediaSession2ImplBase.java
+++ b/media/src/main/java/androidx/media/MediaSession2ImplBase.java
@@ -60,7 +60,7 @@
 @TargetApi(Build.VERSION_CODES.KITKAT)
 class MediaSession2ImplBase extends MediaSession2.SupportLibraryImpl {
     static final String TAG = "MS2ImplBase";
-    static final boolean DEBUG = true; // TODO: Log.isLoggable(TAG, Log.DEBUG);
+    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private final Object mLock = new Object();
 
diff --git a/media/src/main/java/androidx/media/MediaSession2StubImplBase.java b/media/src/main/java/androidx/media/MediaSession2StubImplBase.java
index 5fb31b7..48e641e 100644
--- a/media/src/main/java/androidx/media/MediaSession2StubImplBase.java
+++ b/media/src/main/java/androidx/media/MediaSession2StubImplBase.java
@@ -75,6 +75,7 @@
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO;
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SET_SPEED;
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM;
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST;
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM;
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
@@ -131,7 +132,7 @@
 class MediaSession2StubImplBase extends MediaSessionCompat.Callback {
 
     private static final String TAG = "MS2StubImplBase";
-    private static final boolean DEBUG = true; // TODO: Log.isLoggable(TAG, Log.DEBUG);
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private static final SparseArray<SessionCommand2> sCommandsForOnCommandRequest =
             new SparseArray<>();
@@ -531,7 +532,7 @@
     }
 
     void notifyCurrentMediaItemChanged(final MediaItem2 item) {
-        notifyAll(COMMAND_CODE_PLAYLIST_GET_LIST, new Session2Runnable() {
+        notifyAll(COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM, new Session2Runnable() {
             @Override
             public void run(ControllerInfo controller) throws RemoteException {
                 Bundle bundle = new Bundle();
@@ -622,7 +623,7 @@
 
     void notifyPlaylistChanged(final List<MediaItem2> playlist,
             final MediaMetadata2 metadata) {
-        notifyAll(SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST, new Session2Runnable() {
+        notifyAll(COMMAND_CODE_PLAYLIST_GET_LIST, new Session2Runnable() {
             @Override
             public void run(ControllerInfo controller) throws RemoteException {
                 Bundle bundle = new Bundle();
@@ -866,29 +867,29 @@
                             allowedCommands.toBundle());
                     resultData.putInt(ARGUMENT_PLAYER_STATE, mSession.getPlayerState());
                     resultData.putInt(ARGUMENT_BUFFERING_STATE, mSession.getBufferingState());
-                    synchronized (mLock) {
-                        resultData.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT,
-                                mSession.getPlaybackStateCompat());
-                        // TODO: Insert MediaMetadataCompat
-                    }
+                    resultData.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT,
+                            mSession.getPlaybackStateCompat());
                     resultData.putInt(ARGUMENT_REPEAT_MODE, mSession.getRepeatMode());
                     resultData.putInt(ARGUMENT_SHUFFLE_MODE, mSession.getShuffleMode());
                     final List<MediaItem2> playlist = allowedCommands.hasCommand(
-                            SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST)
-                            ? mSession.getPlaylist() : null;
+                            COMMAND_CODE_PLAYLIST_GET_LIST) ? mSession.getPlaylist() : null;
                     if (playlist != null) {
                         resultData.putParcelableArray(ARGUMENT_PLAYLIST,
                                 MediaUtils2.toMediaItem2ParcelableArray(playlist));
                     }
                     final MediaItem2 currentMediaItem =
-                            allowedCommands.hasCommand(COMMAND_CODE_PLAYLIST_GET_LIST)
+                            allowedCommands.hasCommand(COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM)
                                     ? mSession.getCurrentMediaItem() : null;
                     if (currentMediaItem != null) {
                         resultData.putBundle(ARGUMENT_MEDIA_ITEM, currentMediaItem.toBundle());
                     }
                     resultData.putBundle(ARGUMENT_PLAYBACK_INFO,
                             mSession.getPlaybackInfo().toBundle());
-
+                    final MediaMetadata2 playlistMetadata = mSession.getPlaylistMetadata();
+                    if (playlistMetadata != null) {
+                        resultData.putBundle(ARGUMENT_PLAYLIST_METADATA,
+                                playlistMetadata.toBundle());
+                    }
                     // Double check if session is still there, because close() can be
                     // called in another thread.
                     if (mSession.isClosed()) {
diff --git a/media/src/main/java/androidx/media/Rating2.java b/media/src/main/java/androidx/media/Rating2.java
index 28f7c70..8c81331 100644
--- a/media/src/main/java/androidx/media/Rating2.java
+++ b/media/src/main/java/androidx/media/Rating2.java
@@ -30,7 +30,6 @@
 import java.lang.annotation.RetentionPolicy;
 
 /**
- * @hide
  * A class to encapsulate rating information used as content metadata.
  * A rating is defined by its rating style (see {@link #RATING_HEART},
  * {@link #RATING_THUMB_UP_DOWN}, {@link #RATING_3_STARS}, {@link #RATING_4_STARS},
@@ -40,7 +39,6 @@
  */
 // New version of Rating with following change
 //   - Don't implement Parcelable for updatable support.
-@RestrictTo(LIBRARY_GROUP)
 public final class Rating2 {
     /**
      * @hide
diff --git a/media/src/main/java/androidx/media/SessionCommand2.java b/media/src/main/java/androidx/media/SessionCommand2.java
index aca8234..f017941 100644
--- a/media/src/main/java/androidx/media/SessionCommand2.java
+++ b/media/src/main/java/androidx/media/SessionCommand2.java
@@ -31,14 +31,12 @@
 import java.util.List;
 
 /**
- * @hide
  * Define a command that a {@link MediaController2} can send to a {@link MediaSession2}.
  * <p>
  * If {@link #getCommandCode()} isn't {@link #COMMAND_CODE_CUSTOM}), it's predefined command.
  * If {@link #getCommandCode()} is {@link #COMMAND_CODE_CUSTOM}), it's custom command and
  * {@link #getCustomCommand()} shouldn't be {@code null}.
  */
-@RestrictTo(LIBRARY_GROUP)
 public final class SessionCommand2 {
     /**
      * Command code for the custom command which can be defined by string action in the
@@ -194,10 +192,6 @@
     /**
      * Command code for {@link MediaController2#getPlaylist()}. This will expose metadata
      * information to the controller.
-     * <p>
-     * Command would be sent directly to the playlist agent if the session doesn't reject the
-     * request through the
-     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
      */
     public static final int COMMAND_CODE_PLAYLIST_GET_LIST = 18;
 
@@ -213,10 +207,6 @@
     /**
      * Command code for {@link MediaController2#getPlaylistMetadata()}. This will expose
      * metadata information to the controller.
-     * <p>
-     * Command would be sent directly to the playlist agent if the session doesn't reject the
-     * request through the
-     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
      */
     public static final int COMMAND_CODE_PLAYLIST_GET_LIST_METADATA = 20;
 
@@ -230,6 +220,12 @@
     public static final int COMMAND_CODE_PLAYLIST_SET_LIST_METADATA = 21;
 
     /**
+     * Command code for {@link MediaController2#getCurrentMediaItem()}. This will expose
+     * metadata information to the controller.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM = 20;
+
+    /**
      * Command code for {@link MediaController2#playFromMediaId(String, Bundle)}.
      */
     public static final int COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID = 22;
@@ -279,40 +275,53 @@
      */
     public static final int COMMAND_CODE_SESSION_SELECT_ROUTE = 38;
 
-
     /**
+     * @hide
      * Command code for {@link MediaBrowser2#getChildren(String, int, int, Bundle)}.
      */
+    @RestrictTo(LIBRARY_GROUP)
     public static final int COMMAND_CODE_LIBRARY_GET_CHILDREN = 29;
 
     /**
+     * @hide
      * Command code for {@link MediaBrowser2#getItem(String)}.
      */
+    @RestrictTo(LIBRARY_GROUP)
     public static final int COMMAND_CODE_LIBRARY_GET_ITEM = 30;
 
     /**
+     * @hide
      * Command code for {@link MediaBrowser2#getLibraryRoot(Bundle)}.
      */
+    @RestrictTo(LIBRARY_GROUP)
     public static final int COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT = 31;
 
     /**
+     * @hide
      * Command code for {@link MediaBrowser2#getSearchResult(String, int, int, Bundle)}.
      */
+    @RestrictTo(LIBRARY_GROUP)
     public static final int COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT = 32;
 
     /**
+     * @hide
      * Command code for {@link MediaBrowser2#search(String, Bundle)}.
      */
+    @RestrictTo(LIBRARY_GROUP)
     public static final int COMMAND_CODE_LIBRARY_SEARCH = 33;
 
     /**
+     * @hide
      * Command code for {@link MediaBrowser2#subscribe(String, Bundle)}.
      */
+    @RestrictTo(LIBRARY_GROUP)
     public static final int COMMAND_CODE_LIBRARY_SUBSCRIBE = 34;
 
     /**
+     * @hide
      * Command code for {@link MediaBrowser2#unsubscribe(String)}.
      */
+    @RestrictTo(LIBRARY_GROUP)
     public static final int COMMAND_CODE_LIBRARY_UNSUBSCRIBE = 35;
 
     /**
@@ -429,7 +438,6 @@
             return false;
         }
         SessionCommand2 other = (SessionCommand2) obj;
-        // TODO(jaewan): Compare Commands with the generated UUID, as we're doing for the MI2.
         return mCommandCode == other.mCommandCode
                 && TextUtils.equals(mCustomCommand, other.mCustomCommand);
     }
diff --git a/media/src/main/java/androidx/media/SessionCommandGroup2.java b/media/src/main/java/androidx/media/SessionCommandGroup2.java
index af4d189..691eb70 100644
--- a/media/src/main/java/androidx/media/SessionCommandGroup2.java
+++ b/media/src/main/java/androidx/media/SessionCommandGroup2.java
@@ -34,10 +34,8 @@
 import java.util.Set;
 
 /**
- * @hide
  * A set of {@link SessionCommand2} which represents a command group.
  */
-@RestrictTo(LIBRARY_GROUP)
 public final class SessionCommandGroup2 {
 
     private static final String TAG = "SessionCommandGroup2";
@@ -114,7 +112,6 @@
     }
 
     private void addCommandsWithPrefix(String prefix) {
-        // TODO(jaewan): (Can be post-P): Don't use reflection for this purpose.
         final Field[] fields = SessionCommand2.class.getFields();
         if (fields != null) {
             for (int i = 0; i < fields.length; i++) {
diff --git a/media/src/main/java/androidx/media/SessionToken2.java b/media/src/main/java/androidx/media/SessionToken2.java
index 6b6021a..eb42297 100644
--- a/media/src/main/java/androidx/media/SessionToken2.java
+++ b/media/src/main/java/androidx/media/SessionToken2.java
@@ -30,6 +30,7 @@
 
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 
 import java.lang.annotation.Retention;
@@ -37,9 +38,7 @@
 import java.util.List;
 
 /**
- * @hide
- * Represents an ongoing {@link MediaSession2} or a {@link MediaSessionService2}.
- * If it's representing a session service, it may not be ongoing.
+ * Represents an ongoing {@link MediaSession2}.
  * <p>
  * This may be passed to apps by the session owner to allow them to create a
  * {@link MediaController2} to communicate with the session.
@@ -50,15 +49,31 @@
 //   - Stop implementing Parcelable for updatable support
 //   - Represent session and library service (formerly browser service) in one class.
 //     Previously MediaSession.Token was for session and ComponentName was for service.
-@RestrictTo(LIBRARY_GROUP)
 public final class SessionToken2 {
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(value = {TYPE_SESSION, TYPE_SESSION_SERVICE, TYPE_LIBRARY_SERVICE})
     public @interface TokenType {
     }
 
+    /**
+     * Type for {@link MediaSession2}.
+     */
     public static final int TYPE_SESSION = 0;
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
     public static final int TYPE_SESSION_SERVICE = 1;
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
     public static final int TYPE_LIBRARY_SERVICE = 2;
 
     //private final SessionToken2Provider mProvider;
@@ -79,14 +94,17 @@
     private final String mServiceName;
     private final String mId;
     private final MediaSessionCompat.Token mSessionCompatToken;
+    private final ComponentName mComponentName;
 
     /**
+     * @hide
      * Constructor for the token. You can only create token for session service or library service
      * to use by {@link MediaController2} or {@link MediaBrowser2}.
      *
      * @param context The context.
      * @param serviceComponent The component name of the media browser service.
      */
+    @RestrictTo(LIBRARY_GROUP)
     public SessionToken2(@NonNull Context context, @NonNull ComponentName serviceComponent) {
         this(context, serviceComponent, UID_UNKNOWN);
     }
@@ -106,6 +124,7 @@
         if (serviceComponent == null) {
             throw new IllegalArgumentException("serviceComponent shouldn't be null");
         }
+        mComponentName = serviceComponent;
         mPackageName = serviceComponent.getPackageName();
         mServiceName = serviceComponent.getClassName();
         // Calculate uid if it's not specified.
@@ -117,11 +136,9 @@
                 throw new IllegalArgumentException("Cannot find package " + mPackageName);
             }
         }
-
         mUid = uid;
 
         // Infer id and type from package name and service name
-        // TODO(jaewan): Handle multi-user.
         String id = getSessionIdFromService(manager, MediaLibraryService2.SERVICE_INTERFACE,
                 serviceComponent);
         if (id != null) {
@@ -144,12 +161,14 @@
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    public SessionToken2(int uid, int type, String packageName, String serviceName,
+    SessionToken2(int uid, int type, String packageName, String serviceName,
             String id, MediaSessionCompat.Token sessionCompatToken) {
         mUid = uid;
         mType = type;
         mPackageName = packageName;
         mServiceName = serviceName;
+        mComponentName = (mType == TYPE_SESSION) ? null
+                : new ComponentName(packageName, serviceName);
         mId = id;
         mSessionCompatToken = sessionCompatToken;
     }
@@ -193,14 +212,14 @@
     /**
      * @return package name
      */
-    public String getPackageName() {
+    public @NonNull String getPackageName() {
         return mPackageName;
     }
 
     /**
-     * @return service name
+     * @return service name. Can be {@code null} for TYPE_SESSION.
      */
-    public String getServiceName() {
+    public @Nullable String getServiceName() {
         return mServiceName;
     }
 
@@ -210,8 +229,7 @@
      */
     @RestrictTo(LIBRARY_GROUP)
     public ComponentName getComponentName() {
-        // TODO: Cache the component name?
-        return mType == TYPE_SESSION ? null : new ComponentName(mPackageName, mServiceName);
+        return mComponentName;
     }
 
     /**
@@ -224,7 +242,6 @@
     /**
      * @return type of the token
      * @see #TYPE_SESSION
-     * @see #TYPE_SESSION_SERVICE
      */
     public @TokenType int getType() {
         return mType;
diff --git a/samples/SupportSliceDemos/build.gradle b/samples/SupportSliceDemos/build.gradle
index 4d518ff..8675869 100644
--- a/samples/SupportSliceDemos/build.gradle
+++ b/samples/SupportSliceDemos/build.gradle
@@ -21,9 +21,9 @@
 }
 
 dependencies {
-    implementation(project(":slices-view"))
-    implementation(project(":slices-builders"))
-    implementation(project(":slices-core"))
+    implementation(project(":slice-view"))
+    implementation(project(":slice-builders"))
+    implementation(project(":slice-core"))
     implementation("com.android.support:design:28.0.0-SNAPSHOT", { transitive = false })
     implementation(project(":transition"))
     implementation(project(":recyclerview"))
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 4facfb3..f1e2976 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
@@ -16,6 +16,8 @@
 
 package com.example.androidx.slice.demos;
 
+import static androidx.slice.core.SliceHints.INFINITY;
+
 import static com.example.androidx.slice.demos.SampleSliceProvider.URI_PATHS;
 import static com.example.androidx.slice.demos.SampleSliceProvider.getUri;
 
@@ -48,6 +50,7 @@
 import androidx.lifecycle.LiveData;
 import androidx.slice.Slice;
 import androidx.slice.SliceItem;
+import androidx.slice.SliceMetadata;
 import androidx.slice.widget.EventInfo;
 import androidx.slice.widget.SliceLiveData;
 import androidx.slice.widget.SliceView;
@@ -239,7 +242,17 @@
             mContainer.addView(v);
             mSliceLiveData = SliceLiveData.fromUri(this, uri);
             v.setMode(mSelectedMode);
-            mSliceLiveData.observe(this, v);
+            mSliceLiveData.observe(this, slice -> {
+                v.setSlice(slice);
+                SliceMetadata metadata = SliceMetadata.from(this, slice);
+                long expiry = metadata.getExpiry();
+                if (expiry != INFINITY) {
+                    // Shows the updated text after the TTL expires.
+                    v.postDelayed(() -> v.setSlice(slice),
+                            expiry - System.currentTimeMillis() + 15);
+                }
+            });
+            mSliceLiveData.observe(this, slice -> Log.d(TAG, "Slice: " + slice));
         } else {
             Log.w(TAG, "Invalid uri, skipping slice: " + uri);
         }
diff --git a/settings.gradle b/settings.gradle
index 65ef3f4..16e5be7 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -77,9 +77,9 @@
 includeProject(":recommendation", "recommendation")
 includeProject(":recyclerview", "v7/recyclerview")
 includeProject(":recyclerview-selection", "recyclerview-selection")
-includeProject(":slices-core", "slices/core")
-includeProject(":slices-view", "slices/view")
-includeProject(":slices-builders", "slices/builders")
+includeProject(":slice-core", "slices/core")
+includeProject(":slice-view", "slices/view")
+includeProject(":slice-builders", "slices/builders")
 includeProject(":slidingpanelayout", "slidingpanelayout")
 includeProject(":fragment-ktx", "fragment/ktx")
 includeProject(":swiperefreshlayout", "swiperefreshlayout")
diff --git a/slices/builders/build.gradle b/slices/builders/build.gradle
index a9624b2..91c7599 100644
--- a/slices/builders/build.gradle
+++ b/slices/builders/build.gradle
@@ -23,7 +23,7 @@
 }
 
 dependencies {
-    implementation(project(":slices-core"))
+    implementation(project(":slice-core"))
     implementation project(":annotation")
     implementation project(path: ':core')
 }
diff --git a/slices/core/src/main/java/androidx/slice/Slice.java b/slices/core/src/main/java/androidx/slice/Slice.java
index d18ef23..a2c3325 100644
--- a/slices/core/src/main/java/androidx/slice/Slice.java
+++ b/slices/core/src/main/java/androidx/slice/Slice.java
@@ -455,20 +455,37 @@
     public String toString(String indent) {
         StringBuilder sb = new StringBuilder();
         sb.append(indent);
-        sb.append("slice: ");
-        sb.append("\n");
-        indent += "   ";
+        sb.append("slice ");
+        addHints(sb, mHints);
+        sb.append("{\n");
+        String nextIndent = indent + "  ";
         for (int i = 0; i < mItems.length; i++) {
             SliceItem item = mItems[i];
-            sb.append(item.toString(indent));
-            if (!FORMAT_SLICE.equals(item.getFormat())) {
-                sb.append("\n");
-            }
+            sb.append(item.toString(nextIndent));
         }
+        sb.append(indent);
+        sb.append("}");
         return sb.toString();
     }
 
     /**
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY)
+    public static void addHints(StringBuilder sb, String[] hints) {
+        if (hints.length == 0) return;
+
+        sb.append("(");
+        int end = hints.length - 1;
+        for (int i = 0; i < end; i++) {
+            sb.append(hints[i]);
+            sb.append(", ");
+        }
+        sb.append(hints[end]);
+        sb.append(") ");
+    }
+
+    /**
      * Turns a slice Uri into slice content.
      *
      * @hide
diff --git a/slices/core/src/main/java/androidx/slice/SliceItem.java b/slices/core/src/main/java/androidx/slice/SliceItem.java
index 004e51d..964c5df 100644
--- a/slices/core/src/main/java/androidx/slice/SliceItem.java
+++ b/slices/core/src/main/java/androidx/slice/SliceItem.java
@@ -25,6 +25,8 @@
 import static android.app.slice.SliceItem.FORMAT_TEXT;
 import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
 
+import static androidx.slice.Slice.addHints;
+
 import android.app.PendingIntent;
 import android.app.RemoteInput;
 import android.content.Context;
@@ -412,36 +414,35 @@
     @RestrictTo(Scope.LIBRARY)
     public String toString(String indent) {
         StringBuilder sb = new StringBuilder();
-        if (!FORMAT_SLICE.equals(getFormat())) {
-            sb.append(indent);
-            sb.append(getFormat());
-            sb.append(": ");
-        }
         switch (getFormat()) {
             case FORMAT_SLICE:
                 sb.append(getSlice().toString(indent));
                 break;
             case FORMAT_ACTION:
-                sb.append(getAction());
-                sb.append("\n");
+                sb.append(indent).append(getAction()).append(",\n");
                 sb.append(getSlice().toString(indent));
                 break;
             case FORMAT_TEXT:
-                sb.append(getText());
+                sb.append(indent).append('"').append(getText()).append('"');
                 break;
             case FORMAT_IMAGE:
-                sb.append(getIcon());
+                sb.append(indent).append(getIcon());
                 break;
             case FORMAT_INT:
-                sb.append(getInt());
+                sb.append(indent).append(getInt());
                 break;
-            case FORMAT_TIMESTAMP:
-                sb.append(getTimestamp());
+            case FORMAT_LONG:
+                sb.append(indent).append(getLong());
                 break;
             default:
-                sb.append(SliceItem.typeToString(getFormat()));
+                sb.append(indent).append(SliceItem.typeToString(getFormat()));
                 break;
         }
+        if (!FORMAT_SLICE.equals(getFormat())) {
+            sb.append(' ');
+            addHints(sb, mHints);
+        }
+        sb.append(",\n");
         return sb.toString();
     }
 }
diff --git a/slices/core/src/main/java/androidx/slice/SliceProvider.java b/slices/core/src/main/java/androidx/slice/SliceProvider.java
index 7ec9232..9061ab1 100644
--- a/slices/core/src/main/java/androidx/slice/SliceProvider.java
+++ b/slices/core/src/main/java/androidx/slice/SliceProvider.java
@@ -20,20 +20,8 @@
 import static android.app.slice.SliceProvider.SLICE_TYPE;
 
 import static androidx.slice.compat.SliceProviderCompat.EXTRA_BIND_URI;
-import static androidx.slice.compat.SliceProviderCompat.EXTRA_INTENT;
 import static androidx.slice.compat.SliceProviderCompat.EXTRA_PKG;
 import static androidx.slice.compat.SliceProviderCompat.EXTRA_PROVIDER_PKG;
-import static androidx.slice.compat.SliceProviderCompat.EXTRA_SLICE;
-import static androidx.slice.compat.SliceProviderCompat.EXTRA_SLICE_DESCENDANTS;
-import static androidx.slice.compat.SliceProviderCompat.METHOD_GET_DESCENDANTS;
-import static androidx.slice.compat.SliceProviderCompat.METHOD_GET_PINNED_SPECS;
-import static androidx.slice.compat.SliceProviderCompat.METHOD_MAP_INTENT;
-import static androidx.slice.compat.SliceProviderCompat.METHOD_MAP_ONLY_INTENT;
-import static androidx.slice.compat.SliceProviderCompat.METHOD_PIN;
-import static androidx.slice.compat.SliceProviderCompat.METHOD_SLICE;
-import static androidx.slice.compat.SliceProviderCompat.METHOD_UNPIN;
-import static androidx.slice.compat.SliceProviderCompat.addSpecs;
-import static androidx.slice.compat.SliceProviderCompat.getSpecs;
 import static androidx.slice.core.SliceHints.HINT_PERMISSION_REQUEST;
 
 import android.app.PendingIntent;
@@ -48,13 +36,8 @@
 import android.database.ContentObserver;
 import android.database.Cursor;
 import android.net.Uri;
-import android.os.Binder;
 import android.os.Bundle;
 import android.os.CancellationSignal;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Process;
-import android.os.StrictMode;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -63,11 +46,10 @@
 import androidx.annotation.RestrictTo;
 import androidx.core.app.CoreComponentFactory;
 import androidx.core.os.BuildCompat;
-import androidx.slice.compat.CompatPinnedList;
+import androidx.slice.compat.SliceProviderCompat;
 import androidx.slice.compat.SliceProviderWrapperContainer;
 import androidx.slice.core.R;
 
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Set;
@@ -118,14 +100,9 @@
 
     private static final String TAG = "SliceProvider";
 
-    private static final String DATA_PREFIX = "slice_data_";
-    private static final long SLICE_BIND_ANR = 2000;
-
     private static final boolean DEBUG = false;
-    private final Handler mHandler = new Handler(Looper.getMainLooper());
-    private CompatPinnedList mPinnedList;
 
-    private String mCallback;
+    private SliceProviderCompat mCompat;
 
     /**
      * Implement this to initialize your slice provider on startup.
@@ -154,8 +131,9 @@
 
     @Override
     public final boolean onCreate() {
-        mPinnedList = new CompatPinnedList(getContext(),
-                DATA_PREFIX + getClass().getName());
+        if (!BuildCompat.isAtLeastP()) {
+            mCompat = new SliceProviderCompat(this);
+        }
         return onCreateSliceProvider();
     }
 
@@ -167,115 +145,7 @@
 
     @Override
     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());
-            Bundle b = new Bundle();
-            b.putParcelable(EXTRA_SLICE, s.toBundle());
-            return b;
-        } else if (method.equals(METHOD_MAP_INTENT)) {
-            Intent intent = extras.getParcelable(EXTRA_INTENT);
-            Uri uri = onMapIntentToUri(intent);
-            Bundle b = new Bundle();
-            if (uri != null) {
-                Set<SliceSpec> specs = getSpecs(extras);
-                Slice s = handleBindSlice(uri, specs, getCallingPackage());
-                b.putParcelable(EXTRA_SLICE, s.toBundle());
-            } else {
-                b.putParcelable(EXTRA_SLICE, null);
-            }
-            return b;
-        } else if (method.equals(METHOD_MAP_ONLY_INTENT)) {
-            Intent intent = extras.getParcelable(EXTRA_INTENT);
-            Uri uri = onMapIntentToUri(intent);
-            Bundle b = new Bundle();
-            b.putParcelable(EXTRA_SLICE, uri);
-            return b;
-        } else if (method.equals(METHOD_PIN)) {
-            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
-            Set<SliceSpec> specs = getSpecs(extras);
-            String pkg = extras.getString(EXTRA_PKG);
-            if (mPinnedList.addPin(uri, pkg, specs)) {
-                handleSlicePinned(uri);
-            }
-            return null;
-        } else if (method.equals(METHOD_UNPIN)) {
-            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
-            String pkg = extras.getString(EXTRA_PKG);
-            if (mPinnedList.removePin(uri, pkg)) {
-                handleSliceUnpinned(uri);
-            }
-            return null;
-        } else if (method.equals(METHOD_GET_PINNED_SPECS)) {
-            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
-            Bundle b = new Bundle();
-            addSpecs(b, mPinnedList.getSpecs(uri));
-            return b;
-        } else if (method.equals(METHOD_GET_DESCENDANTS)) {
-            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
-            Bundle b = new Bundle();
-            b.putParcelableArrayList(EXTRA_SLICE_DESCENDANTS,
-                    new ArrayList<>(handleGetDescendants(uri)));
-            return b;
-        }
-        return super.call(method, arg, extras);
-    }
-
-    private Collection<Uri> handleGetDescendants(Uri uri) {
-        mCallback = "onGetSliceDescendants";
-        mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
-        try {
-            return onGetSliceDescendants(uri);
-        } finally {
-            mHandler.removeCallbacks(mAnr);
-        }
-    }
-
-    private void handleSlicePinned(final Uri sliceUri) {
-        mCallback = "onSlicePinned";
-        mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
-        try {
-            onSlicePinned(sliceUri);
-        } finally {
-            mHandler.removeCallbacks(mAnr);
-        }
-    }
-
-    private void handleSliceUnpinned(final Uri sliceUri) {
-        mCallback = "onSliceUnpinned";
-        mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
-        try {
-            onSliceUnpinned(sliceUri);
-        } finally {
-            mHandler.removeCallbacks(mAnr);
-        }
-    }
-
-    private Slice handleBindSlice(final Uri sliceUri, final Set<SliceSpec> specs,
-            final String callingPkg) {
-        // This can be removed once Slice#bindSlice is removed and everyone is using
-        // SliceManager#bindSlice.
-        String pkg = callingPkg != null ? callingPkg
-                : getContext().getPackageManager().getNameForUid(Binder.getCallingUid());
-        if (Binder.getCallingUid() != Process.myUid()) {
-            try {
-                getContext().enforceUriPermission(sliceUri,
-                        Binder.getCallingPid(), Binder.getCallingUid(),
-                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
-                        "Slice binding requires write access to Uri");
-            } catch (SecurityException e) {
-                return createPermissionSlice(getContext(), sliceUri, pkg);
-            }
-        }
-        return onBindSliceStrict(sliceUri, specs);
+        return mCompat != null ? mCompat.call(method, arg, extras) : null;
     }
 
     /**
@@ -337,35 +207,6 @@
         }
     }
 
-    private Slice onBindSliceStrict(Uri sliceUri, Set<SliceSpec> specs) {
-        StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
-        mCallback = "onBindSlice";
-        mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
-        try {
-            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
-                    .detectAll()
-                    .penaltyDeath()
-                    .build());
-            SliceProvider.setSpecs(specs);
-            try {
-                return onBindSlice(sliceUri);
-            } finally {
-                SliceProvider.setSpecs(null);
-                mHandler.removeCallbacks(mAnr);
-            }
-        } finally {
-            StrictMode.setThreadPolicy(oldPolicy);
-        }
-    }
-
-    private final Runnable mAnr = new Runnable() {
-        @Override
-        public void run() {
-            Process.sendSignal(Process.myPid(), Process.SIGNAL_QUIT);
-            Log.wtf(TAG, "Timed out while handling slice callback " + mCallback);
-        }
-    };
-
     /**
      * Implemented to create a slice.
      * <p>
diff --git a/slices/core/src/main/java/androidx/slice/compat/CompatPermissionManager.java b/slices/core/src/main/java/androidx/slice/compat/CompatPermissionManager.java
new file mode 100644
index 0000000..247a763
--- /dev/null
+++ b/slices/core/src/main/java/androidx/slice/compat/CompatPermissionManager.java
@@ -0,0 +1,198 @@
+/*
+ * 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.slice.compat;
+
+import static androidx.core.content.PermissionChecker.PERMISSION_DENIED;
+import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.collection.ArraySet;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+class CompatPermissionManager {
+
+    private static final String TAG = "CompatPermissionManager";
+    public static final String ALL_SUFFIX = "_all";
+
+    private final Context mContext;
+    private final String mPrefsName;
+
+    CompatPermissionManager(Context context, String prefsName) {
+        mContext = context;
+        mPrefsName = prefsName;
+    }
+
+    private SharedPreferences getPrefs() {
+        return mContext.getSharedPreferences(mPrefsName, Context.MODE_PRIVATE);
+    }
+
+    public int checkSlicePermission(Uri uri, int pid, int uid) {
+        for (String pkg : mContext.getPackageManager().getPackagesForUid(uid)) {
+            if (checkSlicePermission(uri, pkg) == PERMISSION_GRANTED) {
+                return PERMISSION_GRANTED;
+            }
+        }
+        // Fall back to allowing uri permissions through.
+        return mContext.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+    }
+
+    private int checkSlicePermission(Uri uri, String pkg) {
+        PermissionState state = getPermissionState(pkg, uri.getAuthority());
+        return state.hasAccess(uri.getPathSegments()) ? PERMISSION_GRANTED : PERMISSION_DENIED;
+    }
+
+    public void grantSlicePermission(Uri uri, String toPkg) {
+        PermissionState state = getPermissionState(toPkg, uri.getAuthority());
+        if (state.addPath(uri.getPathSegments())) {
+            persist(state);
+        }
+    }
+
+    public void revokeSlicePermission(Uri uri, String toPkg) {
+        PermissionState state = getPermissionState(toPkg, uri.getAuthority());
+        if (state.removePath(uri.getPathSegments())) {
+            persist(state);
+        }
+    }
+
+    private synchronized void persist(PermissionState state) {
+        if (!getPrefs().edit()
+                .putStringSet(state.getKey(), state.toPersistable())
+                .putBoolean(state.getKey() + ALL_SUFFIX, state.hasAllPermissions())
+                .commit()) {
+            Log.e(TAG, "Unable to persist permissions");
+        }
+    }
+
+    private PermissionState getPermissionState(String pkg, String authority) {
+        String key = pkg + "_" + authority;
+        Set<String> grant = getPrefs().getStringSet(key, Collections.<String>emptySet());
+        boolean hasAllPermissions = getPrefs().getBoolean(key + ALL_SUFFIX, false);
+        return new PermissionState(grant, key, hasAllPermissions);
+    }
+
+    public static class PermissionState {
+
+        private final ArraySet<String[]> mPaths = new ArraySet<>();
+        private final String mKey;
+
+        PermissionState(Set<String> grant, String key, boolean hasAllPermissions) {
+            if (hasAllPermissions) {
+                mPaths.add(new String[0]);
+            } else {
+                for (String g : grant) {
+                    mPaths.add(decodeSegments(g));
+                }
+            }
+            mKey = key;
+        }
+
+        public boolean hasAllPermissions() {
+            return hasAccess(Collections.<String>emptyList());
+        }
+
+        public String getKey() {
+            return mKey;
+        }
+
+        public Set<String> toPersistable() {
+            ArraySet<String> ret = new ArraySet<>();
+            for (String[] path : mPaths) {
+                ret.add(encodeSegments(path));
+            }
+            return ret;
+        }
+
+        public boolean hasAccess(List<String> path) {
+            String[] inPath = path.toArray(new String[path.size()]);
+            for (String[] p : mPaths) {
+                if (isPathPrefixMatch(p, inPath)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        boolean addPath(List<String> path) {
+            String[] pathSegs = path.toArray(new String[path.size()]);
+            for (int i = mPaths.size() - 1; i >= 0; i--) {
+                String[] existing = mPaths.valueAt(i);
+                if (isPathPrefixMatch(existing, pathSegs)) {
+                    // Nothing to add here.
+                    return false;
+                }
+                if (isPathPrefixMatch(pathSegs, existing)) {
+                    mPaths.removeAt(i);
+                }
+            }
+            mPaths.add(pathSegs);
+            return true;
+        }
+
+        boolean removePath(List<String> path) {
+            boolean changed = false;
+            String[] pathSegs = path.toArray(new String[path.size()]);
+            for (int i = mPaths.size() - 1; i >= 0; i--) {
+                String[] existing = mPaths.valueAt(i);
+                if (isPathPrefixMatch(pathSegs, existing)) {
+                    changed = true;
+                    mPaths.removeAt(i);
+                }
+            }
+            return changed;
+        }
+
+        private boolean isPathPrefixMatch(String[] prefix, String[] path) {
+            final int prefixSize = prefix.length;
+            if (path.length < prefixSize) return false;
+
+            for (int i = 0; i < prefixSize; i++) {
+                if (!Objects.equals(path[i], prefix[i])) {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        private String encodeSegments(String[] s) {
+            String[] out = new String[s.length];
+            for (int i = 0; i < s.length; i++) {
+                out[i] = Uri.encode(s[i]);
+            }
+            return TextUtils.join("/", out);
+        }
+
+        private String[] decodeSegments(String s) {
+            String[] sets = s.split("/", -1);
+            for (int i = 0; i < sets.length; i++) {
+                sets[i] = Uri.decode(sets[i]);
+            }
+            return sets;
+        }
+    }
+}
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 f3270f8..5feaa1e 100644
--- a/slices/core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
+++ b/slices/core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
@@ -20,7 +20,6 @@
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
 import android.content.DialogInterface.OnDismissListener;
-import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.net.Uri;
@@ -78,11 +77,8 @@
     @Override
     public void onClick(DialogInterface dialog, int which) {
         if (which == DialogInterface.BUTTON_POSITIVE) {
-            grantUriPermission(mCallingPkg, mUri.buildUpon().path("").build(),
-                    Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
-                            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
-                            | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
-            getContentResolver().notifyChange(mUri, null);
+            SliceProviderCompat.grantSlicePermission(this, getPackageName(), mCallingPkg,
+                    mUri.buildUpon().path("").build());
         }
         finish();
     }
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 61bc65e..2d06d8c 100644
--- a/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
+++ b/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
@@ -15,8 +15,12 @@
  */
 package androidx.slice.compat;
 
+import static android.app.slice.SliceManager.CATEGORY_SLICE;
+import static android.app.slice.SliceManager.SLICE_METADATA_KEY;
 import static android.app.slice.SliceProvider.SLICE_TYPE;
 
+import static androidx.core.content.PermissionChecker.PERMISSION_DENIED;
+
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -24,10 +28,15 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
+import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
 import android.os.Parcelable;
+import android.os.Process;
 import android.os.RemoteException;
+import android.os.StrictMode;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -36,8 +45,8 @@
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 import androidx.slice.Slice;
+import androidx.slice.SliceProvider;
 import androidx.slice.SliceSpec;
-import androidx.slice.core.SliceHints;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -51,8 +60,11 @@
 @RestrictTo(Scope.LIBRARY)
 public class SliceProviderCompat {
     private static final String TAG = "SliceProviderCompat";
+    private static final String DATA_PREFIX = "slice_data_";
+    private static final String PERMS_PREFIX = "slice_perms_";
 
-    public static final String EXTRA_BIND_URI = "slice_uri";
+    private static final long SLICE_BIND_ANR = 2000;
+
     public static final String METHOD_SLICE = "bind_slice";
     public static final String METHOD_MAP_INTENT = "map_slice";
     public static final String METHOD_PIN = "pin_slice";
@@ -60,7 +72,11 @@
     public static final String METHOD_GET_PINNED_SPECS = "get_specs";
     public static final String METHOD_MAP_ONLY_INTENT = "map_only";
     public static final String METHOD_GET_DESCENDANTS = "get_descendants";
+    public static final String METHOD_CHECK_PERMISSION = "check_perms";
+    public static final String METHOD_GRANT_PERMISSION = "grant_perms";
+    public static final String METHOD_REVOKE_PERMISSION = "revoke_perms";
 
+    public static final String EXTRA_BIND_URI = "slice_uri";
     public static final String EXTRA_INTENT = "slice_intent";
     public static final String EXTRA_SLICE = "slice";
     public static final String EXTRA_SUPPORTED_SPECS = "specs";
@@ -68,6 +84,198 @@
     public static final String EXTRA_PKG = "pkg";
     public static final String EXTRA_PROVIDER_PKG = "provider_pkg";
     public static final String EXTRA_SLICE_DESCENDANTS = "slice_descendants";
+    public static final String EXTRA_UID = "uid";
+    public static final String EXTRA_PID = "pid";
+    public static final String EXTRA_RESULT = "result";
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+    private String mCallback;
+    private final SliceProvider mProvider;
+    private CompatPinnedList mPinnedList;
+    private CompatPermissionManager mPermissionManager;
+
+    public SliceProviderCompat(SliceProvider provider) {
+        mProvider = provider;
+        mPinnedList = new CompatPinnedList(provider.getContext(),
+                DATA_PREFIX + getClass().getName());
+        mPermissionManager = new CompatPermissionManager(provider.getContext(),
+                PERMS_PREFIX + getClass().getName());
+    }
+
+    private Context getContext() {
+        return mProvider.getContext();
+    }
+
+    private String getCallingPackage() {
+        return mProvider.getCallingPackage();
+    }
+
+    /**
+     * Called by SliceProvider when compat is needed.
+     */
+    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());
+            Bundle b = new Bundle();
+            b.putParcelable(EXTRA_SLICE, s.toBundle());
+            return b;
+        } else if (method.equals(METHOD_MAP_INTENT)) {
+            Intent intent = extras.getParcelable(EXTRA_INTENT);
+            Uri uri = mProvider.onMapIntentToUri(intent);
+            Bundle b = new Bundle();
+            if (uri != null) {
+                Set<SliceSpec> specs = getSpecs(extras);
+                Slice s = handleBindSlice(uri, specs, getCallingPackage());
+                b.putParcelable(EXTRA_SLICE, s.toBundle());
+            } else {
+                b.putParcelable(EXTRA_SLICE, null);
+            }
+            return b;
+        } else if (method.equals(METHOD_MAP_ONLY_INTENT)) {
+            Intent intent = extras.getParcelable(EXTRA_INTENT);
+            Uri uri = mProvider.onMapIntentToUri(intent);
+            Bundle b = new Bundle();
+            b.putParcelable(EXTRA_SLICE, uri);
+            return b;
+        } else if (method.equals(METHOD_PIN)) {
+            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+            Set<SliceSpec> specs = getSpecs(extras);
+            String pkg = extras.getString(EXTRA_PKG);
+            if (mPinnedList.addPin(uri, pkg, specs)) {
+                handleSlicePinned(uri);
+            }
+            return null;
+        } else if (method.equals(METHOD_UNPIN)) {
+            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+            String pkg = extras.getString(EXTRA_PKG);
+            if (mPinnedList.removePin(uri, pkg)) {
+                handleSliceUnpinned(uri);
+            }
+            return null;
+        } else if (method.equals(METHOD_GET_PINNED_SPECS)) {
+            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+            Bundle b = new Bundle();
+            addSpecs(b, mPinnedList.getSpecs(uri));
+            return b;
+        } else if (method.equals(METHOD_GET_DESCENDANTS)) {
+            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+            Bundle b = new Bundle();
+            b.putParcelableArrayList(EXTRA_SLICE_DESCENDANTS,
+                    new ArrayList<>(handleGetDescendants(uri)));
+            return b;
+        } else if (method.equals(METHOD_CHECK_PERMISSION)) {
+            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+            String pkg = extras.getString(EXTRA_PKG);
+            int pid = extras.getInt(EXTRA_PID);
+            int uid = extras.getInt(EXTRA_UID);
+            Bundle b = new Bundle();
+            b.putInt(EXTRA_RESULT, mPermissionManager.checkSlicePermission(uri, pid, uid));
+            return b;
+        } else if (method.equals(METHOD_GRANT_PERMISSION)) {
+            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+            String toPkg = extras.getString(EXTRA_PKG);
+            if (Binder.getCallingUid() != Process.myUid()) {
+                throw new SecurityException("Only the owning process can manage slice permissions");
+            }
+            mPermissionManager.grantSlicePermission(uri, toPkg);
+        } else if (method.equals(METHOD_REVOKE_PERMISSION)) {
+            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+            String toPkg = extras.getString(EXTRA_PKG);
+            if (Binder.getCallingUid() != Process.myUid()) {
+                throw new SecurityException("Only the owning process can manage slice permissions");
+            }
+            mPermissionManager.revokeSlicePermission(uri, toPkg);
+        }
+        return null;
+    }
+
+    private Collection<Uri> handleGetDescendants(Uri uri) {
+        mCallback = "onGetSliceDescendants";
+        mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
+        try {
+            return mProvider.onGetSliceDescendants(uri);
+        } finally {
+            mHandler.removeCallbacks(mAnr);
+        }
+    }
+
+    private void handleSlicePinned(final Uri sliceUri) {
+        mCallback = "onSlicePinned";
+        mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
+        try {
+            mProvider.onSlicePinned(sliceUri);
+        } finally {
+            mHandler.removeCallbacks(mAnr);
+        }
+    }
+
+    private void handleSliceUnpinned(final Uri sliceUri) {
+        mCallback = "onSliceUnpinned";
+        mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
+        try {
+            mProvider.onSliceUnpinned(sliceUri);
+        } finally {
+            mHandler.removeCallbacks(mAnr);
+        }
+    }
+
+    private Slice handleBindSlice(final Uri sliceUri, final Set<SliceSpec> specs,
+            final String callingPkg) {
+        // This can be removed once Slice#bindSlice is removed and everyone is using
+        // SliceManager#bindSlice.
+        String pkg = callingPkg != null ? callingPkg
+                : getContext().getPackageManager().getNameForUid(Binder.getCallingUid());
+        if (Binder.getCallingUid() != Process.myUid()) {
+            try {
+                getContext().enforceUriPermission(sliceUri,
+                        Binder.getCallingPid(), Binder.getCallingUid(),
+                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
+                        "Slice binding requires write access to Uri");
+            } catch (SecurityException e) {
+                return mProvider.createPermissionSlice(getContext(), sliceUri, pkg);
+            }
+        }
+        return onBindSliceStrict(sliceUri, specs);
+    }
+
+    private Slice onBindSliceStrict(Uri sliceUri, Set<SliceSpec> specs) {
+        StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
+        mCallback = "onBindSlice";
+        mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
+        try {
+            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
+                    .detectAll()
+                    .penaltyDeath()
+                    .build());
+            SliceProvider.setSpecs(specs);
+            try {
+                return mProvider.onBindSlice(sliceUri);
+            } finally {
+                SliceProvider.setSpecs(null);
+                mHandler.removeCallbacks(mAnr);
+            }
+        } finally {
+            StrictMode.setThreadPolicy(oldPolicy);
+        }
+    }
+
+    private final Runnable mAnr = new Runnable() {
+        @Override
+        public void run() {
+            Process.sendSignal(Process.myPid(), Process.SIGNAL_QUIT);
+            Log.wtf(TAG, "Timed out while handling slice callback " + mCallback);
+        }
+    };
 
     /**
      * Compat version of {@link Slice#bindSlice}.
@@ -137,6 +345,10 @@
      */
     public static Slice bindSlice(Context context, Intent intent,
             Set<SliceSpec> supportedSpecs) {
+        Preconditions.checkNotNull(intent, "intent");
+        Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
+                || intent.getData() != null,
+                String.format("Slice intent must be explicit %s", intent));
         ContentResolver resolver = context.getContentResolver();
 
         // Check if the intent has data for the slice uri on it and use that
@@ -145,10 +357,24 @@
             return bindSlice(context, intentData, supportedSpecs);
         }
         // Otherwise ask the app
+        Intent queryIntent = new Intent(intent);
+        if (!queryIntent.hasCategory(CATEGORY_SLICE)) {
+            queryIntent.addCategory(CATEGORY_SLICE);
+        }
         List<ResolveInfo> providers =
-                context.getPackageManager().queryIntentContentProviders(intent, 0);
-        if (providers == null) {
-            throw new IllegalArgumentException("Unable to resolve intent " + intent);
+                context.getPackageManager().queryIntentContentProviders(queryIntent, 0);
+        if (providers == null || providers.isEmpty()) {
+            // There are no providers, see if this activity has a direct link.
+            ResolveInfo resolve = context.getPackageManager().resolveActivity(intent,
+                    PackageManager.GET_META_DATA);
+            if (resolve != null && resolve.activityInfo != null
+                    && resolve.activityInfo.metaData != null
+                    && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
+                return bindSlice(context, Uri.parse(
+                        resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY)),
+                        supportedSpecs);
+            }
+            return null;
         }
         String authority = providers.get(0).providerInfo.authority;
         Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
@@ -258,7 +484,8 @@
      */
     public static Uri mapIntentToUri(Context context, Intent intent) {
         Preconditions.checkNotNull(intent, "intent");
-        Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null,
+        Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
+                || intent.getData() != null,
                 String.format("Slice intent must be explicit %s", intent));
         ContentResolver resolver = context.getContentResolver();
 
@@ -268,17 +495,21 @@
             return intentData;
         }
         // Otherwise ask the app
+        Intent queryIntent = new Intent(intent);
+        if (!queryIntent.hasCategory(CATEGORY_SLICE)) {
+            queryIntent.addCategory(CATEGORY_SLICE);
+        }
         List<ResolveInfo> providers =
-                context.getPackageManager().queryIntentContentProviders(intent, 0);
+                context.getPackageManager().queryIntentContentProviders(queryIntent, 0);
         if (providers == null || providers.isEmpty()) {
             // There are no providers, see if this activity has a direct link.
             ResolveInfo resolve = context.getPackageManager().resolveActivity(intent,
                     PackageManager.GET_META_DATA);
             if (resolve != null && resolve.activityInfo != null
                     && resolve.activityInfo.metaData != null
-                    && resolve.activityInfo.metaData.containsKey(SliceHints.SLICE_METADATA_KEY)) {
+                    && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
                 return Uri.parse(
-                        resolve.activityInfo.metaData.getString(SliceHints.SLICE_METADATA_KEY));
+                        resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY));
             }
             return null;
         }
@@ -319,6 +550,60 @@
         return Collections.emptyList();
     }
 
-    private SliceProviderCompat() {
+    /**
+     * Compat version of {@link android.app.slice.SliceManager#checkSlicePermission}.
+     */
+    public static int checkSlicePermission(Context context, String packageName, Uri uri, int pid,
+            int uid) {
+        ContentResolver resolver = context.getContentResolver();
+        try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
+            Bundle extras = new Bundle();
+            extras.putParcelable(EXTRA_BIND_URI, uri);
+            extras.putString(EXTRA_PKG, packageName);
+            extras.putInt(EXTRA_PID, pid);
+            extras.putInt(EXTRA_UID, uid);
+
+            final Bundle res = provider.call(METHOD_CHECK_PERMISSION, null, extras);
+            return res.getInt(EXTRA_RESULT);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Unable to check slice permission", e);
+        }
+        return PERMISSION_DENIED;
+    }
+
+    /**
+     * Compat version of {@link android.app.slice.SliceManager#grantSlicePermission}.
+     */
+    public static void grantSlicePermission(Context context, String packageName, String toPackage,
+            Uri uri) {
+        ContentResolver resolver = context.getContentResolver();
+        try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
+            Bundle extras = new Bundle();
+            extras.putParcelable(EXTRA_BIND_URI, uri);
+            extras.putString(EXTRA_PROVIDER_PKG, packageName);
+            extras.putString(EXTRA_PKG, toPackage);
+
+            provider.call(METHOD_GRANT_PERMISSION, null, extras);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Unable to get slice descendants", e);
+        }
+    }
+
+    /**
+     * Compat version of {@link android.app.slice.SliceManager#revokeSlicePermission}.
+     */
+    public static void revokeSlicePermission(Context context, String packageName, String toPackage,
+            Uri uri) {
+        ContentResolver resolver = context.getContentResolver();
+        try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
+            Bundle extras = new Bundle();
+            extras.putParcelable(EXTRA_BIND_URI, uri);
+            extras.putString(EXTRA_PROVIDER_PKG, packageName);
+            extras.putString(EXTRA_PKG, toPackage);
+
+            provider.call(METHOD_REVOKE_PERMISSION, null, extras);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Unable to get slice descendants", e);
+        }
     }
 }
diff --git a/slices/view/api/current.txt b/slices/view/api/current.txt
index 4d97ef2..5a37826 100644
--- a/slices/view/api/current.txt
+++ b/slices/view/api/current.txt
@@ -113,6 +113,7 @@
     ctor public SliceView(android.content.Context, android.util.AttributeSet, int);
     ctor public SliceView(android.content.Context, android.util.AttributeSet, int, int);
     method public int getMode();
+    method public androidx.slice.Slice getSlice();
     method public java.util.List<androidx.slice.SliceItem> getSliceActions();
     method public void onChanged(androidx.slice.Slice);
     method public void onClick(android.view.View);
diff --git a/slices/view/build.gradle b/slices/view/build.gradle
index 5836d20..d214a19 100644
--- a/slices/view/build.gradle
+++ b/slices/view/build.gradle
@@ -23,8 +23,8 @@
 }
 
 dependencies {
-    implementation(project(":slices-core"))
-    implementation(project(":slices-builders"))
+    implementation(project(":slice-core"))
+    implementation(project(":slice-builders"))
     implementation(project(":recyclerview"))
     api(ARCH_LIFECYCLE_LIVEDATA_CORE, libs.exclude_annotations_transitive)
 
diff --git a/slices/view/src/androidTest/AndroidManifest.xml b/slices/view/src/androidTest/AndroidManifest.xml
index 78f3ad8..7c90f90 100644
--- a/slices/view/src/androidTest/AndroidManifest.xml
+++ b/slices/view/src/androidTest/AndroidManifest.xml
@@ -27,6 +27,7 @@
                   android:exported="true">
             <intent-filter>
                 <action android:name="androidx.slice.action.TEST" />
+                <category android:name="android.app.slice.category.SLICE" />
             </intent-filter>
         </provider>
 
diff --git a/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java b/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java
index 61d5c2b..b54335d 100644
--- a/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java
+++ b/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java
@@ -145,8 +145,8 @@
         when(mSliceProvider.onMapIntentToUri(eq(intent))).thenReturn(expected);
         Uri uri = mManager.mapIntentToUri(intent);
 
-        assertEquals(expected, uri);
         verify(mSliceProvider).onMapIntentToUri(eq(intent));
+        assertEquals(expected, uri);
     }
 
     @Test
diff --git a/slices/view/src/androidTest/java/androidx/slice/SlicePermissionTest.java b/slices/view/src/androidTest/java/androidx/slice/SlicePermissionTest.java
new file mode 100644
index 0000000..ffb65e0
--- /dev/null
+++ b/slices/view/src/androidTest/java/androidx/slice/SlicePermissionTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.slice;
+
+import static androidx.core.content.PermissionChecker.PERMISSION_DENIED;
+import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.Uri;
+import android.os.Process;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SlicePermissionTest {
+
+    private static final Uri BASE_URI = Uri.parse("content://androidx.slice.view.test/");
+    private final Context mContext = InstrumentationRegistry.getContext();
+    private String mTestPkg;
+    private int mTestUid;
+    private int mTestPid;
+    private SliceManager mSliceManager;
+
+    @Before
+    public void setup() throws NameNotFoundException {
+        mSliceManager = SliceManager.getInstance(mContext);
+        mTestPkg = mContext.getPackageName();
+        mTestUid = mContext.getPackageManager().getPackageUid(mTestPkg, 0);
+        mTestPid = Process.myPid();
+    }
+
+    @After
+    public void tearDown() {
+        mSliceManager.revokeSlicePermission(mTestPkg, BASE_URI);
+    }
+
+    @Test
+    public void testGrant() {
+        assertEquals(PERMISSION_DENIED,
+                mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+        mSliceManager.grantSlicePermission(mTestPkg, BASE_URI);
+
+        assertEquals(PERMISSION_GRANTED,
+                mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+    }
+
+    @Test
+    public void testGrantParent() {
+        Uri uri = BASE_URI.buildUpon()
+                .appendPath("something")
+                .build();
+
+        assertEquals(PERMISSION_DENIED,
+                mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+
+        mSliceManager.grantSlicePermission(mTestPkg, BASE_URI);
+
+        assertEquals(PERMISSION_GRANTED,
+                mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+    }
+
+    @Test
+    public void testGrantParentExpands() {
+        Uri uri = BASE_URI.buildUpon()
+                .appendPath("something")
+                .build();
+
+        assertEquals(PERMISSION_DENIED,
+                mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+
+        mSliceManager.grantSlicePermission(mTestPkg, uri);
+
+        // Only sub-path granted.
+        assertEquals(PERMISSION_GRANTED,
+                mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+        assertEquals(PERMISSION_DENIED,
+                mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+        mSliceManager.grantSlicePermission(mTestPkg, BASE_URI);
+
+        // Now all granted.
+        assertEquals(PERMISSION_GRANTED,
+                mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+        assertEquals(PERMISSION_GRANTED,
+                mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+    }
+
+    @Test
+    public void testGrantChild() {
+        Uri uri = BASE_URI.buildUpon()
+                .appendPath("something")
+                .build();
+
+        assertEquals(PERMISSION_DENIED,
+                mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+        mSliceManager.grantSlicePermission(mTestPkg, uri);
+
+        // Still no permission because only a child was granted
+        assertEquals(PERMISSION_DENIED,
+                mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+    }
+
+    @Test
+    public void testRevoke() {
+        assertEquals(PERMISSION_DENIED,
+                mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+        mSliceManager.grantSlicePermission(mTestPkg, BASE_URI);
+
+        assertEquals(PERMISSION_GRANTED,
+                mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+        mSliceManager.revokeSlicePermission(mTestPkg, BASE_URI);
+
+        assertEquals(PERMISSION_DENIED,
+                mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+    }
+
+    @Test
+    public void testRevokeParent() {
+        Uri uri = BASE_URI.buildUpon()
+                .appendPath("something")
+                .build();
+        assertEquals(PERMISSION_DENIED,
+                mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+
+        mSliceManager.grantSlicePermission(mTestPkg, uri);
+
+        assertEquals(PERMISSION_GRANTED,
+                mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+
+        mSliceManager.revokeSlicePermission(mTestPkg, BASE_URI);
+
+        // Revoked because parent was revoked
+        assertEquals(PERMISSION_DENIED,
+                mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+    }
+
+    @Test
+    public void testRevokeChild() {
+        Uri uri = BASE_URI.buildUpon()
+                .appendPath("something")
+                .build();
+        assertEquals(PERMISSION_DENIED,
+                mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+        mSliceManager.grantSlicePermission(mTestPkg, BASE_URI);
+
+        assertEquals(PERMISSION_GRANTED,
+                mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+        mSliceManager.revokeSlicePermission(mTestPkg, uri);
+
+        // Not revoked because child was revoked.
+        assertEquals(PERMISSION_GRANTED,
+                mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+    }
+
+}
diff --git a/slices/view/src/main/java/androidx/slice/SliceManager.java b/slices/view/src/main/java/androidx/slice/SliceManager.java
index 63c56e8..37a1960 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManager.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManager.java
@@ -141,9 +141,8 @@
     public abstract @Nullable Slice bindSlice(@NonNull Uri uri);
 
     /**
-     * Turns a slice intent into slice content. Expects an explicit intent. If there is no
-     * {@link android.content.ContentProvider} associated with the given intent this will throw
-     * {@link IllegalArgumentException}.
+     * Turns a slice intent into slice content. Is a shortcut to perform the action
+     * of both {@link #mapIntentToUri(Intent)} and {@link #bindSlice(Uri)} at once.
      *
      * @param intent The intent associated with a slice.
      * @return The Slice provided by the app or null if none is given.
@@ -154,12 +153,23 @@
     public abstract @Nullable Slice bindSlice(@NonNull Intent intent);
 
     /**
-     * Turns a slice intent into a slice uri. Expects an explicit intent. If there is no
-     * {@link android.content.ContentProvider} associated with the given intent this will throw
-     * {@link IllegalArgumentException}.
-     *
+     * Turns a slice intent into a slice uri. Expects an explicit intent.
+     * <p>
+     * This goes through a several stage resolution process to determine if any slice
+     * can represent this intent.
+     * <ol>
+     *  <li> If the intent contains data that {@link android.content.ContentResolver#getType} is
+     *  {@link android.app.slice.SliceProvider#SLICE_TYPE} then the data will be returned.</li>
+     *  <li>If the intent explicitly points at an activity, and that activity has
+     *  meta-data for key {@link android.app.slice.SliceManager#SLICE_METADATA_KEY},
+     *  then the Uri specified there will be returned.</li>
+     *  <li>Lastly, if the intent with {@link android.app.slice.SliceManager#CATEGORY_SLICE} added
+     *  resolves to a provider, then the provider will be asked to
+     *  {@link SliceProvider#onMapIntentToUri} and that result will be returned.</li>
+     *  <li>If no slice is found, then {@code null} is returned.</li>
+     * </ol>
      * @param intent The intent associated with a slice.
-     * @return The Slice Uri provided by the app or null if none is given.
+     * @return The Slice Uri provided by the app or null if none exists.
      * @see Slice
      * @see SliceProvider#onMapIntentToUri(Intent)
      * @see Intent
diff --git a/slices/view/src/main/java/androidx/slice/SliceManagerBase.java b/slices/view/src/main/java/androidx/slice/SliceManagerBase.java
index fb9fccd..5192dad 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManagerBase.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManagerBase.java
@@ -19,11 +19,9 @@
 import static androidx.slice.widget.SliceLiveData.SUPPORTED_SPECS;
 
 import android.content.Context;
-import android.content.Intent;
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.AsyncTask;
-import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.ArrayMap;
@@ -31,7 +29,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
-import androidx.core.content.PermissionChecker;
 
 import java.util.concurrent.Executor;
 
@@ -71,31 +68,6 @@
         if (impl != null) impl.stopListening();
     }
 
-    @Override
-    @PermissionChecker.PermissionResult
-    public int checkSlicePermission(@NonNull Uri uri, int pid, int uid) {
-        // TODO: Switch off Uri permissions.
-        return mContext.checkUriPermission(uri, pid, uid,
-                Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
-    }
-
-    @Override
-    public void grantSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
-        // TODO: Switch off Uri permissions.
-        mContext.grantUriPermission(toPackage, uri,
-                Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
-                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
-                        | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
-    }
-
-    @Override
-    public void revokeSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
-        // TODO: Switch off Uri permissions.
-        if (Build.VERSION.SDK_INT >= 26) {
-            mContext.revokeUriPermission(toPackage, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
-        }
-    }
-
 
     private SliceListenerImpl getListener(Uri uri, SliceCallback callback,
             SliceListenerImpl listener) {
diff --git a/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java b/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
index 1badbb4..f8cda4d 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
@@ -76,6 +76,24 @@
     }
 
     @Override
+    public int checkSlicePermission(Uri uri, int pid, int uid) {
+        return SliceProviderCompat.checkSlicePermission(mContext, mContext.getPackageName(), uri,
+                pid, uid);
+    }
+
+    @Override
+    public void grantSlicePermission(String toPackage, Uri uri) {
+        SliceProviderCompat.grantSlicePermission(mContext, mContext.getPackageName(), toPackage,
+                uri);
+    }
+
+    @Override
+    public void revokeSlicePermission(String toPackage, Uri uri) {
+        SliceProviderCompat.revokeSlicePermission(mContext, mContext.getPackageName(), toPackage,
+                uri);
+    }
+
+    @Override
     public Collection<Uri> getSliceDescendants(Uri uri) {
         return SliceProviderCompat.getSliceDescendants(mContext, uri);
     }
diff --git a/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java b/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
index b28efd1..4c19b84 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
@@ -28,6 +28,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
+import androidx.core.content.PermissionChecker;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -89,6 +90,22 @@
         return mManager.getSliceDescendants(uri);
     }
 
+    @Override
+    @PermissionChecker.PermissionResult
+    public int checkSlicePermission(@NonNull Uri uri, int pid, int uid) {
+        return mManager.checkSlicePermission(uri, pid, uid);
+    }
+
+    @Override
+    public void grantSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
+        mManager.grantSlicePermission(toPackage, uri);
+    }
+
+    @Override
+    public void revokeSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
+        mManager.revokeSlicePermission(toPackage, uri);
+    }
+
     @Nullable
     @Override
     public Uri mapIntentToUri(@NonNull Intent intent) {
diff --git a/slices/view/src/main/java/androidx/slice/widget/ListContent.java b/slices/view/src/main/java/androidx/slice/widget/ListContent.java
index cde20da..1cbf357 100644
--- a/slices/view/src/main/java/androidx/slice/widget/ListContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/ListContent.java
@@ -108,9 +108,12 @@
      * @return the total height of all the rows contained in the provided list.
      */
     public static int getListHeight(Context context, List<SliceItem> listItems) {
+        if (listItems == null) {
+            return 0;
+        }
         int height = 0;
         boolean hasRealHeader = false;
-        if (listItems.size() > 0) {
+        if (!listItems.isEmpty()) {
             SliceItem maybeHeader = listItems.get(0);
             hasRealHeader = !maybeHeader.hasAnyHints(HINT_LIST_ITEM, HINT_HORIZONTAL);
         }
@@ -200,6 +203,7 @@
         return mSeeMoreItem;
     }
 
+    @NonNull
     public ArrayList<SliceItem> getRowItems() {
         return mRowItems;
     }
diff --git a/slices/view/src/main/java/androidx/slice/widget/SliceView.java b/slices/view/src/main/java/androidx/slice/widget/SliceView.java
index 3eeda45..855bded 100644
--- a/slices/view/src/main/java/androidx/slice/widget/SliceView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/SliceView.java
@@ -413,7 +413,18 @@
     }
 
     /**
+     * @return the slice being used to populate this view.
+     */
+    @Nullable
+    public Slice getSlice() {
+        return mCurrentSlice;
+    }
+
+    /**
      * Returns the slice actions presented in this view.
+     * <p>
+     * Note that these may be different from {@link SliceMetadata#getSliceActions()} if the actions
+     * set on the view have been adjusted using {@link #setSliceActions(List)}.
      */
     @Nullable
     public List<SliceItem> getSliceActions() {
diff --git a/v7/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java b/v7/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
index 25aa00d..60eb6d2 100644
--- a/v7/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
+++ b/v7/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
@@ -1180,7 +1180,16 @@
             adapter.addAndNotify(5 + (i % 3) * 3, 1);
             Thread.sleep(25);
         }
-        smoothScrollToPosition(mLayoutManager.findLastVisibleItemPosition() + 20);
+
+        final AtomicInteger lastVisiblePosition = new AtomicInteger();
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                lastVisiblePosition.set(mLayoutManager.findLastVisibleItemPosition());
+            }
+        });
+
+        smoothScrollToPosition(lastVisiblePosition.get() + 20);
         waitForAnimations(2);
         getInstrumentation().waitForIdleSync();
         assertEquals("Children count should add up", childCount.get(),
diff --git a/webkit/build.gradle b/webkit/build.gradle
index d610dad..baed9cb 100644
--- a/webkit/build.gradle
+++ b/webkit/build.gradle
@@ -40,7 +40,7 @@
     }
 
     buildTypes.all {
-        consumerProguardFiles new File(webviewBoundaryInterfacesDir, "proguard.flags")
+        consumerProguardFiles new File(webviewBoundaryInterfacesDir, "proguard.flags") , 'proguard-rules.pro'
     }
 }
 
diff --git a/webkit/proguard-rules.pro b/webkit/proguard-rules.pro
new file mode 100644
index 0000000..86756ab
--- /dev/null
+++ b/webkit/proguard-rules.pro
@@ -0,0 +1,16 @@
+# 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.
+
+# Prevent WebViewClientCompat from being renamed, since chromium depends on this name.
+-keep public class androidx.webkit.WebViewClientCompat { public *; }
diff --git a/webkit/src/main/java/androidx/webkit/ServiceWorkerControllerCompat.java b/webkit/src/main/java/androidx/webkit/ServiceWorkerControllerCompat.java
index 8d9d683..79b714a 100644
--- a/webkit/src/main/java/androidx/webkit/ServiceWorkerControllerCompat.java
+++ b/webkit/src/main/java/androidx/webkit/ServiceWorkerControllerCompat.java
@@ -16,18 +16,11 @@
 
 package androidx.webkit;
 
-import android.os.Build;
-import android.webkit.ServiceWorkerController;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
 import androidx.annotation.RequiresFeature;
 import androidx.annotation.RestrictTo;
-import androidx.webkit.internal.FrameworkServiceWorkerController;
-import androidx.webkit.internal.ServiceWorkerControllerAdapter;
-import androidx.webkit.internal.WebViewFeatureInternal;
-import androidx.webkit.internal.WebViewGlueCommunicator;
+import androidx.webkit.internal.ServiceWorkerControllerImpl;
 
 /**
  * Manages Service Workers used by WebView.
@@ -68,34 +61,7 @@
     }
 
     private static class LAZY_HOLDER {
-        static final ServiceWorkerControllerCompat INSTANCE = createController();
-
-        @SuppressWarnings("NewApi")
-        private static ServiceWorkerControllerCompat createController() {
-            WebViewFeatureInternal webviewFeature =
-                    WebViewFeatureInternal.getFeature(WebViewFeature.SERVICE_WORKER_BASIC_USAGE);
-            if (webviewFeature.isSupportedByFramework()) {
-                return getFrameworkControllerCompat();
-            } else if (webviewFeature.isSupportedByWebView()) {
-                return getSupportLibraryControllerCompat();
-            } else {
-                throw WebViewFeatureInternal.getUnsupportedOperationException();
-            }
-        }
-    }
-
-    /**
-     * Return a version of {@link ServiceWorkerControllerCompat} that only uses framework APIs.
-     */
-    @RequiresApi(Build.VERSION_CODES.N)
-    private static ServiceWorkerControllerCompat getFrameworkControllerCompat() {
-        return new FrameworkServiceWorkerController(
-                ServiceWorkerController.getInstance());
-    }
-
-    private static ServiceWorkerControllerCompat getSupportLibraryControllerCompat() {
-        return new ServiceWorkerControllerAdapter(
-                WebViewGlueCommunicator.getFactory().getServiceWorkerController());
+        static final ServiceWorkerControllerCompat INSTANCE = new ServiceWorkerControllerImpl();
     }
 
     /**
diff --git a/webkit/src/main/java/androidx/webkit/internal/FrameworkServiceWorkerController.java b/webkit/src/main/java/androidx/webkit/internal/FrameworkServiceWorkerController.java
deleted file mode 100644
index 2e02777..0000000
--- a/webkit/src/main/java/androidx/webkit/internal/FrameworkServiceWorkerController.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.os.Build;
-import android.webkit.ServiceWorkerController;
-
-import androidx.annotation.RequiresApi;
-import androidx.webkit.ServiceWorkerClientCompat;
-import androidx.webkit.ServiceWorkerControllerCompat;
-import androidx.webkit.ServiceWorkerWebSettingsCompat;
-
-/**
- * Implementation of {@link ServiceWorkerControllerCompat} meant for use on up-to-date platforms.
- * This class does not use reflection to bypass framework APIs - instead it uses android.webkit
- * APIs.
- */
-@RequiresApi(Build.VERSION_CODES.N)
-public class FrameworkServiceWorkerController extends ServiceWorkerControllerCompat {
-    private final ServiceWorkerController mImpl;
-    private ServiceWorkerWebSettingsCompat mSettings;
-
-    public FrameworkServiceWorkerController(ServiceWorkerController impl) {
-        mImpl = impl;
-    }
-
-    @Override
-    public ServiceWorkerWebSettingsCompat getServiceWorkerWebSettings() {
-        if (mSettings == null) {
-            mSettings = new FrameworksServiceWorkerWebSettings(mImpl.getServiceWorkerWebSettings());
-        }
-        return mSettings;
-    }
-
-    @Override
-    public void setServiceWorkerClient(ServiceWorkerClientCompat client) {
-        mImpl.setServiceWorkerClient(new FrameworkServiceWorkerClient(client));
-    }
-}
diff --git a/webkit/src/main/java/androidx/webkit/internal/FrameworksServiceWorkerWebSettings.java b/webkit/src/main/java/androidx/webkit/internal/FrameworksServiceWorkerWebSettings.java
deleted file mode 100644
index 4373756..0000000
--- a/webkit/src/main/java/androidx/webkit/internal/FrameworksServiceWorkerWebSettings.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * 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.os.Build;
-import android.webkit.ServiceWorkerWebSettings;
-
-import androidx.annotation.RequiresApi;
-import androidx.webkit.ServiceWorkerWebSettingsCompat;
-
-/**
- * Implementation of {@link ServiceWorkerWebSettingsCompat} meant for use on up-to-date platforms.
- * This class does not use reflection to bypass framework APIs - instead it uses android.webkit
- * APIs.
- */
-@RequiresApi(Build.VERSION_CODES.N)
-public class FrameworksServiceWorkerWebSettings extends ServiceWorkerWebSettingsCompat {
-    private final ServiceWorkerWebSettings mImpl;
-
-    public FrameworksServiceWorkerWebSettings(ServiceWorkerWebSettings impl) {
-        mImpl = impl;
-    }
-
-    @Override
-    public void setCacheMode(int mode) {
-        mImpl.setCacheMode(mode);
-    }
-
-    @Override
-    public int getCacheMode() {
-        return mImpl.getCacheMode();
-    }
-
-    @Override
-    public void setAllowContentAccess(boolean allow) {
-        mImpl.setAllowContentAccess(allow);
-    }
-
-    @Override
-    public boolean getAllowContentAccess() {
-        return mImpl.getAllowContentAccess();
-    }
-
-    @Override
-    public void setAllowFileAccess(boolean allow) {
-        mImpl.setAllowContentAccess(allow);
-    }
-
-    @Override
-    public boolean getAllowFileAccess() {
-        return mImpl.getAllowFileAccess();
-    }
-
-    @Override
-    public void setBlockNetworkLoads(boolean flag) {
-        mImpl.setAllowContentAccess(flag);
-    }
-
-    @Override
-    public boolean getBlockNetworkLoads() {
-        return mImpl.getBlockNetworkLoads();
-    }
-}
diff --git a/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerControllerAdapter.java b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerControllerAdapter.java
deleted file mode 100644
index 4baa3ea..0000000
--- a/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerControllerAdapter.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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 androidx.webkit.ServiceWorkerClientCompat;
-import androidx.webkit.ServiceWorkerControllerCompat;
-import androidx.webkit.ServiceWorkerWebSettingsCompat;
-
-import org.chromium.support_lib_boundary.ServiceWorkerControllerBoundaryInterface;
-import org.chromium.support_lib_boundary.ServiceWorkerWebSettingsBoundaryInterface;
-import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
-
-/**
- * Adapter between {@link ServiceWorkerControllerCompat} and
- * {@link ServiceWorkerControllerBoundaryInterface} (the corresponding interface shared with the
- * support library glue in the WebView APK).
- */
-public class ServiceWorkerControllerAdapter extends ServiceWorkerControllerCompat {
-    private final ServiceWorkerControllerBoundaryInterface mImpl;
-    private final ServiceWorkerWebSettingsCompat mWebSettings;
-
-    public ServiceWorkerControllerAdapter(ServiceWorkerControllerBoundaryInterface impl) {
-        mImpl = impl;
-        mWebSettings = new ServiceWorkerWebSettingsAdapter(
-                BoundaryInterfaceReflectionUtil.castToSuppLibClass(
-                        ServiceWorkerWebSettingsBoundaryInterface.class,
-                        mImpl.getServiceWorkerWebSettings()));
-    }
-
-    @Override
-    public ServiceWorkerWebSettingsCompat getServiceWorkerWebSettings() {
-        return mWebSettings;
-    }
-
-    @Override
-    public void setServiceWorkerClient(ServiceWorkerClientCompat client) {
-        mImpl.setServiceWorkerClient(BoundaryInterfaceReflectionUtil.createInvocationHandlerFor(
-                new ServiceWorkerClientAdapter(client)));
-    }
-}
diff --git a/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerControllerImpl.java b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerControllerImpl.java
new file mode 100644
index 0000000..17e0a7d
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerControllerImpl.java
@@ -0,0 +1,90 @@
+/*
+ * 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.annotation.SuppressLint;
+import android.webkit.ServiceWorkerController;
+
+import androidx.webkit.ServiceWorkerClientCompat;
+import androidx.webkit.ServiceWorkerControllerCompat;
+import androidx.webkit.ServiceWorkerWebSettingsCompat;
+
+import org.chromium.support_lib_boundary.ServiceWorkerControllerBoundaryInterface;
+import org.chromium.support_lib_boundary.ServiceWorkerWebSettingsBoundaryInterface;
+import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
+
+/**
+ * Implementation of {@link ServiceWorkerControllerCompat}.
+ * This class uses either the framework, the WebView APK, or both, to implement
+ * {@link ServiceWorkerControllerCompat} functionality.
+ */
+public class ServiceWorkerControllerImpl extends ServiceWorkerControllerCompat {
+    private final ServiceWorkerController mFrameworksImpl;
+    private ServiceWorkerControllerBoundaryInterface mBoundaryInterface;
+    private final ServiceWorkerWebSettingsCompat mWebSettings;
+
+    @SuppressLint("NewApi")
+    public ServiceWorkerControllerImpl() {
+        final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_BASIC_USAGE;
+        if (feature.isSupportedByFramework()) {
+            mFrameworksImpl = ServiceWorkerController.getInstance();
+            // The current WebView APK might not be compatible with the support library, so set the
+            // boundary interface to null for now.
+            mBoundaryInterface = null;
+            mWebSettings = new ServiceWorkerWebSettingsImpl(
+                    mFrameworksImpl.getServiceWorkerWebSettings(), null);
+        } else if (feature.isSupportedByWebView()) {
+            mFrameworksImpl = null;
+            mBoundaryInterface = WebViewGlueCommunicator.getFactory().getServiceWorkerController();
+            mWebSettings = new ServiceWorkerWebSettingsImpl(null,
+                    BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+                            ServiceWorkerWebSettingsBoundaryInterface.class,
+                            mBoundaryInterface.getServiceWorkerWebSettings()));
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    private ServiceWorkerControllerBoundaryInterface getBoundaryInterface() {
+        if (mBoundaryInterface != null) return mBoundaryInterface;
+
+        // If the boundary interface is null we must have a working frameworks implementation to
+        // convert into a boundary interface.
+        mBoundaryInterface = WebViewGlueCommunicator.getFactory().getServiceWorkerController();
+        return mBoundaryInterface;
+    }
+
+    @Override
+    public ServiceWorkerWebSettingsCompat getServiceWorkerWebSettings() {
+        return mWebSettings;
+    }
+
+    @SuppressLint("NewApi")
+    @Override
+    public void setServiceWorkerClient(ServiceWorkerClientCompat client)  {
+        final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_BASIC_USAGE;
+        if (feature.isSupportedByFramework()) {
+            mFrameworksImpl.setServiceWorkerClient(new FrameworkServiceWorkerClient(client));
+        } else if (feature.isSupportedByWebView()) {
+            getBoundaryInterface().setServiceWorkerClient(
+                    BoundaryInterfaceReflectionUtil.createInvocationHandlerFor(
+                            new ServiceWorkerClientAdapter(client)));
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerWebSettingsAdapter.java b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerWebSettingsAdapter.java
deleted file mode 100644
index fd49396..0000000
--- a/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerWebSettingsAdapter.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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 androidx.webkit.ServiceWorkerWebSettingsCompat;
-
-import org.chromium.support_lib_boundary.ServiceWorkerWebSettingsBoundaryInterface;
-
-/**
- * Adapter between {@link ServiceWorkerWebSettingsCompat} and
- * {@link ServiceWorkerWebSettingsBoundaryInterface} (the corresponding interface shared with the
- * support library glue in the WebView APK).
- */
-public class ServiceWorkerWebSettingsAdapter extends ServiceWorkerWebSettingsCompat {
-    private final ServiceWorkerWebSettingsBoundaryInterface mImpl;
-
-    public ServiceWorkerWebSettingsAdapter(ServiceWorkerWebSettingsBoundaryInterface impl) {
-        mImpl = impl;
-    }
-
-    @Override
-    public void setCacheMode(int mode) {
-        mImpl.setCacheMode(mode);
-    }
-
-    @Override
-    public int getCacheMode() {
-        return mImpl.getCacheMode();
-    }
-
-    @Override
-    public void setAllowContentAccess(boolean allow) {
-        mImpl.setAllowContentAccess(allow);
-    }
-
-    @Override
-    public boolean getAllowContentAccess() {
-        return mImpl.getAllowContentAccess();
-    }
-
-    @Override
-    public void setAllowFileAccess(boolean allow) {
-        mImpl.setAllowFileAccess(allow);
-    }
-
-    @Override
-    public boolean getAllowFileAccess() {
-        return mImpl.getAllowFileAccess();
-    }
-
-    @Override
-    public void setBlockNetworkLoads(boolean flag) {
-        mImpl.setBlockNetworkLoads(flag);
-    }
-
-    @Override
-    public boolean getBlockNetworkLoads() {
-        return mImpl.getBlockNetworkLoads();
-    }
-}
diff --git a/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerWebSettingsImpl.java b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerWebSettingsImpl.java
new file mode 100644
index 0000000..9ac4450
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerWebSettingsImpl.java
@@ -0,0 +1,172 @@
+/*
+ * 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.annotation.SuppressLint;
+import android.webkit.ServiceWorkerWebSettings;
+
+import androidx.webkit.ServiceWorkerWebSettingsCompat;
+
+import org.chromium.support_lib_boundary.ServiceWorkerWebSettingsBoundaryInterface;
+
+/**
+ * Implementation of {@link ServiceWorkerWebSettingsCompat}.
+ * This class uses either the framework, the WebView APK, or both, to implement
+ * {@link ServiceWorkerWebSettingsCompat} functionality.
+ */
+public class ServiceWorkerWebSettingsImpl extends ServiceWorkerWebSettingsCompat {
+    private final ServiceWorkerWebSettings mFrameworksImpl;
+    private ServiceWorkerWebSettingsBoundaryInterface mBoundaryInterface;
+
+    /**
+     * This class handles three different scenarios:
+     * 1. The Android version on the device is high enough to support all APIs used.
+     * 2. The Android version on the device is too low to support any ServiceWorkerWebSettings APIs
+     * so we use the support library glue instead through
+     * {@link ServiceWorkerWebSettingsBoundaryInterface}.
+     * 3. The Android version on the device is high enough to support some ServiceWorkerWebSettings
+     * APIs, so we call into them using {@link android.webkit.ServiceWorkerWebSettings}, but the
+     * rest of the APIs are only supported by the support library glue, so whenever we call such an
+     * API we fetch a {@link ServiceWorkerWebSettingsBoundaryInterface} corresponding to our
+     * {@link android.webkit.ServiceWorkerWebSettings}.
+     */
+    public ServiceWorkerWebSettingsImpl(ServiceWorkerWebSettings frameworksImpl,
+            ServiceWorkerWebSettingsBoundaryInterface boundaryInterface) {
+        if (frameworksImpl == null && boundaryInterface == null) {
+            throw new IllegalArgumentException(
+                    "Both of the possible implementations cannot be null!");
+        }
+        mFrameworksImpl = frameworksImpl;
+        mBoundaryInterface = boundaryInterface;
+    }
+
+    private ServiceWorkerWebSettingsBoundaryInterface getBoundaryInterface() {
+        if (mBoundaryInterface != null) return mBoundaryInterface;
+        // If the boundary interface is null we must have a working frameworks implementation to
+        // convert into a boundary interface.
+        mBoundaryInterface =
+                WebViewGlueCommunicator.getCompatConverter().convertServiceWorkerSettings(
+                        mFrameworksImpl);
+        return mBoundaryInterface;
+    }
+
+    @SuppressLint("NewApi")
+    @Override
+    public void setCacheMode(int mode) {
+        final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_CACHE_MODE;
+        if (feature.isSupportedByFramework()) {
+            mFrameworksImpl.setCacheMode(mode);
+        } else if (feature.isSupportedByWebView()) {
+            getBoundaryInterface().setCacheMode(mode);
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    @SuppressLint("NewApi")
+    @Override
+    public int getCacheMode() {
+        final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_CACHE_MODE;
+        if (feature.isSupportedByFramework()) {
+            return mFrameworksImpl.getCacheMode();
+        } else if (feature.isSupportedByWebView()) {
+            return getBoundaryInterface().getCacheMode();
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    @SuppressLint("NewApi")
+    @Override
+    public void setAllowContentAccess(boolean allow) {
+        final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_CONTENT_ACCESS;
+        if (feature.isSupportedByFramework()) {
+            mFrameworksImpl.setAllowContentAccess(allow);
+        } else if (feature.isSupportedByWebView()) {
+            getBoundaryInterface().setAllowContentAccess(allow);
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    @SuppressLint("NewApi")
+    @Override
+    public boolean getAllowContentAccess() {
+        final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_CONTENT_ACCESS;
+        if (feature.isSupportedByFramework()) {
+            return mFrameworksImpl.getAllowContentAccess();
+        } else if (feature.isSupportedByWebView()) {
+            return getBoundaryInterface().getAllowContentAccess();
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    @SuppressLint("NewApi")
+    @Override
+    public void setAllowFileAccess(boolean allow) {
+        final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_FILE_ACCESS;
+        if (feature.isSupportedByFramework()) {
+            mFrameworksImpl.setAllowFileAccess(allow);
+        } else if (feature.isSupportedByWebView()) {
+            getBoundaryInterface().setAllowFileAccess(allow);
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    @SuppressLint("NewApi")
+    @Override
+    public boolean getAllowFileAccess() {
+        final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_FILE_ACCESS;
+        if (feature.isSupportedByFramework()) {
+            return mFrameworksImpl.getAllowFileAccess();
+        } else if (feature.isSupportedByWebView()) {
+            return getBoundaryInterface().getAllowFileAccess();
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    @SuppressLint("NewApi")
+    @Override
+    public void setBlockNetworkLoads(boolean flag) {
+        final WebViewFeatureInternal feature =
+                WebViewFeatureInternal.SERVICE_WORKER_BLOCK_NETWORK_LOADS;
+        if (feature.isSupportedByFramework()) {
+            mFrameworksImpl.setBlockNetworkLoads(flag);
+        } else if (feature.isSupportedByWebView()) {
+            getBoundaryInterface().setBlockNetworkLoads(flag);
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    @SuppressLint("NewApi")
+    @Override
+    public boolean getBlockNetworkLoads() {
+        final WebViewFeatureInternal feature =
+                WebViewFeatureInternal.SERVICE_WORKER_BLOCK_NETWORK_LOADS;
+        if (feature.isSupportedByFramework()) {
+            return mFrameworksImpl.getBlockNetworkLoads();
+        } else if (feature.isSupportedByWebView()) {
+            return getBoundaryInterface().getBlockNetworkLoads();
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java b/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java
index a07cf07..4d264a3 100644
--- a/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java
+++ b/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java
@@ -16,8 +16,10 @@
 
 package androidx.webkit.internal;
 
+import android.webkit.ServiceWorkerWebSettings;
 import android.webkit.WebSettings;
 
+import org.chromium.support_lib_boundary.ServiceWorkerWebSettingsBoundaryInterface;
 import org.chromium.support_lib_boundary.WebSettingsBoundaryInterface;
 import org.chromium.support_lib_boundary.WebkitToCompatConverterBoundaryInterface;
 import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
@@ -42,4 +44,16 @@
         return new WebSettingsAdapter(BoundaryInterfaceReflectionUtil.castToSuppLibClass(
                 WebSettingsBoundaryInterface.class, mImpl.convertSettings(webSettings)));
     }
+
+    /**
+     * Return a {@link ServiceWorkerWebSettingsBoundaryInterface} linked to the given
+     * {@link ServiceWorkerWebSettings }such that calls on either of those objects affect the other
+     * object.
+     */
+    public ServiceWorkerWebSettingsBoundaryInterface convertServiceWorkerSettings(
+            ServiceWorkerWebSettings settings) {
+        return BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+                ServiceWorkerWebSettingsBoundaryInterface.class,
+                mImpl.convertServiceWorkerSettings(settings));
+    }
 }