Merge changes from topic "dp2_ffwd" into pi-preview1-androidx-dev

* changes:
  DO NOT MERGE Partial revert of "Fix breakages in SDK drop"
  Merge remote-tracking branch 'goog/pi-androidx-dev' into merge_for_io
diff --git a/adding-support-library-as-included-build.md b/adding-support-library-as-included-build.md
index 136929f..b77233e 100644
--- a/adding-support-library-as-included-build.md
+++ b/adding-support-library-as-included-build.md
@@ -1,5 +1,8 @@
 # Adding the Support Library Build Within Another Build
 
+Sorry, this doesn't seem to be working at the moment.
+For now, run `./gradlew createArchive` and copy the output to where your project can use it, as described fuller in go/support-dev
+
 Would you like to make a change in Support Library and have it be propagated to
 your downstream Gradle build (generally an app) without having to separately
 build Support Library and then build your application?
diff --git a/app-toolkit/common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java b/app-toolkit/common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java
index bda1259..8f3d7ce 100644
--- a/app-toolkit/common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java
+++ b/app-toolkit/common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java
@@ -240,6 +240,7 @@
             return mNext != null;
         }
 
+        @SuppressWarnings("ReferenceEquality")
         @Override
         public void supportRemove(@NonNull Entry<K, V> entry) {
             if (mExpectedEnd == entry && entry == mNext) {
@@ -256,6 +257,7 @@
             }
         }
 
+        @SuppressWarnings("ReferenceEquality")
         private Entry<K, V> nextNode() {
             if (mNext == mExpectedEnd || mExpectedEnd == null) {
                 return null;
@@ -312,6 +314,7 @@
         private Entry<K, V> mCurrent;
         private boolean mBeforeStart = true;
 
+        @SuppressWarnings("ReferenceEquality")
         @Override
         public void supportRemove(@NonNull Entry<K, V> entry) {
             if (entry == mCurrent) {
@@ -379,6 +382,7 @@
             return mKey + "=" + mValue;
         }
 
+        @SuppressWarnings("ReferenceEquality")
         @Override
         public boolean equals(Object obj) {
             if (obj == this) {
diff --git a/app-toolkit/settings.gradle b/app-toolkit/settings.gradle
index 89f6388..ad2350a 100644
--- a/app-toolkit/settings.gradle
+++ b/app-toolkit/settings.gradle
@@ -60,9 +60,11 @@
 includeProject(":lifecycle:lifecycle-livedata", new File(supportRoot, "lifecycle/livedata"))
 includeProject(":lifecycle:lifecycle-process", new File(supportRoot, "lifecycle/process"))
 includeProject(":lifecycle:lifecycle-reactivestreams", new File(supportRoot, "lifecycle/reactivestreams"))
+includeProject(":lifecycle:lifecycle-reactivestreams-ktx", new File(supportRoot, "lifecycle/reactivestreams/ktx"))
 includeProject(":lifecycle:lifecycle-runtime", new File(supportRoot, "lifecycle/runtime"))
 includeProject(":lifecycle:lifecycle-service", new File(supportRoot, "lifecycle/service"))
 includeProject(":lifecycle:lifecycle-viewmodel", new File(supportRoot, "lifecycle/viewmodel"))
+includeProject(":lifecycle:lifecycle-viewmodel-ktx", new File(supportRoot, "lifecycle/viewmodel/ktx"))
 includeProject(":paging:integration-tests:testapp", new File(supportRoot, "paging/integration-tests/testapp"))
 includeProject(":paging:paging-common", new File(supportRoot, "paging/common"))
 includeProject(":paging:paging-runtime", new File(supportRoot, "paging/runtime"))
@@ -78,6 +80,7 @@
 includeProject(":room:room-testing", new File(supportRoot, "room/testing"))
 includeProject(":sqlite:sqlite", new File(supportRoot, "persistence/db"))
 includeProject(":sqlite:sqlite-framework", new File(supportRoot, "persistence/db-framework"))
+includeProject(":sqlite:sqlite-ktx", new File(supportRoot, "persistence/db/ktx"))
 
 includeProject(":jetifier-core", new File(supportRoot, "jetifier/jetifier/core"))
 includeProject(":jetifier-processor", new File(supportRoot, "jetifier/jetifier/processor"))
diff --git a/buildSrc/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt b/buildSrc/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
index be10aa6..7b0887d 100644
--- a/buildSrc/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
@@ -45,6 +45,8 @@
             "-Xep:JavaLangClash:ERROR",
             "-Xep:PrivateConstructorForUtilityClass:ERROR",
             "-Xep:TypeParameterUnusedInFormals:ERROR",
+            "-Xep:StringSplitter:ERROR",
+            "-Xep:ReferenceEquality:ERROR",
 
             // Nullaway
             "-XepIgnoreUnknownCheckNames", // https://github.com/uber/NullAway/issues/25
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt
index 15c6f42..e85acec 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt
@@ -47,7 +47,6 @@
     const val LOCALBROADCASTMANAGER = "androidx.localbroadcastmanager"
     const val MEDIA = "androidx.media"
     const val MEDIAROUTER = "androidx.mediarouter"
-    const val MEDIAWIDGET = "androidx.mediawidget"
     const val PALETTE = "androidx.palette"
     const val PERCENTLAYOUT = "androidx.percentlayout"
     const val PREFERENCE = "androidx.preference"
diff --git a/buildSrc/src/main/kotlin/androidx/build/SupportJavaLibraryPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/SupportJavaLibraryPlugin.kt
index e8bd816..cee2955 100644
--- a/buildSrc/src/main/kotlin/androidx/build/SupportJavaLibraryPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/SupportJavaLibraryPlugin.kt
@@ -23,6 +23,7 @@
 import org.gradle.api.Plugin
 import org.gradle.api.Project
 import org.gradle.api.plugins.JavaPluginConvention
+import org.gradle.api.tasks.bundling.Jar
 import org.gradle.api.tasks.compile.JavaCompile
 
 /**
@@ -47,6 +48,11 @@
                 convention.targetCompatibility = JavaVersion.VERSION_1_7
             }
             DiffAndDocs.registerJavaProject(project, supportLibraryExtension)
+
+            project.tasks.withType(Jar::class.java) { jarTask ->
+                jarTask.setReproducibleFileOrder(true)
+                jarTask.setPreserveFileTimestamps(false)
+            }
         }
 
         project.apply(mapOf("plugin" to ErrorProneBasePlugin::class.java))
diff --git a/buildSrc/src/main/kotlin/androidx/build/SupportKotlinLibraryPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/SupportKotlinLibraryPlugin.kt
index d2a6ddb..9558f76 100644
--- a/buildSrc/src/main/kotlin/androidx/build/SupportKotlinLibraryPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/SupportKotlinLibraryPlugin.kt
@@ -20,6 +20,7 @@
 import org.gradle.api.Plugin
 import org.gradle.api.Project
 import org.gradle.api.plugins.JavaPluginConvention
+import org.gradle.api.tasks.bundling.Jar
 
 class SupportKotlinLibraryPlugin : Plugin<Project> {
     override fun apply(project: Project) {
@@ -38,6 +39,12 @@
                 convention.sourceCompatibility = JavaVersion.VERSION_1_7
                 convention.targetCompatibility = JavaVersion.VERSION_1_7
             }
+
+            project.tasks.withType(Jar::class.java) { jarTask ->
+                jarTask.setReproducibleFileOrder(true)
+                jarTask.setPreserveFileTimestamps(false)
+            }
+
         }
 
         CheckExternalDependencyLicensesTask.configure(project)
diff --git a/car/api/current.txt b/car/api/current.txt
index d643b33..1396dc5 100644
--- a/car/api/current.txt
+++ b/car/api/current.txt
@@ -378,19 +378,25 @@
   }
 
   public class SeekbarListItem extends androidx.car.widget.ListItem {
-    ctor public SeekbarListItem(android.content.Context, int, int, android.widget.SeekBar.OnSeekBarChangeListener, java.lang.String);
+    ctor public deprecated SeekbarListItem(android.content.Context, int, int, android.widget.SeekBar.OnSeekBarChangeListener, java.lang.String);
+    ctor public SeekbarListItem(android.content.Context);
     method public static androidx.car.widget.SeekbarListItem.ViewHolder createViewHolder(android.view.View);
     method public int getViewType();
     method protected void onBind(androidx.car.widget.SeekbarListItem.ViewHolder);
     method protected void resolveDirtyState();
+    method public void setMax(int);
+    method public void setOnSeekBarChangeListener(android.widget.SeekBar.OnSeekBarChangeListener);
     method public void setPrimaryActionEmptyIcon();
     method public void setPrimaryActionIcon(int);
     method public void setPrimaryActionIcon(android.graphics.drawable.Drawable);
+    method public void setProgress(int);
+    method public void setSecondaryProgress(int);
     method public void setSupplementalEmptyIcon(boolean);
     method public void setSupplementalIcon(int, boolean);
     method public void setSupplementalIcon(int, boolean, android.view.View.OnClickListener);
     method public void setSupplementalIcon(android.graphics.drawable.Drawable, boolean);
     method public void setSupplementalIcon(android.graphics.drawable.Drawable, boolean, android.view.View.OnClickListener);
+    method public void setText(java.lang.String);
   }
 
   public static class SeekbarListItem.ViewHolder extends androidx.car.widget.ListItem.ViewHolder {
diff --git a/car/build.gradle b/car/build.gradle
index 8139c8c..d2e9be6 100644
--- a/car/build.gradle
+++ b/car/build.gradle
@@ -9,7 +9,8 @@
 dependencies {
     api project(':appcompat')
     api project(':cardview')
-    api "com.android.temp.support:design-widget:28.0.0-alpha1", {
+    api "com.android.temp.support:design-widget:28.0.0-alpha1@aar", {
+        transitive = true
         exclude group: 'androidx.annotation'
         exclude group: 'androidx.core'
         exclude group: 'androidx.legacy'
@@ -47,7 +48,7 @@
 
 supportLibrary {
     name = "Android Car Support UI"
-    publish = false
+    publish = true
     mavenVersion = LibraryVersions.SUPPORT_LIBRARY
     mavenGroup = LibraryGroups.CAR
     inceptionYear = "2017"
diff --git a/car/res/drawable/ic_nav_arrow_back.xml b/car/res/drawable/ic_nav_arrow_back.xml
new file mode 100644
index 0000000..574e816
--- /dev/null
+++ b/car/res/drawable/ic_nav_arrow_back.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48">
+
+    <path
+        android:pathData="M0 0h48v48H0z" />
+    <path
+        android:fillColor="#000000"
+        android:pathData="M40 22H15.66l11.17-11.17L24 8 8 24l16 16 2.83-2.83L15.66 26H40v-4z" />
+</vector>
+
diff --git a/car/res/layout/car_list_item_seekbar_content.xml b/car/res/layout/car_list_item_seekbar_content.xml
index cd68f3c..7fae5c1 100644
--- a/car/res/layout/car_list_item_seekbar_content.xml
+++ b/car/res/layout/car_list_item_seekbar_content.xml
@@ -47,6 +47,7 @@
             android:layout_height="wrap_content"
             android:paddingTop="@dimen/car_seekbar_padding"
             android:paddingBottom="@dimen/car_seekbar_padding"
+            android:min="0"
             android:splitTrack="false"/>
     </LinearLayout>
 
diff --git a/car/res/values-af/strings.xml b/car/res/values-af/strings.xml
index f307fc7..1e90b40 100644
--- a/car/res/values-af/strings.xml
+++ b/car/res/values-af/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Konsentreer op die pad"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Vou knoppie in/uit"</string>
 </resources>
diff --git a/car/res/values-am/strings.xml b/car/res/values-am/strings.xml
index 30ae48e..6759ee3 100644
--- a/car/res/values-am/strings.xml
+++ b/car/res/values-am/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"መንገዱ ላይ ያተኩሩ"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"አዝራርን ዘርጋ/ሰብስብ"</string>
 </resources>
diff --git a/car/res/values-ar/strings.xml b/car/res/values-ar/strings.xml
index e970ed9..845908b 100644
--- a/car/res/values-ar/strings.xml
+++ b/car/res/values-ar/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"ركِّز في الطريق"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"زر التوسيع/التصغير"</string>
 </resources>
diff --git a/car/res/values-as/strings.xml b/car/res/values-as/strings.xml
new file mode 100644
index 0000000..e5a0015
--- /dev/null
+++ b/car/res/values-as/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="speed_bump_lockout_message" msgid="5405697774899378511">"ৰাষ্টাত মনোযোগ দিয়ক"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"সম্প্ৰসাৰণ/সংকোচন বুটাম"</string>
+</resources>
diff --git a/car/res/values-az/strings.xml b/car/res/values-az/strings.xml
index e813849..b5e25dc 100644
--- a/car/res/values-az/strings.xml
+++ b/car/res/values-az/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Diqqətinizi yola yönəldin"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Düyməni genişləndirin/yığcamlaşdırın"</string>
 </resources>
diff --git a/car/res/values-b+sr+Latn/strings.xml b/car/res/values-b+sr+Latn/strings.xml
index a83a82c..43dbac2 100644
--- a/car/res/values-b+sr+Latn/strings.xml
+++ b/car/res/values-b+sr+Latn/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Fokusirajte se na put"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Dugme Proširi/skupi"</string>
 </resources>
diff --git a/car/res/values-be/strings.xml b/car/res/values-be/strings.xml
index 80912e4..cfef873 100644
--- a/car/res/values-be/strings.xml
+++ b/car/res/values-be/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Увага на дарогу"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Кнопка \"Разгарнуць/згарнуць\""</string>
 </resources>
diff --git a/car/res/values-bg/strings.xml b/car/res/values-bg/strings.xml
index dd5811f..49e7a61 100644
--- a/car/res/values-bg/strings.xml
+++ b/car/res/values-bg/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Съсредоточете се върху пътя"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Бутон за разгъване/свиване"</string>
 </resources>
diff --git a/car/res/values-bn/strings.xml b/car/res/values-bn/strings.xml
index fcf0165..4ab2ca4 100644
--- a/car/res/values-bn/strings.xml
+++ b/car/res/values-bn/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"মনোযোগ দিয়ে গাড়ি চালান"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"বোতাম বড় করুন/আড়াল করুন"</string>
 </resources>
diff --git a/car/res/values-bs/strings.xml b/car/res/values-bs/strings.xml
index 99e655e..6ff5714 100644
--- a/car/res/values-bs/strings.xml
+++ b/car/res/values-bs/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Fokusirajte se na cestu"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Dugme proširi/suzi"</string>
 </resources>
diff --git a/car/res/values-ca/strings.xml b/car/res/values-ca/strings.xml
index 758f6e7..8e20fd8 100644
--- a/car/res/values-ca/strings.xml
+++ b/car/res/values-ca/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Concentra\'t en la carretera"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Botó per desplegar o replegar"</string>
 </resources>
diff --git a/car/res/values-cs/strings.xml b/car/res/values-cs/strings.xml
index 27090ef..1b4cad2 100644
--- a/car/res/values-cs/strings.xml
+++ b/car/res/values-cs/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Soustřeďte se na silnici"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Tlačítko rozbalení/sbalení"</string>
 </resources>
diff --git a/car/res/values-da/strings.xml b/car/res/values-da/strings.xml
index 96cc062..4a0e180 100644
--- a/car/res/values-da/strings.xml
+++ b/car/res/values-da/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Hold øjnene på vejen"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Knappen Udvid/skjul"</string>
 </resources>
diff --git a/car/res/values-de/strings.xml b/car/res/values-de/strings.xml
index ee289ac..f670bd6 100644
--- a/car/res/values-de/strings.xml
+++ b/car/res/values-de/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Achte auf den Verkehr"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Schaltfläche zum Maximieren/Minimieren"</string>
 </resources>
diff --git a/car/res/values-el/strings.xml b/car/res/values-el/strings.xml
index cf8ab8f..4010875 100644
--- a/car/res/values-el/strings.xml
+++ b/car/res/values-el/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Επικεντρωθείτε στον δρόμο"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Κουμπί ανάπτυξης/σύμπτυξης"</string>
 </resources>
diff --git a/car/res/values-en-rAU/strings.xml b/car/res/values-en-rAU/strings.xml
index 8cc3360..a307f7e 100644
--- a/car/res/values-en-rAU/strings.xml
+++ b/car/res/values-en-rAU/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Focus on the road"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Expand/collapse button"</string>
 </resources>
diff --git a/car/res/values-en-rCA/strings.xml b/car/res/values-en-rCA/strings.xml
index 8cc3360..a307f7e 100644
--- a/car/res/values-en-rCA/strings.xml
+++ b/car/res/values-en-rCA/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Focus on the road"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Expand/collapse button"</string>
 </resources>
diff --git a/car/res/values-en-rGB/strings.xml b/car/res/values-en-rGB/strings.xml
index 8cc3360..a307f7e 100644
--- a/car/res/values-en-rGB/strings.xml
+++ b/car/res/values-en-rGB/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Focus on the road"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Expand/collapse button"</string>
 </resources>
diff --git a/car/res/values-en-rIN/strings.xml b/car/res/values-en-rIN/strings.xml
index 8cc3360..a307f7e 100644
--- a/car/res/values-en-rIN/strings.xml
+++ b/car/res/values-en-rIN/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Focus on the road"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Expand/collapse button"</string>
 </resources>
diff --git a/car/res/values-en-rXC/strings.xml b/car/res/values-en-rXC/strings.xml
index c11c1cb..16d0daf 100644
--- a/car/res/values-en-rXC/strings.xml
+++ b/car/res/values-en-rXC/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‏‏‎‎‎‎‎‏‎‎‏‏‏‎‎‏‎‏‎‏‏‏‏‏‎‎‎‏‎‏‎‎‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‎‎‏‎‏‎‎‏‏‏‏‎Focus on the road‎‏‎‎‏‎"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‎‏‎‏‎‏‏‏‎‏‏‏‎‎‏‎‎‎‎‏‎‎‏‎‎‏‎‏‎‏‏‎‎‎‏‎‎‏‎‎‏‏‏‏‏‏‏‎‏‎‏‏‏‎‎‏‏‎‎‎Expand/collapse button‎‏‎‎‏‎"</string>
 </resources>
diff --git a/car/res/values-es-rUS/strings.xml b/car/res/values-es-rUS/strings.xml
index 6ac20d7..46a5e11 100644
--- a/car/res/values-es-rUS/strings.xml
+++ b/car/res/values-es-rUS/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Concéntrate en el camino"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Botón Expandir/contraer"</string>
 </resources>
diff --git a/car/res/values-es/strings.xml b/car/res/values-es/strings.xml
index 09a493f..33ed10b 100644
--- a/car/res/values-es/strings.xml
+++ b/car/res/values-es/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Céntrate en la carretera"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Botón para mostrar u ocultar"</string>
 </resources>
diff --git a/car/res/values-et/strings.xml b/car/res/values-et/strings.xml
index 66f6e48..7561817 100644
--- a/car/res/values-et/strings.xml
+++ b/car/res/values-et/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Keskenduge teele"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Nupp Laienda/Ahenda"</string>
 </resources>
diff --git a/car/res/values-eu/strings.xml b/car/res/values-eu/strings.xml
index 3773f71..0e92575 100644
--- a/car/res/values-eu/strings.xml
+++ b/car/res/values-eu/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Jarri arreta errepidean"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Zabaltzeko/Tolesteko botoia"</string>
 </resources>
diff --git a/car/res/values-fa/strings.xml b/car/res/values-fa/strings.xml
index 8668d6b..59152f0 100644
--- a/car/res/values-fa/strings.xml
+++ b/car/res/values-fa/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"روی جاده تمرکز داشته باشید"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"دکمه بزرگ کردن/کوچک کردن"</string>
 </resources>
diff --git a/car/res/values-fi/strings.xml b/car/res/values-fi/strings.xml
index e93cb9c..5bbb440 100644
--- a/car/res/values-fi/strings.xml
+++ b/car/res/values-fi/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Pidä katse tiessä"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Laajennus- ja tiivistyspainike"</string>
 </resources>
diff --git a/car/res/values-fr-rCA/strings.xml b/car/res/values-fr-rCA/strings.xml
index f32315b..e90ddf2 100644
--- a/car/res/values-fr-rCA/strings.xml
+++ b/car/res/values-fr-rCA/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Concentrez-vous sur la route"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Bouton Développer/Réduire"</string>
 </resources>
diff --git a/car/res/values-fr/strings.xml b/car/res/values-fr/strings.xml
index f32315b..e90ddf2 100644
--- a/car/res/values-fr/strings.xml
+++ b/car/res/values-fr/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Concentrez-vous sur la route"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Bouton Développer/Réduire"</string>
 </resources>
diff --git a/car/res/values-gl/strings.xml b/car/res/values-gl/strings.xml
index 42bc515..c89cc35 100644
--- a/car/res/values-gl/strings.xml
+++ b/car/res/values-gl/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Céntrate na estrada"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Botón despregar/contraer"</string>
 </resources>
diff --git a/car/res/values-gu/strings.xml b/car/res/values-gu/strings.xml
index f215ec2..4397f5d 100644
--- a/car/res/values-gu/strings.xml
+++ b/car/res/values-gu/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"રસ્તા પર ફોકસ કરો"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"વિસ્તાર કરો/સંકુચિત કરો બટન"</string>
 </resources>
diff --git a/car/res/values-hi/strings.xml b/car/res/values-hi/strings.xml
index 53f8a8d..08d973b 100644
--- a/car/res/values-hi/strings.xml
+++ b/car/res/values-hi/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"सड़क पर ध्यान दें"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"बड़ा/छोटा करने वाला बटन"</string>
 </resources>
diff --git a/car/res/values-hr/strings.xml b/car/res/values-hr/strings.xml
index 0a08dcc..5714327 100644
--- a/car/res/values-hr/strings.xml
+++ b/car/res/values-hr/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Usredotočite se na cestu"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Gumb za proširivanje/sažimanje"</string>
 </resources>
diff --git a/car/res/values-hu/strings.xml b/car/res/values-hu/strings.xml
index 3618719..88c2577 100644
--- a/car/res/values-hu/strings.xml
+++ b/car/res/values-hu/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Figyeljen az útra"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Gomb kibontása/összecsukása"</string>
 </resources>
diff --git a/car/res/values-hy/strings.xml b/car/res/values-hy/strings.xml
index 3a85b41..d97cd0a 100644
--- a/car/res/values-hy/strings.xml
+++ b/car/res/values-hy/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Հետևեք ճանապարհին"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"«Ծավալել/ծալել» կոճակ"</string>
 </resources>
diff --git a/car/res/values-in/strings.xml b/car/res/values-in/strings.xml
index 81daf29..faf2f43 100644
--- a/car/res/values-in/strings.xml
+++ b/car/res/values-in/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Konsentrasi saat mengemudi"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Tombol luaskan/ciutkan"</string>
 </resources>
diff --git a/car/res/values-is/strings.xml b/car/res/values-is/strings.xml
index 4b5d26d..7c2dd76 100644
--- a/car/res/values-is/strings.xml
+++ b/car/res/values-is/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Einbeittu þér að akstrinum"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Hnappur til að stækka/minnka"</string>
 </resources>
diff --git a/car/res/values-it/strings.xml b/car/res/values-it/strings.xml
index b395409..d2b3f4f 100644
--- a/car/res/values-it/strings.xml
+++ b/car/res/values-it/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Concentrati sulla strada"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Pulsante Espandi/Comprimi"</string>
 </resources>
diff --git a/car/res/values-iw/strings.xml b/car/res/values-iw/strings.xml
index ab92be5..79bad9c 100644
--- a/car/res/values-iw/strings.xml
+++ b/car/res/values-iw/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"עליך להתמקד בכביש"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"לחצן הרחבה וכיווץ"</string>
 </resources>
diff --git a/car/res/values-ja/strings.xml b/car/res/values-ja/strings.xml
index 89fce3b..87deba0 100644
--- a/car/res/values-ja/strings.xml
+++ b/car/res/values-ja/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"運転に集中してください"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"展開 / 折りたたみボタン"</string>
 </resources>
diff --git a/car/res/values-ka/strings.xml b/car/res/values-ka/strings.xml
index e3f2e07..b525a9b 100644
--- a/car/res/values-ka/strings.xml
+++ b/car/res/values-ka/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"კონცენტრირდით გზაზე"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"ღილაკის გაფართოება/ჩაკეცვა"</string>
 </resources>
diff --git a/car/res/values-kk/strings.xml b/car/res/values-kk/strings.xml
index bbccd56..ff327d3 100644
--- a/car/res/values-kk/strings.xml
+++ b/car/res/values-kk/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Жолға назар аударыңыз"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"\"Жаю/Жию\" түймесі"</string>
 </resources>
diff --git a/car/res/values-km/strings.xml b/car/res/values-km/strings.xml
index 50fe2db..f9d9111 100644
--- a/car/res/values-km/strings.xml
+++ b/car/res/values-km/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"ផ្តោតលើ​ការបើកបរ"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"ប៊ូតុង​ពង្រីក/បង្រួម"</string>
 </resources>
diff --git a/car/res/values-kn/strings.xml b/car/res/values-kn/strings.xml
index 6562ee2..50ba985 100644
--- a/car/res/values-kn/strings.xml
+++ b/car/res/values-kn/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"ರಸ್ತೆಯ ಮೇಲೆ ಗಮನಹರಿಸಿ"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"ವಿಸ್ತರಿಸಿ/ಕುಗ್ಗಿಸಿ ಬಟನ್"</string>
 </resources>
diff --git a/car/res/values-ko/strings.xml b/car/res/values-ko/strings.xml
index ac5865a..0081b5c 100644
--- a/car/res/values-ko/strings.xml
+++ b/car/res/values-ko/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"도로 상황에 집중하세요."</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"펼치기/접기 버튼"</string>
 </resources>
diff --git a/car/res/values-ky/strings.xml b/car/res/values-ky/strings.xml
index 3640239..8a752fa 100644
--- a/car/res/values-ky/strings.xml
+++ b/car/res/values-ky/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Жолго көңүл буруңуз"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Жайып көрсөтүү/жыйыштыруу баскычы"</string>
 </resources>
diff --git a/car/res/values-lo/strings.xml b/car/res/values-lo/strings.xml
index 4af3152..02afe60 100644
--- a/car/res/values-lo/strings.xml
+++ b/car/res/values-lo/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"ຕັ້ງໃຈຂັບລົດ"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"ປຸ່ມຫຍໍ້/ຂະຫຍາຍ"</string>
 </resources>
diff --git a/car/res/values-lt/strings.xml b/car/res/values-lt/strings.xml
index 685bbe5..5281e18 100644
--- a/car/res/values-lt/strings.xml
+++ b/car/res/values-lt/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Sutelkite dėmesį į kelią"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Mygtukas „Išskleisti / sutraukti“"</string>
 </resources>
diff --git a/car/res/values-lv/strings.xml b/car/res/values-lv/strings.xml
index 417d331..7b4372b 100644
--- a/car/res/values-lv/strings.xml
+++ b/car/res/values-lv/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Pievērsieties autovadīšanai"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Izvēršanas/sakļaušanas poga"</string>
 </resources>
diff --git a/car/res/values-mk/strings.xml b/car/res/values-mk/strings.xml
index 7377299..5184503 100644
--- a/car/res/values-mk/strings.xml
+++ b/car/res/values-mk/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Фокусирајте се на патот"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Копче за проширување/собирање"</string>
 </resources>
diff --git a/car/res/values-ml/strings.xml b/car/res/values-ml/strings.xml
index d5ad91d..8047b4c 100644
--- a/car/res/values-ml/strings.xml
+++ b/car/res/values-ml/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"റോഡിൽ ശ്രദ്ധിക്കുക"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"വികസിപ്പിക്കുക/ചുരുക്കുക ബട്ടൺ"</string>
 </resources>
diff --git a/car/res/values-mn/strings.xml b/car/res/values-mn/strings.xml
index 4b249a4..a8ef13c 100644
--- a/car/res/values-mn/strings.xml
+++ b/car/res/values-mn/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Зам дээр төвлөрөх"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Дэлгэх/буулгах товчлуур"</string>
 </resources>
diff --git a/car/res/values-mr/strings.xml b/car/res/values-mr/strings.xml
index c79f3f3..3032c97 100644
--- a/car/res/values-mr/strings.xml
+++ b/car/res/values-mr/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"रस्त्यावर फोकस करा"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"बटण विस्‍तृत करा/कोलॅप्‍स करा"</string>
 </resources>
diff --git a/car/res/values-ms/strings.xml b/car/res/values-ms/strings.xml
index d209113..301f7eb 100644
--- a/car/res/values-ms/strings.xml
+++ b/car/res/values-ms/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Beri tumpuan pada jalan raya"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Butang kembangkan/runtuhkan"</string>
 </resources>
diff --git a/car/res/values-my/strings.xml b/car/res/values-my/strings.xml
index 438729a..f5317f7 100644
--- a/car/res/values-my/strings.xml
+++ b/car/res/values-my/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"လမ်းကို အာရုံစိုက်ရန်"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"ချဲ့ရန်/ခေါက်သိမ်းရန် ခလုတ်"</string>
 </resources>
diff --git a/car/res/values-nb/strings.xml b/car/res/values-nb/strings.xml
index eb3a144..e4c4810 100644
--- a/car/res/values-nb/strings.xml
+++ b/car/res/values-nb/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Fokuser på veien"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Vis/skjul-knapp"</string>
 </resources>
diff --git a/car/res/values-ne/strings.xml b/car/res/values-ne/strings.xml
index d066c0b..c4499b8 100644
--- a/car/res/values-ne/strings.xml
+++ b/car/res/values-ne/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"सडकमा ध्यान केन्द्रित गर्नु…"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"विस्तृत/संक्षिप्त गर्ने बटन"</string>
 </resources>
diff --git a/car/res/values-nl/strings.xml b/car/res/values-nl/strings.xml
index 7fcb11e..de08f63 100644
--- a/car/res/values-nl/strings.xml
+++ b/car/res/values-nl/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Houd je aandacht op de weg"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Knop voor uitvouwen/samenvouwen"</string>
 </resources>
diff --git a/car/res/values-or/strings.xml b/car/res/values-or/strings.xml
new file mode 100644
index 0000000..3a003b8
--- /dev/null
+++ b/car/res/values-or/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="speed_bump_lockout_message" msgid="5405697774899378511">"ରାସ୍ତା ଉପରେ ଧ୍ୟାନରଖନ୍ତୁ"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"ବିସ୍ତାର/ସଂକୋଚନ ବଟନ୍"</string>
+</resources>
diff --git a/car/res/values-pa/strings.xml b/car/res/values-pa/strings.xml
index 137bd2a..63a19f3 100644
--- a/car/res/values-pa/strings.xml
+++ b/car/res/values-pa/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"ਸੜਕ \'ਤੇ ਧਿਆਨ ਦਿਓ"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"ਵਿਸਤਾਰ ਕਰੋ/ਸਮੇਟੋ ਬਟਨ"</string>
 </resources>
diff --git a/car/res/values-pl/strings.xml b/car/res/values-pl/strings.xml
index c5aa323..133d27e 100644
--- a/car/res/values-pl/strings.xml
+++ b/car/res/values-pl/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Skup się na drodze"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Przycisk zwijania/rozwijania"</string>
 </resources>
diff --git a/car/res/values-pt-rBR/strings.xml b/car/res/values-pt-rBR/strings.xml
index a6c515a..6c6e459 100644
--- a/car/res/values-pt-rBR/strings.xml
+++ b/car/res/values-pt-rBR/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Foco na estrada"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Botão \"Expandir/Recolher\""</string>
 </resources>
diff --git a/car/res/values-pt-rPT/strings.xml b/car/res/values-pt-rPT/strings.xml
index 2338efe..911b120 100644
--- a/car/res/values-pt-rPT/strings.xml
+++ b/car/res/values-pt-rPT/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Concentre-se na estrada."</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Botão Expandir/reduzir"</string>
 </resources>
diff --git a/car/res/values-pt/strings.xml b/car/res/values-pt/strings.xml
index a6c515a..6c6e459 100644
--- a/car/res/values-pt/strings.xml
+++ b/car/res/values-pt/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Foco na estrada"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Botão \"Expandir/Recolher\""</string>
 </resources>
diff --git a/car/res/values-ro/strings.xml b/car/res/values-ro/strings.xml
index 20cc3e7..7fe204a 100644
--- a/car/res/values-ro/strings.xml
+++ b/car/res/values-ro/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Concentrați-vă asupra drumului"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Butonul de extindere/restrângere"</string>
 </resources>
diff --git a/car/res/values-ru/strings.xml b/car/res/values-ru/strings.xml
index 198f7fa..6554dc3 100644
--- a/car/res/values-ru/strings.xml
+++ b/car/res/values-ru/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Следите за дорогой"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Кнопка \"Развернуть/свернуть\""</string>
 </resources>
diff --git a/car/res/values-si/strings.xml b/car/res/values-si/strings.xml
index 530c12a..48977bd 100644
--- a/car/res/values-si/strings.xml
+++ b/car/res/values-si/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"මාර්ගයට අවධානය යොමු කරන්න"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"දිග හැරීමේ/හැකිළීමේ බොත්තම"</string>
 </resources>
diff --git a/car/res/values-sk/strings.xml b/car/res/values-sk/strings.xml
index a959d09..d8175c4 100644
--- a/car/res/values-sk/strings.xml
+++ b/car/res/values-sk/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Sústreďte sa na cestu"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Tlačidlo rozbalenia/zbalenia"</string>
 </resources>
diff --git a/car/res/values-sl/strings.xml b/car/res/values-sl/strings.xml
index c0a8164..85f2602 100644
--- a/car/res/values-sl/strings.xml
+++ b/car/res/values-sl/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Glejte na cesto"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Gumb za razširitev/strnitev"</string>
 </resources>
diff --git a/car/res/values-sq/strings.xml b/car/res/values-sq/strings.xml
index c8c91ef..87cb023 100644
--- a/car/res/values-sq/strings.xml
+++ b/car/res/values-sq/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Përqendrohu te rruga"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Butoni i zgjerimit/palosjes"</string>
 </resources>
diff --git a/car/res/values-sr/strings.xml b/car/res/values-sr/strings.xml
index d7a6b85..d3fe526 100644
--- a/car/res/values-sr/strings.xml
+++ b/car/res/values-sr/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Фокусирајте се на пут"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Дугме Прошири/скупи"</string>
 </resources>
diff --git a/car/res/values-sv/strings.xml b/car/res/values-sv/strings.xml
index 3798509..c4ae438 100644
--- a/car/res/values-sv/strings.xml
+++ b/car/res/values-sv/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Fokusera på körningen"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Knappen Utöka/komprimera"</string>
 </resources>
diff --git a/car/res/values-sw/strings.xml b/car/res/values-sw/strings.xml
index a5b76c7..d3472f5 100644
--- a/car/res/values-sw/strings.xml
+++ b/car/res/values-sw/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Tia makini barabarani"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Kitufe cha kupanua/kukunja"</string>
 </resources>
diff --git a/car/res/values-ta/strings.xml b/car/res/values-ta/strings.xml
index e97f385..e13ede5 100644
--- a/car/res/values-ta/strings.xml
+++ b/car/res/values-ta/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"வாகனம் ஓட்டும்போது கவனம் தேவை"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"விரிவாக்குவதற்கான/சுருக்குவதற்கான பட்டன்"</string>
 </resources>
diff --git a/car/res/values-te/strings.xml b/car/res/values-te/strings.xml
index 9079a09..b106363 100644
--- a/car/res/values-te/strings.xml
+++ b/car/res/values-te/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"రహదారిపై దృష్టి ఉంచండి"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"విస్తరించు/కుదించు బటన్"</string>
 </resources>
diff --git a/car/res/values-th/strings.xml b/car/res/values-th/strings.xml
index 3ba0d6b..05be415 100644
--- a/car/res/values-th/strings.xml
+++ b/car/res/values-th/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"จดจ่อกับถนน"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"ปุ่มขยาย/ยุบ"</string>
 </resources>
diff --git a/car/res/values-tl/strings.xml b/car/res/values-tl/strings.xml
index 395e555..a56c37c 100644
--- a/car/res/values-tl/strings.xml
+++ b/car/res/values-tl/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Tumuon sa kalsada"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Button na i-expand/i-collapse"</string>
 </resources>
diff --git a/car/res/values-tr/strings.xml b/car/res/values-tr/strings.xml
index a0f0b22..b28b66e 100644
--- a/car/res/values-tr/strings.xml
+++ b/car/res/values-tr/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Dikkatinizi yola verin"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Genişlet/daralt düğmesi"</string>
 </resources>
diff --git a/car/res/values-uk/strings.xml b/car/res/values-uk/strings.xml
index 2657348..1964936 100644
--- a/car/res/values-uk/strings.xml
+++ b/car/res/values-uk/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Зосередьтеся на дорозі"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Кнопка \"Розгорнути або згорнути\""</string>
 </resources>
diff --git a/car/res/values-ur/strings.xml b/car/res/values-ur/strings.xml
index 60c578c..c1d6b3e 100644
--- a/car/res/values-ur/strings.xml
+++ b/car/res/values-ur/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"سڑک پر توجہ مرکوز کریں"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"پھیلائیں/سکیڑیں بٹن"</string>
 </resources>
diff --git a/car/res/values-uz/strings.xml b/car/res/values-uz/strings.xml
index bdaba48..a830b84 100644
--- a/car/res/values-uz/strings.xml
+++ b/car/res/values-uz/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Diqqatingizni yo‘lga qarating"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Yoyish/yig‘ish tugmasi"</string>
 </resources>
diff --git a/car/res/values-vi/strings.xml b/car/res/values-vi/strings.xml
index 37457d4..144f41a 100644
--- a/car/res/values-vi/strings.xml
+++ b/car/res/values-vi/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Tập trung vào đường đi"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Nút mở rộng/thu gọn"</string>
 </resources>
diff --git a/car/res/values-zh-rCN/strings.xml b/car/res/values-zh-rCN/strings.xml
index ec0c3c4..0c2caea 100644
--- a/car/res/values-zh-rCN/strings.xml
+++ b/car/res/values-zh-rCN/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"请专心驾驶"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"“展开”/“收起”按钮"</string>
 </resources>
diff --git a/car/res/values-zh-rHK/strings.xml b/car/res/values-zh-rHK/strings.xml
index 61102cc..09c1a9e 100644
--- a/car/res/values-zh-rHK/strings.xml
+++ b/car/res/values-zh-rHK/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"請專心駕駛"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"展開/收合按鈕"</string>
 </resources>
diff --git a/car/res/values-zh-rTW/strings.xml b/car/res/values-zh-rTW/strings.xml
index 61102cc..09c1a9e 100644
--- a/car/res/values-zh-rTW/strings.xml
+++ b/car/res/values-zh-rTW/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"請專心駕駛"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"展開/收合按鈕"</string>
 </resources>
diff --git a/car/res/values-zu/strings.xml b/car/res/values-zu/strings.xml
index bdf7337..9403834 100644
--- a/car/res/values-zu/strings.xml
+++ b/car/res/values-zu/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="speed_bump_lockout_message" msgid="5405697774899378511">"Gxila emgwaqweni"</string>
+    <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Inkinobho yokunweba/ukugoqa"</string>
 </resources>
diff --git a/car/res/values/styles.xml b/car/res/values/styles.xml
index d6b6d28..aee10d2 100644
--- a/car/res/values/styles.xml
+++ b/car/res/values/styles.xml
@@ -173,6 +173,20 @@
         <item name="android:textColor">@color/car_body4</item>
     </style>
 
+    <!-- The styling for action button text. -->
+    <style name="TextAppearance.Car.Action1">
+        <item name="android:fontFamily">sans-serif-medium</item>
+        <item name="android:textStyle">normal</item>
+        <item name="android:textAllCaps">true</item>
+        <item name="android:textSize">@dimen/car_action1_size</item>
+        <item name="android:textColor">@color/car_accent</item>
+    </style>
+
+    <!-- The styling for menu text in action bar. -->
+    <style name="TextAppearance.Car.ActionBar.Menu" parent="TextAppearance.Car.Action1">
+        <item name="android:textColor">?attr/actionMenuTextColor</item>
+    </style>
+
     <!-- Styles for TextInputLayout hints. -->
     <style name="TextAppearance.Car.Hint" parent="TextAppearance.Car.Body2" />
 
@@ -181,11 +195,33 @@
     <!-- ======= -->
     <eat-comment />
 
-    <!-- The styling for the action bar. -->
+    <!-- The styling for Toolbar used as action bar. -->
     <style name="Widget.Car.Toolbar" parent="Widget.AppCompat.Toolbar">
-        <item name="titleTextAppearance">@style/TextAppearance.Car.Title.Light</item>
-        <item name="contentInsetStart">@dimen/car_keyline_1</item>
+        <item name="android:minHeight">?attr/actionBarSize</item>
+        <item name="background">@color/car_card</item>
         <item name="contentInsetEnd">@dimen/car_keyline_1</item>
+        <item name="contentInsetStart">@dimen/car_keyline_1</item>
+        <item name="elevation">@dimen/car_action_bar_elevation</item>
+        <item name="subtitleTextAppearance">@style/TextAppearance.Car.Body2</item>
+        <item name="titleTextAppearance">@style/TextAppearance.Car.Title2</item>
+        <item name="navigationIcon">@drawable/ic_nav_arrow_back</item>
+    </style>
+
+    <!-- The styling for the navigation button in action bar. -->
+    <style name="Widget.Car.Toolbar.Button.Navigation"
+           parent="Widget.AppCompat.Toolbar.Button.Navigation">
+        <item name="android:background">@drawable/car_card_ripple_background</item>
+        <item name="android:scaleType">center</item>
+        <item name="android:tint">@color/car_tint</item>
+    </style>
+
+    <style name="Widget.Car.ActionButton" parent="Widget.AppCompat.ActionButton">
+        <item name="android:background">?attr/actionBarItemBackground</item>
+        <item name="android:minHeight">@dimen/car_action_bar_height</item>
+        <item name="android:paddingLeft">@dimen/car_padding_2</item>
+        <item name="android:paddingRight">@dimen/car_padding_2</item>
+        <item name="android:scaleType">fitCenter</item>
+        <item name="android:tint">@color/car_tint</item>
     </style>
 
     <!-- The style for the menu bar (i.e. hamburger) and back arrow in the navigation drawer. -->
diff --git a/car/res/values/themes.xml b/car/res/values/themes.xml
index 526fd79..8d8c6ce 100644
--- a/car/res/values/themes.xml
+++ b/car/res/values/themes.xml
@@ -32,12 +32,19 @@
         <item name="android:editTextColor">@color/car_body1</item>
         <item name="android:colorControlNormal">@color/car_body2</item>
         <item name="android:seekBarStyle">@style/Widget.Car.SeekBar</item>
+        <item name="actionBarItemBackground">@drawable/car_card_ripple_background</item>
+        <item name="actionBarSize">@dimen/car_app_bar_height</item>
+        <item name="actionButtonStyle">@style/Widget.Car.ActionButton</item>
+        <item name="actionMenuTextAppearance">@style/TextAppearance.Car.ActionBar.Menu</item>
+        <item name="actionMenuTextColor">@color/car_accent</item>
         <item name="carDialogTheme">@style/Theme.Car.Dialog</item>
-        <item name="pagedListViewStyle">@style/Widget.Car.List</item>
         <item name="listItemBackgroundColor">@color/car_card</item>
         <item name="listItemTitleTextAppearance">@style/TextAppearance.Car.Body1</item>
         <item name="listItemBodyTextAppearance">@style/TextAppearance.Car.Body2</item>
         <item name="listItemSubheaderTextAppearance">@style/TextAppearance.Car.Subheader</item>
+        <item name="pagedListViewStyle">@style/Widget.Car.List</item>
+        <item name="toolbarNavigationButtonStyle">@style/Widget.Car.Toolbar.Button.Navigation</item>
+        <item name="toolbarStyle">@style/Widget.Car.Toolbar</item>
     </style>
 
     <!-- Theme for the Car that is a passthrough for the default theme. -->
diff --git a/car/src/androidTest/java/androidx/car/widget/SeekbarListItemTest.java b/car/src/androidTest/java/androidx/car/widget/SeekbarListItemTest.java
index f8b8d33..5233734 100644
--- a/car/src/androidTest/java/androidx/car/widget/SeekbarListItemTest.java
+++ b/car/src/androidTest/java/androidx/car/widget/SeekbarListItemTest.java
@@ -40,6 +40,8 @@
 import android.view.ViewGroup;
 import android.widget.SeekBar;
 
+import androidx.car.test.R;
+
 import org.hamcrest.Matcher;
 import org.junit.Assume;
 import org.junit.Before;
@@ -51,8 +53,6 @@
 import java.util.Arrays;
 import java.util.List;
 
-import androidx.car.test.R;
-
 /**
  * Tests the layout configuration in {@link SeekbarListItem}.
  */
@@ -67,11 +67,6 @@
     private PagedListViewTestActivity mActivity;
     private PagedListView mPagedListView;
 
-    private boolean isAutoDevice() {
-        PackageManager packageManager = mActivityRule.getActivity().getPackageManager();
-        return packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
-    }
-
     @Before
     public void setUp() {
         Assume.assumeTrue(isAutoDevice());
@@ -79,44 +74,6 @@
         mPagedListView = mActivity.findViewById(R.id.paged_list_view);
     }
 
-    private void setupPagedListView(List<? extends ListItem> items) {
-        ListItemProvider provider = new ListItemProvider.ListProvider(
-                new ArrayList<>(items));
-        try {
-            mActivityRule.runOnUiThread(() -> {
-                mPagedListView.setAdapter(new ListItemAdapter(mActivity, provider));
-            });
-        } catch (Throwable throwable) {
-            throwable.printStackTrace();
-            throw new RuntimeException(throwable);
-        }
-        // Wait for paged list view to layout by using espresso to scroll to a position.
-        onView(withId(R.id.recycler_view)).perform(scrollToPosition(0));
-    }
-
-    private void verifyViewDefaultVisibility(View view) {
-        if (view instanceof ViewGroup) {
-            ViewGroup viewGroup = (ViewGroup) view;
-            final int childCount = viewGroup.getChildCount();
-            for (int i = 0; i < childCount; i++) {
-                verifyViewDefaultVisibility(viewGroup.getChildAt(i));
-            }
-        } else if (view instanceof SeekBar) {
-            assertThat(view.getVisibility(), is(equalTo(View.VISIBLE)));
-        } else {
-            assertThat("Visibility of view "
-                    + mActivity.getResources().getResourceEntryName(view.getId())
-                    + " by default should be GONE.",
-                    view.getVisibility(), is(equalTo(View.GONE)));
-        }
-    }
-
-    private SeekbarListItem.ViewHolder getViewHolderAtPosition(int position) {
-        return (SeekbarListItem.ViewHolder) mPagedListView.getRecyclerView()
-                .findViewHolderForAdapterPosition(
-                        position);
-    }
-
     @Test
     public void testOnlySliderIsVisibleInEmptyItem() {
         SeekbarListItem item = new SeekbarListItem(mActivity, 0, 0, null, null);
@@ -182,6 +139,82 @@
     }
 
     @Test
+    public void testSettingMax() {
+        final int max = 50;
+        SeekbarListItem item = new SeekbarListItem(mActivity);
+        item.setMax(max);
+
+        setupPagedListView(Arrays.asList(item));
+
+        assertThat(getViewHolderAtPosition(0).getSeekBar().getMax(), is(equalTo(max)));
+    }
+
+    @Test
+    public void testSettingProgress() {
+        final int progress = 100;
+        SeekbarListItem item = new SeekbarListItem(mActivity);
+        item.setMax(progress);
+        item.setProgress(progress);
+
+        setupPagedListView(Arrays.asList(item));
+
+        assertThat(getViewHolderAtPosition(0).getSeekBar().getProgress(), is(equalTo(progress)));
+    }
+
+    @Test
+    public void testSettingSecondaryProgress() {
+        final int secondaryProgress = 50;
+        SeekbarListItem item = new SeekbarListItem(mActivity);
+        item.setMax(secondaryProgress);
+        item.setSecondaryProgress(secondaryProgress);
+
+        setupPagedListView(Arrays.asList(item));
+
+        assertThat(getViewHolderAtPosition(0).getSeekBar().getSecondaryProgress(),
+                is(equalTo(secondaryProgress)));
+    }
+
+    @Test
+    public void testSettingOnSeekBarChangeListener() throws Throwable {
+        boolean[] changed = new boolean[]{false};
+
+        SeekbarListItem item = new SeekbarListItem(mActivity);
+        item.setMax(100);
+        item.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                changed[0] = true;
+            }
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {}
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {}
+        });
+
+        setupPagedListView(Arrays.asList(item));
+
+        mActivityRule.runOnUiThread(() -> getViewHolderAtPosition(0).getSeekBar().setProgress(50));
+        assertTrue(changed[0]);
+    }
+
+    @Test
+    public void testUpdatingProgress() {
+        final int progress = 50;
+        final int newProgress = 100;
+
+        SeekbarListItem item = new SeekbarListItem(mActivity);
+        item.setMax(newProgress);
+        item.setProgress(progress);
+        setupPagedListView(Arrays.asList(item));
+
+        item.setProgress(newProgress);
+        refreshUi();
+
+        assertThat(getViewHolderAtPosition(0).getSeekBar().getProgress(), is(equalTo(newProgress)));
+    }
+
+    @Test
     public void testPrimaryIconIsNotClickable() {
         SeekbarListItem item0 = new SeekbarListItem(mActivity, 0, 0, null, null);
         item0.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon);
@@ -291,6 +324,62 @@
                 is(equalTo(drawable)));
     }
 
+    private boolean isAutoDevice() {
+        PackageManager packageManager = mActivityRule.getActivity().getPackageManager();
+        return packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+    }
+
+    private void setupPagedListView(List<? extends ListItem> items) {
+        ListItemProvider provider = new ListItemProvider.ListProvider(
+                new ArrayList<>(items));
+        try {
+            mActivityRule.runOnUiThread(() -> {
+                mPagedListView.setAdapter(new ListItemAdapter(mActivity, provider));
+            });
+        } catch (Throwable throwable) {
+            throwable.printStackTrace();
+            throw new RuntimeException(throwable);
+        }
+        // Wait for paged list view to layout by using espresso to scroll to a position.
+        onView(withId(R.id.recycler_view)).perform(scrollToPosition(0));
+    }
+
+    private void verifyViewDefaultVisibility(View view) {
+        if (view instanceof ViewGroup) {
+            ViewGroup viewGroup = (ViewGroup) view;
+            final int childCount = viewGroup.getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                verifyViewDefaultVisibility(viewGroup.getChildAt(i));
+            }
+        } else if (view instanceof SeekBar) {
+            assertThat(view.getVisibility(), is(equalTo(View.VISIBLE)));
+        } else {
+            assertThat("Visibility of view "
+                            + mActivity.getResources().getResourceEntryName(view.getId())
+                            + " by default should be GONE.",
+                    view.getVisibility(), is(equalTo(View.GONE)));
+        }
+    }
+
+    private SeekbarListItem.ViewHolder getViewHolderAtPosition(int position) {
+        return (SeekbarListItem.ViewHolder) mPagedListView.getRecyclerView()
+                .findViewHolderForAdapterPosition(
+                        position);
+    }
+
+    private void refreshUi() {
+        try {
+            mActivityRule.runOnUiThread(() -> {
+                mPagedListView.getAdapter().notifyDataSetChanged();
+            });
+        } catch (Throwable throwable) {
+            throwable.printStackTrace();
+            throw new RuntimeException(throwable);
+        }
+        // Wait for paged list view to layout by using espresso to scroll to a position.
+        onView(withId(R.id.recycler_view)).perform(scrollToPosition(0));
+    }
+
     private static ViewAction clickChildViewWithId(final int id) {
         return new ViewAction() {
             @Override
diff --git a/car/src/main/java/androidx/car/widget/DayNightStyle.java b/car/src/main/java/androidx/car/widget/DayNightStyle.java
index 37d0d51..73f9ce4 100644
--- a/car/src/main/java/androidx/car/widget/DayNightStyle.java
+++ b/car/src/main/java/androidx/car/widget/DayNightStyle.java
@@ -16,8 +16,12 @@
 
 package androidx.car.widget;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 import androidx.annotation.IntDef;
 
+import java.lang.annotation.Retention;
+
 /**
  * Specifies how the system UI should respond to day/night mode events.
  *
@@ -37,6 +41,7 @@
         DayNightStyle.FORCE_NIGHT,
         DayNightStyle.FORCE_DAY,
 })
+@Retention(SOURCE)
 public @interface DayNightStyle {
     /**
      * Sets the foreground color to be automatically changed based on day/night mode, assuming the
diff --git a/car/src/main/java/androidx/car/widget/PagedListView.java b/car/src/main/java/androidx/car/widget/PagedListView.java
index cddc89d..b951bc1 100644
--- a/car/src/main/java/androidx/car/widget/PagedListView.java
+++ b/car/src/main/java/androidx/car/widget/PagedListView.java
@@ -18,6 +18,8 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
@@ -50,6 +52,8 @@
 import androidx.recyclerview.widget.OrientationHelper;
 import androidx.recyclerview.widget.RecyclerView;
 
+import java.lang.annotation.Retention;
+
 /**
  * View that wraps a {@link RecyclerView} and a scroll bar that has
  * page up and down arrows. Interaction with this view is similar to a {@code RecyclerView} as it
@@ -187,6 +191,7 @@
             Gutter.END,
             Gutter.BOTH,
     })
+    @Retention(SOURCE)
     public @interface Gutter {
         /**
          * No gutter on either side of the list items. The items will span the full width of the
diff --git a/car/src/main/java/androidx/car/widget/SeekbarListItem.java b/car/src/main/java/androidx/car/widget/SeekbarListItem.java
index b6477c1..994ba4f 100644
--- a/car/src/main/java/androidx/car/widget/SeekbarListItem.java
+++ b/car/src/main/java/androidx/car/widget/SeekbarListItem.java
@@ -66,6 +66,8 @@
  *
  * <p>When conflicting methods are called (e.g. setting primary action to both primary icon and
  * no icon), the last called method wins.
+ *
+ * {@code minimum value} is set to 0.
  */
 public class SeekbarListItem extends ListItem<SeekbarListItem.ViewHolder> {
 
@@ -99,8 +101,9 @@
 
     private String mText;
 
-    private int mProgress;
     private int mMax;
+    private int mProgress;
+    private int mSecondaryProgress;
     private SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener;
 
     @SupplementalActionType private int mSupplementalActionType = SUPPLEMENTAL_ACTION_NO_ACTION;
@@ -123,16 +126,22 @@
      * @param progress the current progress of the specified value.
      * @param listener listener to receive notification of changes to progress level.
      * @param text displays a text on top of the SeekBar.
+     *
+     * @deprecated use {@link #SeekbarListItem(Context)} and individual field setters instead.
      */
+    @Deprecated
     public SeekbarListItem(Context context, int max, int progress,
             SeekBar.OnSeekBarChangeListener listener, String text) {
+        this(context);
+
+        setMax(max);
+        setProgress(progress);
+        setOnSeekBarChangeListener(listener);
+        setText(text);
+    }
+
+    public SeekbarListItem(Context context) {
         mContext = context;
-
-        mMax = max;
-        mProgress = progress;
-        mOnSeekBarChangeListener = listener;
-        mText = text;
-
         markDirty();
     }
 
@@ -145,6 +154,46 @@
     }
 
     /**
+     * Sets max value of seekbar.
+     */
+    public void setMax(int max) {
+        mMax = max;
+        markDirty();
+    }
+
+    /**
+     * Sets progress of seekbar.
+     */
+    public void setProgress(int progress) {
+        mProgress = progress;
+        markDirty();
+    }
+
+    /**
+     * Sets secondary progress of seekbar.
+     */
+    public void setSecondaryProgress(int secondaryProgress) {
+        mSecondaryProgress = secondaryProgress;
+        markDirty();
+    }
+
+    /**
+     * Sets {@link SeekBar.OnSeekBarChangeListener}.
+     */
+    public void setOnSeekBarChangeListener(SeekBar.OnSeekBarChangeListener listener) {
+        mOnSeekBarChangeListener = listener;
+        markDirty();
+    }
+
+    /**
+     * Sets text that sits on top of seekbar.
+     */
+    public void setText(String text) {
+        mText = text;
+        markDirty();
+    }
+
+    /**
      * Calculates the layout params for views in {@link ViewHolder}.
      */
     @Override
@@ -272,6 +321,7 @@
         mBinders.add(vh -> {
             vh.getSeekBar().setMax(mMax);
             vh.getSeekBar().setProgress(mProgress);
+            vh.getSeekBar().setSecondaryProgress(mSecondaryProgress);
             vh.getSeekBar().setOnSeekBarChangeListener(mOnSeekBarChangeListener);
 
             if (!TextUtils.isEmpty(mText)) {
diff --git a/collection/ktx/OWNERS b/collection/ktx/OWNERS
new file mode 100644
index 0000000..e450f4c
--- /dev/null
+++ b/collection/ktx/OWNERS
@@ -0,0 +1 @@
+jakew@google.com
diff --git a/collection/ktx/build.gradle b/collection/ktx/build.gradle
new file mode 100644
index 0000000..83944f2
--- /dev/null
+++ b/collection/ktx/build.gradle
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+
+plugins {
+    id("SupportKotlinLibraryPlugin")
+}
+
+dependencies {
+    compile(project(":collection"))
+    compile(KOTLIN_STDLIB)
+    testCompile(JUNIT)
+    testCompile(TRUTH)
+    testCompile(project(":internal-testutils-ktx"))
+}
+
+supportLibrary {
+    name = "Collections Kotlin Extensions"
+    publish = true
+    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenGroup = LibraryGroups.COLLECTION
+    inceptionYear = "2018"
+    description = "Kotlin extensions for 'collection' artifact"
+}
diff --git a/collection/ktx/src/main/java/androidx/collection/ArrayMap.kt b/collection/ktx/src/main/java/androidx/collection/ArrayMap.kt
new file mode 100644
index 0000000..8f3299e
--- /dev/null
+++ b/collection/ktx/src/main/java/androidx/collection/ArrayMap.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.collection
+
+/** Returns an empty new [ArrayMap]. */
+inline fun <K, V> arrayMapOf(): ArrayMap<K, V> = ArrayMap()
+
+/**
+ * Returns a new [ArrayMap] with the specified contents, given as a list of pairs where the first
+ * component is the key and the second component is the value.
+ *
+ * If multiple pairs have the same key, the resulting map will contain the value from the last of
+ * those pairs.
+ */
+fun <K, V> arrayMapOf(vararg pairs: Pair<K, V>): ArrayMap<K, V> {
+    val map = ArrayMap<K, V>(pairs.size)
+    for (pair in pairs) {
+        map[pair.first] = pair.second
+    }
+    return map
+}
diff --git a/collection/ktx/src/main/java/androidx/collection/ArraySet.kt b/collection/ktx/src/main/java/androidx/collection/ArraySet.kt
new file mode 100644
index 0000000..07b4be7
--- /dev/null
+++ b/collection/ktx/src/main/java/androidx/collection/ArraySet.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.collection
+
+/** Returns an empty new [ArraySet]. */
+inline fun <T> arraySetOf(): ArraySet<T> = ArraySet()
+
+/** Returns a new [ArraySet] with the specified contents. */
+fun <T> arraySetOf(vararg values: T): ArraySet<T> {
+    val set = ArraySet<T>(values.size)
+    @Suppress("LoopToCallChain") // Causes needless copy to a list.
+    for (value in values) {
+        set.add(value)
+    }
+    return set
+}
diff --git a/collection/ktx/src/main/java/androidx/collection/LongSparseArray.kt b/collection/ktx/src/main/java/androidx/collection/LongSparseArray.kt
new file mode 100644
index 0000000..fd6201a
--- /dev/null
+++ b/collection/ktx/src/main/java/androidx/collection/LongSparseArray.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.collection
+
+/** Returns the number of key/value pairs in the collection. */
+inline val <T> LongSparseArray<T>.size get() = size()
+
+/** Returns true if the collection contains [key]. */
+inline operator fun <T> LongSparseArray<T>.contains(key: Long) = indexOfKey(key) >= 0
+
+/** Allows the use of the index operator for storing values in the collection. */
+inline operator fun <T> LongSparseArray<T>.set(key: Long, value: T) = put(key, value)
+
+/** Creates a new collection by adding or replacing entries from [other]. */
+operator fun <T> LongSparseArray<T>.plus(other: LongSparseArray<T>): LongSparseArray<T> {
+    val new = LongSparseArray<T>(size() + other.size())
+    new.putAll(this)
+    new.putAll(other)
+    return new
+}
+
+/** Returns true if the collection contains [key]. */
+inline fun <T> LongSparseArray<T>.containsKey(key: Long) = indexOfKey(key) >= 0
+
+/** Returns true if the collection contains [value]. */
+inline fun <T> LongSparseArray<T>.containsValue(value: T) = indexOfValue(value) != -1
+
+/** Return the value corresponding to [key], or [defaultValue] when not present. */
+inline fun <T> LongSparseArray<T>.getOrDefault(key: Long, defaultValue: T) =
+    get(key) ?: defaultValue
+
+/** Return the value corresponding to [key], or from [defaultValue] when not present. */
+inline fun <T> LongSparseArray<T>.getOrElse(key: Long, defaultValue: () -> T) =
+    get(key) ?: defaultValue()
+
+/** Return true when the collection contains elements. */
+inline fun <T> LongSparseArray<T>.isNotEmpty() = size() != 0
+
+/** Removes the entry for [key] only if it is mapped to [value]. */
+fun <T> LongSparseArray<T>.remove(key: Long, value: T): Boolean {
+    val index = indexOfKey(key)
+    if (index != -1 && value == valueAt(index)) {
+        removeAt(index)
+        return true
+    }
+    return false
+}
+
+/** Update this collection by adding or replacing entries from [other]. */
+fun <T> LongSparseArray<T>.putAll(other: LongSparseArray<T>) = other.forEach(::put)
+
+/** Performs the given [action] for each key/value entry. */
+inline fun <T> LongSparseArray<T>.forEach(action: (key: Long, value: T) -> Unit) {
+    for (index in 0 until size()) {
+        action(keyAt(index), valueAt(index))
+    }
+}
+
+/** Return an iterator over the collection's keys. */
+fun <T> LongSparseArray<T>.keyIterator(): LongIterator = object : LongIterator() {
+    var index = 0
+    override fun hasNext() = index < size()
+    override fun nextLong() = keyAt(index++)
+}
+
+/** Return an iterator over the collection's values. */
+fun <T> LongSparseArray<T>.valueIterator(): Iterator<T> = object : Iterator<T> {
+    var index = 0
+    override fun hasNext() = index < size()
+    override fun next() = valueAt(index++)
+}
diff --git a/collection/ktx/src/main/java/androidx/collection/LruCache.kt b/collection/ktx/src/main/java/androidx/collection/LruCache.kt
new file mode 100644
index 0000000..554a012
--- /dev/null
+++ b/collection/ktx/src/main/java/androidx/collection/LruCache.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+package androidx.collection
+
+/**
+ * Creates an [LruCache] with the given parameters.
+ *
+ * @param maxSize for caches that do not specify [sizeOf], this is
+ * the maximum number of entries in the cache. For all other caches,
+ * this is the maximum sum of the sizes of the entries in this cache.
+ * @param sizeOf function that returns the size of the entry for key and value in
+ * user-defined units. The default implementation returns 1.
+ * @param create a create called after a cache miss to compute a value for the corresponding key.
+ * Returns the computed value or null if no value can be computed. The default implementation
+ * returns null.
+ * @param onEntryRemoved a function called for entries that have been evicted or removed.
+ *
+ * @see LruCache.sizeOf
+ * @see LruCache.create
+ * @see LruCache.entryRemoved
+ */
+inline fun <K : Any, V : Any> lruCache(
+    maxSize: Int,
+    crossinline sizeOf: (key: K, value: V) -> Int = { _, _ -> 1 },
+    @Suppress("USELESS_CAST") // https://youtrack.jetbrains.com/issue/KT-21946
+    crossinline create: (key: K) -> V? = { null as V? },
+    crossinline onEntryRemoved: (evicted: Boolean, key: K, oldValue: V, newValue: V?) -> Unit =
+        { _, _, _, _ -> }
+): LruCache<K, V> {
+    return object : LruCache<K, V>(maxSize) {
+        override fun sizeOf(key: K, value: V) = sizeOf(key, value)
+        override fun create(key: K) = create(key)
+        override fun entryRemoved(evicted: Boolean, key: K, oldValue: V, newValue: V?) {
+            onEntryRemoved(evicted, key, oldValue, newValue)
+        }
+    }
+}
diff --git a/collection/ktx/src/main/java/androidx/collection/SparseArray.kt b/collection/ktx/src/main/java/androidx/collection/SparseArray.kt
new file mode 100644
index 0000000..e1d2382
--- /dev/null
+++ b/collection/ktx/src/main/java/androidx/collection/SparseArray.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.collection
+
+/** Returns the number of key/value pairs in the collection. */
+inline val <T> SparseArrayCompat<T>.size get() = size()
+
+/** Returns true if the collection contains [key]. */
+inline operator fun <T> SparseArrayCompat<T>.contains(key: Int) = indexOfKey(key) >= 0
+
+/** Allows the use of the index operator for storing values in the collection. */
+inline operator fun <T> SparseArrayCompat<T>.set(key: Int, value: T) = put(key, value)
+
+/** Creates a new collection by adding or replacing entries from [other]. */
+operator fun <T> SparseArrayCompat<T>.plus(other: SparseArrayCompat<T>): SparseArrayCompat<T> {
+    val new = SparseArrayCompat<T>(size() + other.size())
+    new.putAll(this)
+    new.putAll(other)
+    return new
+}
+
+/** Returns true if the collection contains [key]. */
+inline fun <T> SparseArrayCompat<T>.containsKey(key: Int) = indexOfKey(key) >= 0
+
+/** Returns true if the collection contains [value]. */
+inline fun <T> SparseArrayCompat<T>.containsValue(value: T) = indexOfValue(value) != -1
+
+/** Return the value corresponding to [key], or [defaultValue] when not present. */
+inline fun <T> SparseArrayCompat<T>.getOrDefault(key: Int, defaultValue: T) =
+    get(key) ?: defaultValue
+
+/** Return the value corresponding to [key], or from [defaultValue] when not present. */
+inline fun <T> SparseArrayCompat<T>.getOrElse(key: Int, defaultValue: () -> T) =
+    get(key) ?: defaultValue()
+
+/** Return true when the collection contains elements. */
+inline fun <T> SparseArrayCompat<T>.isNotEmpty() = size() != 0
+
+/** Removes the entry for [key] only if it is mapped to [value]. */
+fun <T> SparseArrayCompat<T>.remove(key: Int, value: T): Boolean {
+    val index = indexOfKey(key)
+    if (index != -1 && value == valueAt(index)) {
+        removeAt(index)
+        return true
+    }
+    return false
+}
+
+/** Update this collection by adding or replacing entries from [other]. */
+fun <T> SparseArrayCompat<T>.putAll(other: SparseArrayCompat<T>) = other.forEach(::put)
+
+/** Performs the given [action] for each key/value entry. */
+inline fun <T> SparseArrayCompat<T>.forEach(action: (key: Int, value: T) -> Unit) {
+    for (index in 0 until size()) {
+        action(keyAt(index), valueAt(index))
+    }
+}
+
+/** Return an iterator over the collection's keys. */
+fun <T> SparseArrayCompat<T>.keyIterator(): IntIterator = object : IntIterator() {
+    var index = 0
+    override fun hasNext() = index < size()
+    override fun nextInt() = keyAt(index++)
+}
+
+/** Return an iterator over the collection's values. */
+fun <T> SparseArrayCompat<T>.valueIterator(): Iterator<T> = object : Iterator<T> {
+    var index = 0
+    override fun hasNext() = index < size()
+    override fun next() = valueAt(index++)
+}
diff --git a/collection/ktx/src/test/java/androidx/collection/ArrayMapTest.kt b/collection/ktx/src/test/java/androidx/collection/ArrayMapTest.kt
new file mode 100644
index 0000000..8c002dd
--- /dev/null
+++ b/collection/ktx/src/test/java/androidx/collection/ArrayMapTest.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ArrayMapTest {
+    @Test fun empty() {
+        val map = arrayMapOf<String, String>()
+        assertEquals(0, map.size)
+    }
+
+    @Test fun nonEmpty() {
+        val map = arrayMapOf("foo" to "bar", "bar" to "baz")
+        assertThat(map).containsExactly("foo", "bar", "bar", "baz")
+    }
+
+    @Test fun duplicateKeyKeepsLast() {
+        val map = arrayMapOf("foo" to "bar", "foo" to "baz")
+        assertThat(map).containsExactly("foo", "baz")
+    }
+}
diff --git a/collection/ktx/src/test/java/androidx/collection/ArraySetTest.kt b/collection/ktx/src/test/java/androidx/collection/ArraySetTest.kt
new file mode 100644
index 0000000..71a561d
--- /dev/null
+++ b/collection/ktx/src/test/java/androidx/collection/ArraySetTest.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ArraySetTest {
+    @Test fun empty() {
+        val set = arraySetOf<String>()
+        assertEquals(0, set.size)
+    }
+
+    @Test fun nonEmpty() {
+        val set = arraySetOf("foo", "bar", "baz")
+        assertThat(set).containsExactly("foo", "bar", "baz")
+    }
+}
diff --git a/collection/ktx/src/test/java/androidx/collection/LongSparseArrayTest.kt b/collection/ktx/src/test/java/androidx/collection/LongSparseArrayTest.kt
new file mode 100644
index 0000000..3419664
--- /dev/null
+++ b/collection/ktx/src/test/java/androidx/collection/LongSparseArrayTest.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class LongSparseArrayTest {
+    @Test fun sizeProperty() {
+        val array = LongSparseArray<String>()
+        assertEquals(0, array.size)
+        array.put(1L, "one")
+        assertEquals(1, array.size)
+    }
+
+    @Test fun containsOperator() {
+        val array = LongSparseArray<String>()
+        assertFalse(1L in array)
+        array.put(1L, "one")
+        assertTrue(1L in array)
+    }
+
+    @Test fun containsOperatorWithValue() {
+        val array = LongSparseArray<String>()
+
+        array.put(1L, "one")
+        assertFalse(2L in array)
+
+        array.put(2L, "two")
+        assertTrue(2L in array)
+    }
+
+    @Test fun setOperator() {
+        val array = LongSparseArray<String>()
+        array[1L] = "one"
+        assertEquals("one", array.get(1L))
+    }
+
+    @Test fun plusOperator() {
+        val first = LongSparseArray<String>().apply { put(1L, "one") }
+        val second = LongSparseArray<String>().apply { put(2L, "two") }
+        val combined = first + second
+        assertEquals(2, combined.size())
+        assertEquals(1L, combined.keyAt(0))
+        assertEquals("one", combined.valueAt(0))
+        assertEquals(2L, combined.keyAt(1))
+        assertEquals("two", combined.valueAt(1))
+    }
+
+    @Test fun containsKey() {
+        val array = LongSparseArray<String>()
+        assertFalse(array.containsKey(1L))
+        array.put(1L, "one")
+        assertTrue(array.containsKey(1L))
+    }
+
+    @Test fun containsKeyWithValue() {
+        val array = LongSparseArray<String>()
+
+        array.put(1L, "one")
+        assertFalse(array.containsKey(2L))
+
+        array.put(2L, "one")
+        assertTrue(array.containsKey(2L))
+    }
+
+    @Test fun containsValue() {
+        val array = LongSparseArray<String>()
+        assertFalse(array.containsValue("one"))
+        array.put(1L, "one")
+        assertTrue(array.containsValue("one"))
+    }
+
+    @Test fun getOrDefault() {
+        val array = LongSparseArray<Any>()
+        val default = Any()
+        assertSame(default, array.getOrDefault(1L, default))
+        array.put(1L, "one")
+        assertEquals("one", array.getOrDefault(1L, default))
+    }
+
+    @Test fun getOrElse() {
+        val array = LongSparseArray<Any>()
+        val default = Any()
+        assertSame(default, array.getOrElse(1L) { default })
+        array.put(1L, "one")
+        assertEquals("one", array.getOrElse(1L) { fail() })
+    }
+
+    @Test fun isNotEmpty() {
+        val array = LongSparseArray<String>()
+        assertFalse(array.isNotEmpty())
+        array.put(1L, "one")
+        assertTrue(array.isNotEmpty())
+    }
+
+    @Test fun removeValue() {
+        val array = LongSparseArray<String>()
+        array.put(1L, "one")
+        assertFalse(array.remove(0L, "one"))
+        assertEquals(1, array.size())
+        assertFalse(array.remove(1L, "two"))
+        assertEquals(1, array.size())
+        assertTrue(array.remove(1L, "one"))
+        assertEquals(0, array.size())
+    }
+
+    @Test fun putAll() {
+        val dest = LongSparseArray<String>()
+        val source = LongSparseArray<String>()
+        source.put(1L, "one")
+
+        assertEquals(0, dest.size())
+        dest.putAll(source)
+        assertEquals(1, dest.size())
+    }
+
+    @Test fun forEach() {
+        val array = LongSparseArray<String>()
+        array.forEach { _, _ -> fail() }
+
+        array.put(1L, "one")
+        array.put(2L, "two")
+        array.put(6L, "six")
+
+        val keys = mutableListOf<Long>()
+        val values = mutableListOf<String>()
+        array.forEach { key, value ->
+            keys.add(key)
+            values.add(value)
+        }
+        assertThat(keys).containsExactly(1L, 2L, 6L)
+        assertThat(values).containsExactly("one", "two", "six")
+    }
+
+    @Test fun keyIterator() {
+        val array = LongSparseArray<String>()
+        assertFalse(array.keyIterator().hasNext())
+
+        array.put(1L, "one")
+        array.put(2L, "two")
+        array.put(6L, "six")
+
+        val iterator = array.keyIterator()
+        assertTrue(iterator.hasNext())
+        assertEquals(1L, iterator.nextLong())
+        assertTrue(iterator.hasNext())
+        assertEquals(2L, iterator.nextLong())
+        assertTrue(iterator.hasNext())
+        assertEquals(6L, iterator.nextLong())
+        assertFalse(iterator.hasNext())
+    }
+
+    @Test fun valueIterator() {
+        val array = LongSparseArray<String>()
+        assertFalse(array.valueIterator().hasNext())
+
+        array.put(1L, "one")
+        array.put(2L, "two")
+        array.put(6L, "six")
+
+        val iterator = array.valueIterator()
+        assertTrue(iterator.hasNext())
+        assertEquals("one", iterator.next())
+        assertTrue(iterator.hasNext())
+        assertEquals("two", iterator.next())
+        assertTrue(iterator.hasNext())
+        assertEquals("six", iterator.next())
+        assertFalse(iterator.hasNext())
+    }
+}
diff --git a/collection/ktx/src/test/java/androidx/collection/LruCacheTest.kt b/collection/ktx/src/test/java/androidx/collection/LruCacheTest.kt
new file mode 100644
index 0000000..c47b423
--- /dev/null
+++ b/collection/ktx/src/test/java/androidx/collection/LruCacheTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+package androidx.collection
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class LruCacheTest {
+    private data class TestData(val x: String = "bla")
+
+    @Test fun size() {
+        val cache = lruCache<String, TestData>(200, { k, (x) -> k.length * x.length })
+        cache.put("long", TestData())
+        assertEquals(cache.size(), 12)
+    }
+
+    @Test fun create() {
+        val cache = lruCache<String, TestData>(200, create = { key -> TestData("$key foo") })
+        assertEquals(cache.get("kung"), TestData("kung foo"))
+    }
+
+    @Test fun onEntryRemoved() {
+        var wasCalled = false
+
+        val cache = lruCache<String, TestData>(200, onEntryRemoved = { _, _, _, _ ->
+            wasCalled = true
+        })
+        val initial = TestData()
+        cache.put("a", initial)
+        cache.remove("a")
+        assertTrue(wasCalled)
+    }
+}
diff --git a/collection/ktx/src/test/java/androidx/collection/SparseArrayTest.kt b/collection/ktx/src/test/java/androidx/collection/SparseArrayTest.kt
new file mode 100644
index 0000000..90d46c8
--- /dev/null
+++ b/collection/ktx/src/test/java/androidx/collection/SparseArrayTest.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class SparseArrayCompatTest {
+    @Test fun sizeProperty() {
+        val array = SparseArrayCompat<String>()
+        assertEquals(0, array.size)
+        array.put(1, "one")
+        assertEquals(1, array.size)
+    }
+
+    @Test fun containsOperator() {
+        val array = SparseArrayCompat<String>()
+        assertFalse(1 in array)
+        array.put(1, "one")
+        assertTrue(1 in array)
+    }
+
+    @Test fun containsOperatorWithItem() {
+        val array = SparseArrayCompat<String>()
+
+        array.put(1, "one")
+        assertFalse(2 in array)
+
+        array.put(2, "two")
+        assertTrue(2 in array)
+    }
+
+    @Test fun setOperator() {
+        val array = SparseArrayCompat<String>()
+        array[1] = "one"
+        assertEquals("one", array.get(1))
+    }
+
+    @Test fun plusOperator() {
+        val first = SparseArrayCompat<String>().apply { put(1, "one") }
+        val second = SparseArrayCompat<String>().apply { put(2, "two") }
+        val combined = first + second
+        assertEquals(2, combined.size())
+        assertEquals(1, combined.keyAt(0))
+        assertEquals("one", combined.valueAt(0))
+        assertEquals(2, combined.keyAt(1))
+        assertEquals("two", combined.valueAt(1))
+    }
+
+    @Test fun containsKey() {
+        val array = SparseArrayCompat<String>()
+        assertFalse(array.containsKey(1))
+        array.put(1, "one")
+        assertTrue(array.containsKey(1))
+    }
+
+    @Test fun containsValue() {
+        val array = SparseArrayCompat<String>()
+        assertFalse(array.containsValue("one"))
+        array.put(1, "one")
+        assertTrue(array.containsValue("one"))
+    }
+
+    @Test fun getOrDefault() {
+        val array = SparseArrayCompat<Any>()
+        val default = Any()
+        assertSame(default, array.getOrDefault(1, default))
+        array.put(1, "one")
+        assertEquals("one", array.getOrDefault(1, default))
+    }
+
+    @Test fun getOrElse() {
+        val array = SparseArrayCompat<Any>()
+        val default = Any()
+        assertSame(default, array.getOrElse(1) { default })
+        array.put(1, "one")
+        assertEquals("one", array.getOrElse(1) { fail() })
+    }
+
+    @Test fun isNotEmpty() {
+        val array = SparseArrayCompat<String>()
+        assertFalse(array.isNotEmpty())
+        array.put(1, "one")
+        assertTrue(array.isNotEmpty())
+    }
+
+    @Test fun removeValue() {
+        val array = SparseArrayCompat<String>()
+        array.put(1, "one")
+        assertFalse(array.remove(0, "one"))
+        assertEquals(1, array.size())
+        assertFalse(array.remove(1, "two"))
+        assertEquals(1, array.size())
+        assertTrue(array.remove(1, "one"))
+        assertEquals(0, array.size())
+    }
+
+    @Test fun putAll() {
+        val dest = SparseArrayCompat<String>()
+        val source = SparseArrayCompat<String>()
+        source.put(1, "one")
+
+        assertEquals(0, dest.size())
+        dest.putAll(source)
+        assertEquals(1, dest.size())
+    }
+
+    @Test fun forEach() {
+        val array = SparseArrayCompat<String>()
+        array.forEach { _, _ -> fail() }
+
+        array.put(1, "one")
+        array.put(2, "two")
+        array.put(6, "six")
+
+        val keys = mutableListOf<Int>()
+        val values = mutableListOf<String>()
+        array.forEach { key, value ->
+            keys.add(key)
+            values.add(value)
+        }
+        assertThat(keys).containsExactly(1, 2, 6)
+        assertThat(values).containsExactly("one", "two", "six")
+    }
+
+    @Test fun keyIterator() {
+        val array = SparseArrayCompat<String>()
+        assertFalse(array.keyIterator().hasNext())
+
+        array.put(1, "one")
+        array.put(2, "two")
+        array.put(6, "six")
+
+        val iterator = array.keyIterator()
+        assertTrue(iterator.hasNext())
+        assertEquals(1, iterator.nextInt())
+        assertTrue(iterator.hasNext())
+        assertEquals(2, iterator.nextInt())
+        assertTrue(iterator.hasNext())
+        assertEquals(6, iterator.nextInt())
+        assertFalse(iterator.hasNext())
+    }
+
+    @Test fun valueIterator() {
+        val array = SparseArrayCompat<String>()
+        assertFalse(array.valueIterator().hasNext())
+
+        array.put(1, "one")
+        array.put(2, "two")
+        array.put(6, "six")
+
+        val iterator = array.valueIterator()
+        assertTrue(iterator.hasNext())
+        assertEquals("one", iterator.next())
+        assertTrue(iterator.hasNext())
+        assertEquals("two", iterator.next())
+        assertTrue(iterator.hasNext())
+        assertEquals("six", iterator.next())
+        assertFalse(iterator.hasNext())
+    }
+}
diff --git a/compat/api/current.txt b/compat/api/current.txt
index c80da7b..529cbba 100644
--- a/compat/api/current.txt
+++ b/compat/api/current.txt
@@ -81,6 +81,20 @@
     method public static void setExactAndAllowWhileIdle(android.app.AlarmManager, int, long, android.app.PendingIntent);
   }
 
+  public class AppComponentFactory extends android.app.AppComponentFactory {
+    ctor public AppComponentFactory();
+    method public final android.app.Activity instantiateActivity(java.lang.ClassLoader, java.lang.String, android.content.Intent) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+    method public android.app.Activity instantiateActivityCompat(java.lang.ClassLoader, java.lang.String, android.content.Intent) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+    method public final android.app.Application instantiateApplication(java.lang.ClassLoader, java.lang.String) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+    method public android.app.Application instantiateApplicationCompat(java.lang.ClassLoader, java.lang.String) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+    method public final android.content.ContentProvider instantiateProvider(java.lang.ClassLoader, java.lang.String) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+    method public android.content.ContentProvider instantiateProviderCompat(java.lang.ClassLoader, java.lang.String) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+    method public final android.content.BroadcastReceiver instantiateReceiver(java.lang.ClassLoader, java.lang.String, android.content.Intent) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+    method public android.content.BroadcastReceiver instantiateReceiverCompat(java.lang.ClassLoader, java.lang.String, android.content.Intent) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+    method public final android.app.Service instantiateService(java.lang.ClassLoader, java.lang.String, android.content.Intent) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+    method public android.app.Service instantiateServiceCompat(java.lang.ClassLoader, java.lang.String, android.content.Intent) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+  }
+
   public class AppLaunchChecker {
     ctor public deprecated AppLaunchChecker();
     method public static boolean hasStartedFromLauncher(android.content.Context);
@@ -163,6 +177,7 @@
     method public static int getBadgeIconType(android.app.Notification);
     method public static java.lang.String getCategory(android.app.Notification);
     method public static java.lang.String getChannelId(android.app.Notification);
+    method public static java.lang.CharSequence getContentTitle(android.app.Notification);
     method public static android.os.Bundle getExtras(android.app.Notification);
     method public static java.lang.String getGroup(android.app.Notification);
     method public static int getGroupAlertBehavior(android.app.Notification);
@@ -206,6 +221,7 @@
     field public static final java.lang.String EXTRA_LARGE_ICON_BIG = "android.largeIcon.big";
     field public static final java.lang.String EXTRA_MEDIA_SESSION = "android.mediaSession";
     field public static final java.lang.String EXTRA_MESSAGES = "android.messages";
+    field public static final java.lang.String EXTRA_MESSAGING_STYLE_USER = "android.messagingStyleUser";
     field public static final java.lang.String EXTRA_PEOPLE = "android.people";
     field public static final java.lang.String EXTRA_PICTURE = "android.picture";
     field public static final java.lang.String EXTRA_PROGRESS = "android.progress";
@@ -442,7 +458,8 @@
   }
 
   public static class NotificationCompat.MessagingStyle extends androidx.core.app.NotificationCompat.Style {
-    ctor public NotificationCompat.MessagingStyle(java.lang.CharSequence);
+    ctor public deprecated NotificationCompat.MessagingStyle(java.lang.CharSequence);
+    ctor public NotificationCompat.MessagingStyle(androidx.core.app.Person);
     method public void addCompatExtras(android.os.Bundle);
     method public deprecated androidx.core.app.NotificationCompat.MessagingStyle addMessage(java.lang.CharSequence, long, java.lang.CharSequence);
     method public androidx.core.app.NotificationCompat.MessagingStyle addMessage(java.lang.CharSequence, long, androidx.core.app.Person);
@@ -450,7 +467,8 @@
     method public static androidx.core.app.NotificationCompat.MessagingStyle extractMessagingStyleFromNotification(android.app.Notification);
     method public java.lang.CharSequence getConversationTitle();
     method public java.util.List<androidx.core.app.NotificationCompat.MessagingStyle.Message> getMessages();
-    method public java.lang.CharSequence getUserDisplayName();
+    method public androidx.core.app.Person getUser();
+    method public deprecated java.lang.CharSequence getUserDisplayName();
     method public boolean isGroupConversation();
     method public androidx.core.app.NotificationCompat.MessagingStyle setConversationTitle(java.lang.CharSequence);
     method public androidx.core.app.NotificationCompat.MessagingStyle setGroupConversation(boolean);
@@ -809,6 +827,11 @@
     method public static long getLongVersionCode(android.content.pm.PackageInfo);
   }
 
+  public final class PermissionInfoCompat {
+    method public static int getProtection(android.content.pm.PermissionInfo);
+    method public static int getProtectionFlags(android.content.pm.PermissionInfo);
+  }
+
   public class ShortcutInfoCompat {
     method public android.content.ComponentName getActivity();
     method public java.lang.CharSequence getDisabledMessage();
@@ -866,6 +889,10 @@
 
 package androidx.core.database {
 
+  public final class CursorWindowCompat {
+    method public android.database.CursorWindow create(java.lang.String, long);
+  }
+
   public final deprecated class DatabaseUtilsCompat {
     method public static deprecated java.lang.String[] appendSelectionArgs(java.lang.String[], java.lang.String[]);
     method public static deprecated java.lang.String concatenateWhere(java.lang.String, java.lang.String);
@@ -873,6 +900,14 @@
 
 }
 
+package androidx.core.database.sqlite {
+
+  public final class SQLiteCursorCompat {
+    method public void setFillWindowForwardOnly(android.database.sqlite.SQLiteCursor, boolean);
+  }
+
+}
+
 package androidx.core.graphics {
 
   public final class BitmapCompat {
@@ -1275,6 +1310,40 @@
     method public static java.lang.String maximizeAndGetScript(java.util.Locale);
   }
 
+  public class PrecomputedTextCompat implements android.text.Spannable {
+    method public char charAt(int);
+    method public static androidx.core.text.PrecomputedTextCompat create(java.lang.CharSequence, androidx.core.text.PrecomputedTextCompat.Params);
+    method public int getParagraphCount();
+    method public int getParagraphEnd(int);
+    method public int getParagraphStart(int);
+    method public androidx.core.text.PrecomputedTextCompat.Params getParams();
+    method public int getSpanEnd(java.lang.Object);
+    method public int getSpanFlags(java.lang.Object);
+    method public int getSpanStart(java.lang.Object);
+    method public <T> T[] getSpans(int, int, java.lang.Class<T>);
+    method public int length();
+    method public int nextSpanTransition(int, int, java.lang.Class);
+    method public void removeSpan(java.lang.Object);
+    method public void setSpan(java.lang.Object, int, int, int);
+    method public java.lang.CharSequence subSequence(int, int);
+  }
+
+  public static final class PrecomputedTextCompat.Params {
+    ctor public PrecomputedTextCompat.Params(android.text.PrecomputedText.Params);
+    method public int getBreakStrategy();
+    method public int getHyphenationFrequency();
+    method public android.text.TextDirectionHeuristic getTextDirection();
+    method public android.text.TextPaint getTextPaint();
+  }
+
+  public static class PrecomputedTextCompat.Params.Builder {
+    ctor public PrecomputedTextCompat.Params.Builder(android.text.TextPaint);
+    method public androidx.core.text.PrecomputedTextCompat.Params build();
+    method public androidx.core.text.PrecomputedTextCompat.Params.Builder setBreakStrategy(int);
+    method public androidx.core.text.PrecomputedTextCompat.Params.Builder setHyphenationFrequency(int);
+    method public androidx.core.text.PrecomputedTextCompat.Params.Builder setTextDirection(android.text.TextDirectionHeuristic);
+  }
+
   public abstract interface TextDirectionHeuristicCompat {
     method public abstract boolean isRtl(char[], int, int);
     method public abstract boolean isRtl(java.lang.CharSequence, int, int);
@@ -1401,6 +1470,15 @@
     method public abstract void onActionProviderVisibilityChanged(boolean);
   }
 
+  public final class DisplayCutoutCompat {
+    ctor public DisplayCutoutCompat(android.graphics.Rect, java.util.List<android.graphics.Rect>);
+    method public java.util.List<android.graphics.Rect> getBoundingRects();
+    method public int getSafeInsetBottom();
+    method public int getSafeInsetLeft();
+    method public int getSafeInsetRight();
+    method public int getSafeInsetTop();
+  }
+
   public final class DragAndDropPermissionsCompat {
     method public void release();
   }
@@ -1908,6 +1986,7 @@
     method public static deprecated int getScaledPagingTouchSlop(android.view.ViewConfiguration);
     method public static float getScaledVerticalScrollFactor(android.view.ViewConfiguration, android.content.Context);
     method public static deprecated boolean hasPermanentMenuKey(android.view.ViewConfiguration);
+    method public static boolean shouldShowMenuShortcutsWhenKeyboardPresent(android.view.ViewConfiguration, android.content.Context);
   }
 
   public final class ViewGroupCompat {
@@ -2005,8 +2084,10 @@
 
   public class WindowInsetsCompat {
     ctor public WindowInsetsCompat(androidx.core.view.WindowInsetsCompat);
+    method public androidx.core.view.WindowInsetsCompat consumeDisplayCutout();
     method public androidx.core.view.WindowInsetsCompat consumeStableInsets();
     method public androidx.core.view.WindowInsetsCompat consumeSystemWindowInsets();
+    method public androidx.core.view.DisplayCutoutCompat getDisplayCutout();
     method public int getStableInsetBottom();
     method public int getStableInsetLeft();
     method public int getStableInsetRight();
@@ -2319,7 +2400,7 @@
     method public int getColumnSpan();
     method public int getRowIndex();
     method public int getRowSpan();
-    method public boolean isHeading();
+    method public deprecated boolean isHeading();
     method public boolean isSelected();
     method public static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat obtain(int, int, int, int, boolean, boolean);
     method public static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat obtain(int, int, int, int, boolean);
diff --git a/compat/res/values-as/strings.xml b/compat/res/values-as/strings.xml
new file mode 100644
index 0000000..b9c349e
--- /dev/null
+++ b/compat/res/values-as/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="status_bar_notification_info_overflow" msgid="7988687684186075107">"৯৯৯+"</string>
+</resources>
diff --git a/compat/res/values-or/strings.xml b/compat/res/values-or/strings.xml
new file mode 100644
index 0000000..f544aef
--- /dev/null
+++ b/compat/res/values-or/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="status_bar_notification_info_overflow" msgid="7988687684186075107">"999+"</string>
+</resources>
diff --git a/compat/res/values/dimens.xml b/compat/res/values/dimens.xml
index 1dcff5e..9ece458 100644
--- a/compat/res/values/dimens.xml
+++ b/compat/res/values/dimens.xml
@@ -70,4 +70,10 @@
 
     <!-- the paddingtop on the right side of the notification (for time etc.) -->
     <dimen name="notification_right_side_padding_top">2dp</dimen>
+
+    <!-- the maximum width of the large icon, above which it will be downscaled -->
+    <dimen name="compat_notification_large_icon_max_width">320dp</dimen>
+
+    <!-- the maximum height of the large icon, above which it will be downscaled -->
+    <dimen name="compat_notification_large_icon_max_height">320dp</dimen>
 </resources>
diff --git a/compat/src/androidTest/java/androidx/core/app/NotificationCompatTest.java b/compat/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
index a882891..32daade 100644
--- a/compat/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
+++ b/compat/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
@@ -30,6 +30,7 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import android.app.Notification;
 import android.content.Context;
@@ -528,6 +529,29 @@
     }
 
     @Test
+    public void testMessagingStyle_nullPerson() {
+        NotificationCompat.MessagingStyle messagingStyle =
+                new NotificationCompat.MessagingStyle("self name");
+        messagingStyle.addMessage("text", 200, (Person) null);
+
+        Notification notification = new NotificationCompat.Builder(mContext, "test id")
+                .setSmallIcon(1)
+                .setContentTitle("test title")
+                .setStyle(messagingStyle)
+                .build();
+
+        List<Message> result = NotificationCompat.MessagingStyle
+                .extractMessagingStyleFromNotification(notification)
+                .getMessages();
+
+        assertEquals(1, result.size());
+        assertEquals("text", result.get(0).getText());
+        assertEquals(200, result.get(0).getTimestamp());
+        assertNull(result.get(0).getPerson());
+        assertNull(result.get(0).getSender());
+    }
+
+    @Test
     public void testMessagingStyle_message() {
         NotificationCompat.MessagingStyle messagingStyle =
                 new NotificationCompat.MessagingStyle("self name");
@@ -560,6 +584,16 @@
     }
 
     @Test
+    public void testMessagingStyle_requiresNonEmptyUserName() {
+        try {
+            new NotificationCompat.MessagingStyle(new Person.Builder().build());
+            fail("Expected IllegalArgumentException about a non-empty user name.");
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+
+    @Test
     public void testMessagingStyle_isGroupConversation() {
         mContext.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.P;
         NotificationCompat.MessagingStyle messagingStyle =
@@ -683,19 +717,113 @@
         assertTrue(result.isGroupConversation());
     }
 
+    @SdkSuppress(minSdkVersion = 28)
     @Test
-    public void testMessagingStyle_extras() {
+    public void testMessagingStyle_applyNoTitleAndNotGroup() {
         NotificationCompat.MessagingStyle messagingStyle =
-                new NotificationCompat.MessagingStyle("test name")
+                new NotificationCompat.MessagingStyle("self name")
+                        .setGroupConversation(false)
+                        .addMessage(
+                                new Message(
+                                        "body",
+                                        1,
+                                        new Person.Builder().setName("example name").build()))
+                        .addMessage(new Message("body 2", 2, (Person) null));
+
+        Notification resultNotification = new NotificationCompat.Builder(mContext, "test id")
+                .setStyle(messagingStyle)
+                .build();
+        NotificationCompat.MessagingStyle resultCompatMessagingStyle =
+                NotificationCompat.MessagingStyle
+                        .extractMessagingStyleFromNotification(resultNotification);
+
+        // SDK >= 28 applies no title when none is provided to MessagingStyle.
+        assertNull(resultCompatMessagingStyle.getConversationTitle());
+        assertFalse(resultCompatMessagingStyle.isGroupConversation());
+    }
+
+    @SdkSuppress(minSdkVersion = 24, maxSdkVersion = 27)
+    @Test
+    public void testMessagingStyle_applyNoTitleAndNotGroup_legacy() {
+        NotificationCompat.MessagingStyle messagingStyle =
+                new NotificationCompat.MessagingStyle("self name")
+                        .setGroupConversation(false)
+                        .addMessage(
+                                new Message(
+                                        "body",
+                                        1,
+                                        new Person.Builder().setName("example name").build()))
+                        .addMessage(new Message("body 2", 2, (Person) null));
+
+        Notification resultNotification = new NotificationCompat.Builder(mContext, "test id")
+                .setStyle(messagingStyle)
+                .build();
+        NotificationCompat.MessagingStyle resultCompatMessagingStyle =
+                NotificationCompat.MessagingStyle
+                        .extractMessagingStyleFromNotification(resultNotification);
+
+        // SDK [24, 27] applies first incoming message sender name as Notification content title.
+        assertEquals("example name", NotificationCompat.getContentTitle(resultNotification));
+        assertNull(resultCompatMessagingStyle.getConversationTitle());
+        assertFalse(resultCompatMessagingStyle.isGroupConversation());
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    public void testMessagingStyle_applyConversationTitleAndNotGroup() {
+        NotificationCompat.MessagingStyle messagingStyle =
+                new NotificationCompat.MessagingStyle("self name")
+                        .setGroupConversation(false)
+                        .setConversationTitle("test title");
+
+        Notification resultNotification = new NotificationCompat.Builder(mContext, "test id")
+                .setStyle(messagingStyle)
+                .build();
+        NotificationCompat.MessagingStyle resultMessagingStyle =
+                NotificationCompat.MessagingStyle
+                        .extractMessagingStyleFromNotification(resultNotification);
+
+        // SDK >= 28 applies provided title to MessagingStyle.
+        assertEquals("test title", resultMessagingStyle.getConversationTitle());
+        assertFalse(resultMessagingStyle.isGroupConversation());
+    }
+
+    @SdkSuppress(minSdkVersion = 19, maxSdkVersion = 27)
+    @Test
+    public void testMessagingStyle_applyConversationTitleAndNotGroup_legacy() {
+        NotificationCompat.MessagingStyle messagingStyle =
+                new NotificationCompat.MessagingStyle("self name")
+                        .setGroupConversation(false)
+                        .setConversationTitle("test title");
+
+        Notification resultNotification = new NotificationCompat.Builder(mContext, "test id")
+                .setStyle(messagingStyle)
+                .build();
+        NotificationCompat.MessagingStyle resultMessagingStyle =
+                NotificationCompat.MessagingStyle
+                        .extractMessagingStyleFromNotification(resultNotification);
+
+        // SDK <= 27 applies MessagingStyle title as Notification content title.
+        assertEquals("test title", NotificationCompat.getContentTitle(resultNotification));
+        assertEquals("test title", resultMessagingStyle.getConversationTitle());
+        assertFalse(resultMessagingStyle.isGroupConversation());
+    }
+
+    @Test
+    public void testMessagingStyle_restoreFromCompatExtras() {
+        NotificationCompat.MessagingStyle messagingStyle =
+                new NotificationCompat.MessagingStyle(
+                        new Person.Builder().setName("test name").build())
                         .setGroupConversation(true);
         Bundle bundle = new Bundle();
         messagingStyle.addCompatExtras(bundle);
 
         NotificationCompat.MessagingStyle resultMessagingStyle =
-                new NotificationCompat.MessagingStyle("test name");
+                new NotificationCompat.MessagingStyle(new Person.Builder().setName("temp").build());
         resultMessagingStyle.restoreFromCompatExtras(bundle);
 
         assertTrue(resultMessagingStyle.isGroupConversation());
+        assertEquals("test name", resultMessagingStyle.getUser().getName());
     }
 
     @Test
@@ -763,6 +891,16 @@
         verifyInvisibleActionExists(notification);
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void getContentTitle() {
+        Notification notification = new NotificationCompat.Builder(mContext, "test channel")
+                .setContentTitle("example title")
+                .build();
+
+        assertEquals("example title", NotificationCompat.getContentTitle(notification));
+    }
+
     private static void verifyInvisibleActionExists(Notification notification) {
         List<NotificationCompat.Action> result =
                 NotificationCompat.getInvisibleActions(notification);
diff --git a/compat/src/androidTest/java/androidx/core/content/pm/PermissionInfoCompatTest.java b/compat/src/androidTest/java/androidx/core/content/pm/PermissionInfoCompatTest.java
new file mode 100644
index 0000000..79613be
--- /dev/null
+++ b/compat/src/androidTest/java/androidx/core/content/pm/PermissionInfoCompatTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.core.content.pm;
+
+import android.content.pm.PermissionInfo;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PermissionInfoCompatTest {
+    @Test
+    public void testGetProtectionAndFlags() {
+        PermissionInfo pi = new PermissionInfo();
+
+        pi.protectionLevel = PermissionInfo.PROTECTION_DANGEROUS
+                | PermissionInfo.PROTECTION_FLAG_PRIVILEGED;
+
+        Assert.assertEquals(PermissionInfo.PROTECTION_DANGEROUS,
+                PermissionInfoCompat.getProtection(pi));
+
+        Assert.assertEquals(PermissionInfo.PROTECTION_FLAG_PRIVILEGED,
+                PermissionInfoCompat.getProtectionFlags(pi));
+
+        pi.protectionLevel = 0xf | 0xfff0;
+        Assert.assertEquals(0xf, PermissionInfoCompat.getProtection(pi));
+        Assert.assertEquals(0xfff0, PermissionInfoCompat.getProtectionFlags(pi));
+    }
+}
diff --git a/compat/src/androidTest/java/androidx/core/graphics/drawable/IconCompatTest.java b/compat/src/androidTest/java/androidx/core/graphics/drawable/IconCompatTest.java
index 75ad1d5..2ca517f 100644
--- a/compat/src/androidTest/java/androidx/core/graphics/drawable/IconCompatTest.java
+++ b/compat/src/androidTest/java/androidx/core/graphics/drawable/IconCompatTest.java
@@ -32,6 +32,7 @@
 import android.graphics.drawable.AdaptiveIconDrawable;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -258,7 +259,7 @@
     public void testBitmapIconCompat_getType() {
         IconCompat icon = IconCompat.createWithBitmap(Bitmap.createBitmap(16, 16,
                 Bitmap.Config.ARGB_8888));
-        assertEquals(IconCompat.TYPE_BITMAP, icon.getType());
+        assertEquals(Icon.TYPE_BITMAP, icon.getType());
     }
 
     @Test
@@ -266,7 +267,7 @@
         byte[] data = new byte[4];
         data[0] = data[1] = data[2] = data[3] = (byte) 255;
         IconCompat icon = IconCompat.createWithData(data, 0, 4);
-        assertEquals(IconCompat.TYPE_DATA, icon.getType());
+        assertEquals(Icon.TYPE_DATA, icon.getType());
     }
 
     @Test
@@ -278,11 +279,11 @@
             String filePath = file.toURI().getPath();
 
             IconCompat icon = IconCompat.createWithContentUri(Uri.fromFile(file));
-            assertEquals(IconCompat.TYPE_URI, icon.getType());
+            assertEquals(Icon.TYPE_URI, icon.getType());
             assertEquals(filePath, icon.getUri().getPath());
 
             icon = IconCompat.createWithContentUri(file.toURI().toString());
-            assertEquals(IconCompat.TYPE_URI, icon.getType());
+            assertEquals(Icon.TYPE_URI, icon.getType());
             assertEquals(filePath, icon.getUri().getPath());
         } finally {
             file.delete();
@@ -292,7 +293,7 @@
     @Test
     public void testResourceIconCompat_getType() {
         IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.bmp_test);
-        assertEquals(IconCompat.TYPE_RESOURCE, icon.getType());
+        assertEquals(Icon.TYPE_RESOURCE, icon.getType());
         assertEquals("androidx.core.test", icon.getResPackage());
         assertEquals(R.drawable.bmp_test, icon.getResId());
     }
diff --git a/compat/src/androidTest/java/androidx/core/text/PrecomputedTextCompatTest.java b/compat/src/androidTest/java/androidx/core/text/PrecomputedTextCompatTest.java
new file mode 100644
index 0000000..a822b7c
--- /dev/null
+++ b/compat/src/androidTest/java/androidx/core/text/PrecomputedTextCompatTest.java
@@ -0,0 +1,302 @@
+/*
+ * 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.core.text;
+
+import static android.text.TextDirectionHeuristics.LTR;
+import static android.text.TextDirectionHeuristics.RTL;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.graphics.Color;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.text.Layout;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextDirectionHeuristics;
+import android.text.TextPaint;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.TypefaceSpan;
+
+import androidx.core.text.PrecomputedTextCompat.Params;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PrecomputedTextCompatTest {
+
+    private static final CharSequence NULL_CHAR_SEQUENCE = null;
+    private static final String STRING = "Hello, World!";
+    private static final String MULTIPARA_STRING = "Hello,\nWorld!";
+
+    private static final int SPAN_START = 3;
+    private static final int SPAN_END = 7;
+    private static final TypefaceSpan SPAN = new TypefaceSpan("serif");
+    private static final Spanned SPANNED;
+    static {
+        final SpannableStringBuilder ssb = new SpannableStringBuilder(STRING);
+        ssb.setSpan(SPAN, SPAN_START, SPAN_END, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        SPANNED = ssb;
+    }
+
+    private static final TextPaint PAINT = new TextPaint();
+
+    @Test
+    public void testParams_create() {
+        assertNotNull(new Params.Builder(PAINT).build());
+        assertNotNull(new Params.Builder(PAINT)
+                .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE).build());
+        assertNotNull(new Params.Builder(PAINT)
+                .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL).build());
+        assertNotNull(new Params.Builder(PAINT)
+                .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+                .setTextDirection(LTR).build());
+    }
+
+    @Test
+    public void testParams_SetGet() {
+        assertEquals(Layout.BREAK_STRATEGY_SIMPLE, new Params.Builder(PAINT)
+                .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE).build().getBreakStrategy());
+        assertEquals(Layout.HYPHENATION_FREQUENCY_NONE, new Params.Builder(PAINT)
+                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE).build()
+                .getHyphenationFrequency());
+        assertEquals(RTL, new Params.Builder(PAINT).setTextDirection(RTL).build()
+                .getTextDirection());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 23)
+    public void testParams_GetDefaultValues() {
+        assertEquals(Layout.BREAK_STRATEGY_HIGH_QUALITY,
+                new Params.Builder(PAINT).build().getBreakStrategy());
+        assertEquals(Layout.HYPHENATION_FREQUENCY_NORMAL,
+                new Params.Builder(PAINT).build().getHyphenationFrequency());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 18)
+    public void testParams_GetDefaultValues2() {
+        assertEquals(TextDirectionHeuristics.FIRSTSTRONG_LTR,
+                new Params.Builder(PAINT).build().getTextDirection());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 23)
+    public void testParams_equals() {
+        final Params base = new Params.Builder(PAINT)
+                .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
+                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+                .setTextDirection(LTR).build();
+
+        assertFalse(base.equals(null));
+        assertTrue(base.equals(base));
+        assertFalse(base.equals(new Object()));
+
+        Params other = new Params.Builder(PAINT)
+                .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
+                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+                .setTextDirection(LTR).build();
+        assertTrue(base.equals(other));
+        assertTrue(other.equals(base));
+        assertEquals(base.hashCode(), other.hashCode());
+
+        other = new Params.Builder(PAINT)
+                .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+                .setTextDirection(LTR).build();
+        assertFalse(base.equals(other));
+        assertFalse(other.equals(base));
+
+        other = new Params.Builder(PAINT)
+                .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
+                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
+                .setTextDirection(LTR).build();
+        assertFalse(base.equals(other));
+        assertFalse(other.equals(base));
+
+
+        other = new Params.Builder(PAINT)
+                .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
+                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+                .setTextDirection(RTL).build();
+        assertFalse(base.equals(other));
+        assertFalse(other.equals(base));
+
+
+        TextPaint anotherPaint = new TextPaint(PAINT);
+        anotherPaint.setTextSize(PAINT.getTextSize() * 2.0f);
+        other = new Params.Builder(anotherPaint)
+                .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
+                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+                .setTextDirection(LTR).build();
+        assertFalse(base.equals(other));
+        assertFalse(other.equals(base));
+    }
+
+    @Test
+    public void testParams_equals2() {
+        final Params base = new Params.Builder(PAINT).build();
+
+        assertFalse(base.equals(null));
+        assertTrue(base.equals(base));
+        assertFalse(base.equals(new Object()));
+
+        Params other = new Params.Builder(PAINT).build();
+        assertTrue(base.equals(other));
+        assertTrue(other.equals(base));
+        assertEquals(base.hashCode(), other.hashCode());
+
+        TextPaint paint = new TextPaint(PAINT);
+        paint.setTextSize(paint.getTextSize() * 2.0f + 1.0f);
+        other = new Params.Builder(paint).build();
+        assertFalse(base.equals(other));
+        assertFalse(other.equals(base));
+    }
+
+    @Test
+    public void testCreate_withNull() {
+        final Params param = new Params.Builder(PAINT).build();
+        try {
+            PrecomputedTextCompat.create(NULL_CHAR_SEQUENCE, param);
+            fail();
+        } catch (NullPointerException e) {
+            // pass
+        }
+        try {
+            PrecomputedTextCompat.create(STRING, null);
+            fail();
+        } catch (NullPointerException e) {
+            // pass
+        }
+    }
+
+    @Test
+    public void testCharSequenceInteface() {
+        final Params param = new Params.Builder(PAINT).build();
+        final CharSequence s = PrecomputedTextCompat.create(STRING, param);
+        assertEquals(STRING.length(), s.length());
+        assertEquals('H', s.charAt(0));
+        assertEquals('e', s.charAt(1));
+        assertEquals('l', s.charAt(2));
+        assertEquals('l', s.charAt(3));
+        assertEquals('o', s.charAt(4));
+        assertEquals(',', s.charAt(5));
+        assertEquals("Hello, World!", s.toString());
+
+        final CharSequence s3 = s.subSequence(0, 3);
+        assertEquals(3, s3.length());
+        assertEquals('H', s3.charAt(0));
+        assertEquals('e', s3.charAt(1));
+        assertEquals('l', s3.charAt(2));
+
+    }
+
+    @Test
+    public void testSpannedInterface_Spanned() {
+        final Params param = new Params.Builder(PAINT).build();
+        final Spanned s = PrecomputedTextCompat.create(SPANNED, param);
+        final TypefaceSpan[] spans = s.getSpans(0, s.length(), TypefaceSpan.class);
+        assertNotNull(spans);
+        assertEquals(1, spans.length);
+        assertEquals(SPAN, spans[0]);
+
+        assertEquals(SPAN_START, s.getSpanStart(SPAN));
+        assertEquals(SPAN_END, s.getSpanEnd(SPAN));
+        assertTrue((s.getSpanFlags(SPAN) & Spanned.SPAN_INCLUSIVE_EXCLUSIVE) != 0);
+
+        assertEquals(SPAN_START, s.nextSpanTransition(0, s.length(), TypefaceSpan.class));
+        assertEquals(SPAN_END, s.nextSpanTransition(SPAN_START, s.length(), TypefaceSpan.class));
+    }
+
+    @Test
+    public void testSpannedInterface_Spannable() {
+        final BackgroundColorSpan span = new BackgroundColorSpan(Color.RED);
+        final Params param = new Params.Builder(PAINT).build();
+        final Spannable s = PrecomputedTextCompat.create(STRING, param);
+        assertEquals(0, s.getSpans(0, s.length(), BackgroundColorSpan.class).length);
+
+        s.setSpan(span, SPAN_START, SPAN_END, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+
+        final BackgroundColorSpan[] spans = s.getSpans(0, s.length(), BackgroundColorSpan.class);
+        assertEquals(SPAN_START, s.getSpanStart(span));
+        assertEquals(SPAN_END, s.getSpanEnd(span));
+        assertTrue((s.getSpanFlags(span) & Spanned.SPAN_INCLUSIVE_EXCLUSIVE) != 0);
+
+        assertEquals(SPAN_START, s.nextSpanTransition(0, s.length(), BackgroundColorSpan.class));
+        assertEquals(SPAN_END,
+                s.nextSpanTransition(SPAN_START, s.length(), BackgroundColorSpan.class));
+
+        s.removeSpan(span);
+        assertEquals(0, s.getSpans(0, s.length(), BackgroundColorSpan.class).length);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testSpannedInterface_Spannable_setSpan_MetricsAffectingSpan() {
+        final Params param = new Params.Builder(PAINT).build();
+        final Spannable s = PrecomputedTextCompat.create(SPANNED, param);
+        s.setSpan(SPAN, SPAN_START, SPAN_END, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testSpannedInterface_Spannable_removeSpan_MetricsAffectingSpan() {
+        final Params param = new Params.Builder(PAINT).build();
+        final Spannable s = PrecomputedTextCompat.create(SPANNED, param);
+        s.removeSpan(SPAN);
+    }
+
+    @Test
+    public void testSpannedInterface_String() {
+        final Params param = new Params.Builder(PAINT).build();
+        final Spanned s = PrecomputedTextCompat.create(STRING, param);
+        TypefaceSpan[] spans = s.getSpans(0, s.length(), TypefaceSpan.class);
+        assertNotNull(spans);
+        assertEquals(0, spans.length);
+
+        assertEquals(-1, s.getSpanStart(SPAN));
+        assertEquals(-1, s.getSpanEnd(SPAN));
+        assertEquals(0, s.getSpanFlags(SPAN));
+
+        assertEquals(s.length(), s.nextSpanTransition(0, s.length(), TypefaceSpan.class));
+    }
+
+    @Test
+    public void testGetParagraphCount() {
+        final Params param = new Params.Builder(PAINT).build();
+        final PrecomputedTextCompat pm = PrecomputedTextCompat.create(STRING, param);
+        assertEquals(1, pm.getParagraphCount());
+        assertEquals(0, pm.getParagraphStart(0));
+        assertEquals(STRING.length(), pm.getParagraphEnd(0));
+
+        final PrecomputedTextCompat pm1 = PrecomputedTextCompat.create(MULTIPARA_STRING, param);
+        assertEquals(2, pm1.getParagraphCount());
+        assertEquals(0, pm1.getParagraphStart(0));
+        assertEquals(7, pm1.getParagraphEnd(0));
+        assertEquals(7, pm1.getParagraphStart(1));
+        assertEquals(pm1.length(), pm1.getParagraphEnd(1));
+    }
+
+}
diff --git a/compat/src/androidTest/java/androidx/core/text/util/FindAddressTest.java b/compat/src/androidTest/java/androidx/core/text/util/FindAddressTest.java
index 292f5b1..9cfa78a 100644
--- a/compat/src/androidTest/java/androidx/core/text/util/FindAddressTest.java
+++ b/compat/src/androidTest/java/androidx/core/text/util/FindAddressTest.java
@@ -138,6 +138,15 @@
     public void testFullAddressWithoutZipCode() {
         assertIsAddress("1600 Amphitheatre Parkway Mountain View, CA");
         assertIsAddress("201 S. Division St. Suite 500 Ann Arbor, MI");
+
+        // Check that addresses without a zip code are only accepted at the end of the string.
+        // This isn't implied by the documentation but was the case in the old implementation
+        // and fixing this bug creates a lot of false positives while fixing relatively few
+        // false negatives. In these examples, "one point" is parsed as a street and "as" is a
+        // state abbreviation (this is taken from a false positive reported in a bug).
+        Assert.assertTrue(containsAddress("one point I was as"));
+        Assert.assertTrue(containsAddress("At one point I was as ignorant as"));
+        Assert.assertFalse(containsAddress("At one point I was as ignorant as them"));
     }
 
     @Test
diff --git a/compat/src/androidTest/java/androidx/core/text/util/LinkifyCompatTest.java b/compat/src/androidTest/java/androidx/core/text/util/LinkifyCompatTest.java
index 8435df1..4400d22 100644
--- a/compat/src/androidTest/java/androidx/core/text/util/LinkifyCompatTest.java
+++ b/compat/src/androidTest/java/androidx/core/text/util/LinkifyCompatTest.java
@@ -781,12 +781,6 @@
     // ADDRESS RELATED TESTS
 
     @Test
-    public void testFindAddress_withoutZipcode() {
-        final String address = "455 LARKSPUR DRIVE CALIFORNIA SPRINGS CALIFORNIA";
-        verifyAddLinksWithMapAddressSucceeds("Should match map address: " + address, address);
-    }
-
-    @Test
     public void testFindAddress_withZipcode() {
         final String address = "455 LARKSPUR DRIVE CALIFORNIA SPRINGS CALIFORNIA 92826";
         verifyAddLinksWithMapAddressSucceeds("Should match map address: " + address, address);
diff --git a/compat/src/androidTest/java/androidx/core/view/DisplayCutoutCompatTest.java b/compat/src/androidTest/java/androidx/core/view/DisplayCutoutCompatTest.java
new file mode 100644
index 0000000..88a31f6
--- /dev/null
+++ b/compat/src/androidTest/java/androidx/core/view/DisplayCutoutCompatTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.core.view;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+
+import android.graphics.Rect;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DisplayCutoutCompatTest {
+
+    DisplayCutoutCompat mCutoutTop;
+    DisplayCutoutCompat mCutoutTopBottom;
+    DisplayCutoutCompat mCutoutTopBottomClone;
+    DisplayCutoutCompat mCutoutLeftRight;
+
+    @Before
+    public void setUp() throws Exception {
+        mCutoutTop = new DisplayCutoutCompat(new Rect(0, 10, 0, 0), Arrays.asList(
+                new Rect(50, 0, 60, 10)));
+        mCutoutTopBottom = new DisplayCutoutCompat(new Rect(0, 10, 0, 20), Arrays.asList(
+                new Rect(50, 0, 60, 10),
+                new Rect(50, 100, 60, 120)));
+        mCutoutTopBottomClone = new DisplayCutoutCompat(new Rect(0, 10, 0, 20), Arrays.asList(
+                new Rect(50, 0, 60, 10),
+                new Rect(50, 100, 60, 120)));
+        mCutoutLeftRight = new DisplayCutoutCompat(new Rect(30, 0, 40, 0), Arrays.asList(
+                new Rect(0, 50, 30, 60),
+                new Rect(100, 60, 140, 50)));
+    }
+
+    @Test
+    public void testSafeInsets() {
+        if (SDK_INT >= 28) {
+            assertEquals("left", 30, mCutoutLeftRight.getSafeInsetLeft());
+            assertEquals("top", 10, mCutoutTopBottom.getSafeInsetTop());
+            assertEquals("right", 40, mCutoutLeftRight.getSafeInsetRight());
+            assertEquals("bottom", 20, mCutoutTopBottom.getSafeInsetBottom());
+        } else {
+            assertEquals("left", 0, mCutoutLeftRight.getSafeInsetLeft());
+            assertEquals("top", 0, mCutoutTopBottom.getSafeInsetTop());
+            assertEquals("right", 0, mCutoutLeftRight.getSafeInsetRight());
+            assertEquals("bottom", 0, mCutoutTopBottom.getSafeInsetBottom());
+        }
+    }
+
+    @Test
+    public void testBoundingRects() {
+        if (SDK_INT >= 28) {
+            assertEquals(Arrays.asList(new Rect(50, 0, 60, 10)), mCutoutTop.getBoundingRects());
+        } else {
+            assertNull(mCutoutTop.getBoundingRects());
+        }
+    }
+
+    @Test
+    public void testEquals() {
+        assertEquals(mCutoutTopBottomClone, mCutoutTopBottom);
+
+        if (SDK_INT >= 28) {
+            assertNotEquals(mCutoutTopBottom, mCutoutLeftRight);
+        }
+    }
+    @Test
+    public void testHashCode() {
+        assertEquals(mCutoutTopBottomClone.hashCode(), mCutoutTopBottom.hashCode());
+    }
+}
diff --git a/compat/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java b/compat/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java
index 7b862c2..3e668b4 100644
--- a/compat/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java
+++ b/compat/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java
@@ -19,9 +19,9 @@
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.core.IsEqual.equalTo;
 import static org.junit.Assert.assertThat;
-import static org.junit.Assume.assumeTrue;
 
 import android.os.Build;
+import android.support.test.filters.SdkSuppress;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -68,9 +68,9 @@
         assertThat(nodeCompat.getTooltipText(), equalTo(tooltipText));
     }
 
+    @SdkSuppress(minSdkVersion = 19)
     @Test
     public void testGetSetShowingHintText() {
-        assumeTrue(Build.VERSION.SDK_INT >= 19);
         AccessibilityNodeInfoCompat nodeCompat = obtainedWrappedNodeCompat();
         nodeCompat.setShowingHintText(true);
         assertThat(nodeCompat.isShowingHintText(), is(true));
@@ -78,9 +78,9 @@
         assertThat(nodeCompat.isShowingHintText(), is(false));
     }
 
+    @SdkSuppress(minSdkVersion = 19)
     @Test
     public void testGetSetScreenReaderFocusable() {
-        assumeTrue(Build.VERSION.SDK_INT >= 19);
         AccessibilityNodeInfoCompat nodeCompat = obtainedWrappedNodeCompat();
         nodeCompat.setScreenReaderFocusable(true);
         assertThat(nodeCompat.isScreenReaderFocusable(), is(true));
@@ -88,14 +88,18 @@
         assertThat(nodeCompat.isScreenReaderFocusable(), is(false));
     }
 
+    @SdkSuppress(minSdkVersion = 19)
     @Test
     public void testGetSetHeading() {
-        assumeTrue(Build.VERSION.SDK_INT >= 19);
         AccessibilityNodeInfoCompat nodeCompat = obtainedWrappedNodeCompat();
         nodeCompat.setHeading(true);
         assertThat(nodeCompat.isHeading(), is(true));
         nodeCompat.setHeading(false);
         assertThat(nodeCompat.isHeading(), is(false));
+        AccessibilityNodeInfoCompat.CollectionItemInfoCompat collectionItemInfo =
+                AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(0, 1, 0, 1, true);
+        nodeCompat.setCollectionItemInfo(collectionItemInfo);
+        assertThat(nodeCompat.isHeading(), is(true));
     }
 
     private AccessibilityNodeInfoCompat obtainedWrappedNodeCompat() {
diff --git a/compat/src/main/AndroidManifest.xml b/compat/src/main/AndroidManifest.xml
index d18ee92..6c4338d 100644
--- a/compat/src/main/AndroidManifest.xml
+++ b/compat/src/main/AndroidManifest.xml
@@ -13,4 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<manifest package="androidx.core"/>
+<manifest package="androidx.core" xmlns:android="http://schemas.android.com/apk/res/android">
+    <application
+        android:appComponentFactory="androidx.core.app.CoreComponentFactory" />
+</manifest>
diff --git a/compat/src/main/java/androidx/core/app/AppComponentFactory.java b/compat/src/main/java/androidx/core/app/AppComponentFactory.java
new file mode 100644
index 0000000..b6383da
--- /dev/null
+++ b/compat/src/main/java/androidx/core/app/AppComponentFactory.java
@@ -0,0 +1,197 @@
+/*
+ * 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.core.app;
+
+import static androidx.core.app.CoreComponentFactory.checkCompatWrapper;
+
+import android.app.Activity;
+import android.app.Application;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ContentProvider;
+import android.content.Intent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Version of {@link android.app.AppComponentFactory} that works with androidx libraries.
+ *
+ * Note: This will only work on API 28+ and does not backport AppComponentFactory functionality.
+ */
+@RequiresApi(28)
+public class AppComponentFactory extends android.app.AppComponentFactory {
+
+    /**
+     * @see #instantiateActivityCompat
+     */
+    @Override
+    public final Activity instantiateActivity(ClassLoader cl, String className, Intent intent)
+            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+        return checkCompatWrapper(instantiateActivityCompat(cl, className, intent));
+    }
+
+    /**
+     * @see #instantiateApplicationCompat
+     */
+    @Override
+    public final Application instantiateApplication(ClassLoader cl, String className)
+            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+        return checkCompatWrapper(instantiateApplicationCompat(cl, className));
+    }
+
+    /**
+     * @see #instantiateReceiverCompat
+     */
+    @Override
+    public final BroadcastReceiver instantiateReceiver(ClassLoader cl, String className,
+            Intent intent)
+            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+        return checkCompatWrapper(instantiateReceiverCompat(cl, className, intent));
+    }
+
+    /**
+     * @see #instantiateProviderCompat
+     */
+    @Override
+    public final ContentProvider instantiateProvider(ClassLoader cl, String className)
+            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+        return checkCompatWrapper(instantiateProviderCompat(cl, className));
+    }
+
+    /**
+     * @see #instantiateServiceCompat
+     */
+    @Override
+    public final Service instantiateService(ClassLoader cl, String className, Intent intent)
+            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+        return checkCompatWrapper(instantiateServiceCompat(cl, className, intent));
+    }
+
+    /**
+     * Allows application to override the creation of the application object. This can be used to
+     * perform things such as dependency injection or class loader changes to these
+     * classes.
+     * <p>
+     * This method is only intended to provide a hook for instantiation. It does not provide
+     * earlier access to the Application object. The returned object will not be initialized
+     * as a Context yet and should not be used to interact with other android APIs.
+     *
+     * @param cl        The default classloader to use for instantiation.
+     * @param className The class to be instantiated.
+     */
+    public @NonNull Application instantiateApplicationCompat(@NonNull ClassLoader cl,
+            @NonNull String className)
+            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+        try {
+            return (Application) cl.loadClass(className).getDeclaredConstructor().newInstance();
+        } catch (InvocationTargetException | NoSuchMethodException e) {
+            throw new RuntimeException("Couldn't call constructor", e);
+        }
+    }
+
+    /**
+     * Allows application to override the creation of activities. This can be used to
+     * perform things such as dependency injection or class loader changes to these
+     * classes.
+     * <p>
+     * This method is only intended to provide a hook for instantiation. It does not provide
+     * earlier access to the Activity object. The returned object will not be initialized
+     * as a Context yet and should not be used to interact with other android APIs.
+     *
+     * @param cl        The default classloader to use for instantiation.
+     * @param className The class to be instantiated.
+     * @param intent    Intent creating the class.
+     */
+    public @NonNull Activity instantiateActivityCompat(@NonNull ClassLoader cl,
+            @NonNull String className, @Nullable Intent intent)
+            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+        try {
+            return (Activity) cl.loadClass(className).getDeclaredConstructor().newInstance();
+        } catch (InvocationTargetException | NoSuchMethodException e) {
+            throw new RuntimeException("Couldn't call constructor", e);
+        }
+    }
+
+    /**
+     * Allows application to override the creation of receivers. This can be used to
+     * perform things such as dependency injection or class loader changes to these
+     * classes.
+     *
+     * @param cl        The default classloader to use for instantiation.
+     * @param className The class to be instantiated.
+     * @param intent    Intent creating the class.
+     */
+    public @NonNull BroadcastReceiver instantiateReceiverCompat(@NonNull ClassLoader cl,
+            @NonNull String className, @Nullable Intent intent)
+            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+        try {
+            return (BroadcastReceiver) cl.loadClass(className).getDeclaredConstructor()
+                    .newInstance();
+        } catch (InvocationTargetException | NoSuchMethodException e) {
+            throw new RuntimeException("Couldn't call constructor", e);
+        }
+    }
+
+    /**
+     * Allows application to override the creation of services. This can be used to
+     * perform things such as dependency injection or class loader changes to these
+     * classes.
+     * <p>
+     * This method is only intended to provide a hook for instantiation. It does not provide
+     * earlier access to the Service object. The returned object will not be initialized
+     * as a Context yet and should not be used to interact with other android APIs.
+     *
+     * @param cl        The default classloader to use for instantiation.
+     * @param className The class to be instantiated.
+     * @param intent    Intent creating the class.
+     */
+    public @NonNull Service instantiateServiceCompat(@NonNull ClassLoader cl,
+            @NonNull String className, @Nullable Intent intent)
+            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+        try {
+            return (Service) cl.loadClass(className).getDeclaredConstructor().newInstance();
+        } catch (InvocationTargetException | NoSuchMethodException e) {
+            throw new RuntimeException("Couldn't call constructor", e);
+        }
+    }
+
+    /**
+     * Allows application to override the creation of providers. This can be used to
+     * perform things such as dependency injection or class loader changes to these
+     * classes.
+     * <p>
+     * This method is only intended to provide a hook for instantiation. It does not provide
+     * earlier access to the ContentProvider object. The returned object will not be initialized
+     * with a Context yet and should not be used to interact with other android APIs.
+     *
+     * @param cl        The default classloader to use for instantiation.
+     * @param className The class to be instantiated.
+     */
+    public @NonNull ContentProvider instantiateProviderCompat(@NonNull ClassLoader cl,
+            @NonNull String className)
+            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+        try {
+            return (ContentProvider) cl.loadClass(className).getDeclaredConstructor().newInstance();
+        } catch (InvocationTargetException | NoSuchMethodException e) {
+            throw new RuntimeException("Couldn't call constructor", e);
+        }
+    }
+}
diff --git a/compat/src/main/java/androidx/core/app/CoreComponentFactory.java b/compat/src/main/java/androidx/core/app/CoreComponentFactory.java
new file mode 100644
index 0000000..b55f03f
--- /dev/null
+++ b/compat/src/main/java/androidx/core/app/CoreComponentFactory.java
@@ -0,0 +1,113 @@
+/*
+ * 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.core.app;
+
+import android.app.Activity;
+import android.app.AppComponentFactory;
+import android.app.Application;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ContentProvider;
+import android.content.Intent;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Instance of AppComponentFactory for support libraries.
+ * @see CompatWrapped
+ * @hide
+ */
+@RequiresApi(api = 28)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class CoreComponentFactory extends AppComponentFactory {
+    private static final String TAG = "CoreComponentFactory";
+
+    @Override
+    public Activity instantiateActivity(ClassLoader cl, String className, Intent intent)
+            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+        return checkCompatWrapper(super.instantiateActivity(cl, className, intent));
+    }
+
+    @Override
+    public Application instantiateApplication(ClassLoader cl, String className)
+            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+        return checkCompatWrapper(super.instantiateApplication(cl, className));
+    }
+
+    @Override
+    public BroadcastReceiver instantiateReceiver(ClassLoader cl, String className,
+            Intent intent)
+            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+        return checkCompatWrapper(super.instantiateReceiver(cl, className, intent));
+    }
+
+    @Override
+    public ContentProvider instantiateProvider(ClassLoader cl, String className)
+            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+        return checkCompatWrapper(super.instantiateProvider(cl, className));
+    }
+
+    @Override
+    public Service instantiateService(ClassLoader cl, String className, Intent intent)
+            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+        return checkCompatWrapper(super.instantiateService(cl, className, intent));
+    }
+
+    static <T> T checkCompatWrapper(T obj) {
+        if (obj instanceof CompatWrapped) {
+            T wrapper = (T) ((CompatWrapped) obj).getWrapper();
+            if (wrapper != null) {
+                return wrapper;
+            }
+        }
+        return obj;
+    }
+
+    /**
+     * Implement this interface to allow a different class to be returned when instantiating
+     * on certain API levels.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public interface CompatWrapped {
+        /**
+         * Called while this class is being instantiated by the OS.
+         *
+         * If an object is returned then it will be used in place of the class.
+         * Note: this will not be called on API <= 27.
+         *
+         * Example:
+         * <pre class="prettyprint">
+         * {@literal
+         * public AndroidXContentProvider extends ContentProvider implements CompatWrapped {
+         *     ...
+         *
+         *     public Object getWrapper() {
+         *         if (SDK_INT >= 29) {
+         *             return new AndroidXContentProviderV29(this);
+         *         }
+         *         return null;
+         *     }
+         * }
+         * }
+         * </pre>
+         */
+        Object getWrapper();
+    }
+
+}
diff --git a/compat/src/main/java/androidx/core/app/NotificationCompat.java b/compat/src/main/java/androidx/core/app/NotificationCompat.java
index caf94e6..4d7bf40 100644
--- a/compat/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/compat/src/main/java/androidx/core/app/NotificationCompat.java
@@ -386,13 +386,17 @@
 
     /**
      * Notification key: the username to be displayed for all messages sent by the user
-     * including
-     * direct replies
-     * {@link MessagingStyle} notification.
+     * including direct replies {@link MessagingStyle} notification.
      */
     public static final String EXTRA_SELF_DISPLAY_NAME = "android.selfDisplayName";
 
     /**
+     * Notification key: the person to display for all messages sent by the user, including direct
+     * replies to {@link MessagingStyle} notifications.
+     */
+    public static final String EXTRA_MESSAGING_STYLE_USER = "android.messagingStyleUser";
+
+    /**
      * Notification key: a {@link String} to be displayed as the title to a conversation
      * represented by a {@link MessagingStyle}
      */
@@ -963,11 +967,39 @@
          * Set the large icon that is shown in the ticker and notification.
          */
         public Builder setLargeIcon(Bitmap icon) {
-            mLargeIcon = icon;
+            mLargeIcon = reduceLargeIconSize(icon);
             return this;
         }
 
         /**
+         * Reduce the size of a notification icon if it's overly large. The framework does
+         * this automatically starting from API 27.
+         */
+        private Bitmap reduceLargeIconSize(Bitmap icon) {
+            if (icon == null || Build.VERSION.SDK_INT >= 27) {
+                return icon;
+            }
+
+            Resources res = mContext.getResources();
+            int maxWidth =
+                    res.getDimensionPixelSize(R.dimen.compat_notification_large_icon_max_width);
+            int maxHeight =
+                    res.getDimensionPixelSize(R.dimen.compat_notification_large_icon_max_height);
+            if (icon.getWidth() <= maxWidth && icon.getHeight() <= maxHeight) {
+                return icon;
+            }
+
+            double scale = Math.min(
+                    maxWidth / (double) Math.max(1, icon.getWidth()),
+                    maxHeight / (double) Math.max(1, icon.getHeight()));
+            return Bitmap.createScaledBitmap(
+                    icon,
+                    (int) Math.ceil(icon.getWidth() * scale),
+                    (int) Math.ceil(icon.getHeight() * scale),
+                    true /* filtered */);
+        }
+
+        /**
          * Set the sound to play.  It will play on the default stream.
          *
          * <p>
@@ -1644,6 +1676,9 @@
         }
 
         /**
+         * Applies the compat style data to the framework {@link Notification} in a backwards
+         * compatible way. All other data should be stored within the Notification's extras.
+         *
          * @hide
          */
         @RestrictTo(LIBRARY_GROUP)
@@ -2123,29 +2158,54 @@
          */
         public static final int MAXIMUM_RETAINED_MESSAGES = 25;
 
-        CharSequence mUserDisplayName;
-        @Nullable CharSequence mConversationTitle;
-        List<Message> mMessages = new ArrayList<>();
-        @Nullable Boolean mIsGroupConversation;
+        private final List<Message> mMessages = new ArrayList<>();
+        private Person mUser;
+        private @Nullable CharSequence mConversationTitle;
+        private @Nullable Boolean mIsGroupConversation;
 
-        MessagingStyle() {
-        }
+        /** Private empty constructor for {@link Style#restoreFromCompatExtras(Bundle)}. */
+        private MessagingStyle() {}
 
         /**
          * @param userDisplayName Required - the name to be displayed for any replies sent by the
          * user before the posting app reposts the notification with those messages after they've
          * been actually sent and in previous messages sent by the user added in
          * {@link #addMessage(Message)}
+         * @deprecated Use {@code #MessagingStyle(Person)} instead.
          */
+        @Deprecated
         public MessagingStyle(@NonNull CharSequence userDisplayName) {
-            mUserDisplayName = userDisplayName;
+            mUser = new Person.Builder().setName(userDisplayName).build();
         }
 
         /**
-         * Returns the name to be displayed for any replies sent by the user
+         * Creates a new {@link MessagingStyle} object. Note that {@link Person} must have a
+         * non-empty name.
+         *
+         * @param user This {@link Person}'s name will be shown when this app's notification is
+         * being replied to. It's used temporarily so the app has time to process the send request
+         * and repost the notification with updates to the conversation.
          */
+        public MessagingStyle(@NonNull Person user) {
+            if (TextUtils.isEmpty(user.getName())) {
+                throw new IllegalArgumentException("User's name must not be empty.");
+            }
+            mUser = user;
+        }
+
+        /**
+         * Returns the name to be displayed for any replies sent by the user.
+         *
+         * @deprecated Use {@link #getUser()} instead.
+         */
+        @Deprecated
         public CharSequence getUserDisplayName() {
-            return mUserDisplayName;
+            return mUser.getName();
+        }
+
+        /** Returns the person to be used for any replies sent by the user. */
+        public Person getUser() {
+            return mUser;
         }
 
         /**
@@ -2286,19 +2346,20 @@
          */
         public static MessagingStyle extractMessagingStyleFromNotification(
                 Notification notification) {
-            MessagingStyle style;
             Bundle extras = NotificationCompat.getExtras(notification);
-            if (extras != null && !extras.containsKey(EXTRA_SELF_DISPLAY_NAME)) {
-                style = null;
-            } else {
-                try {
-                    style = new MessagingStyle();
-                    style.restoreFromCompatExtras(extras);
-                } catch (ClassCastException e) {
-                    style = null;
-                }
+            if (extras != null
+                    && !extras.containsKey(EXTRA_SELF_DISPLAY_NAME)
+                    && !extras.containsKey(EXTRA_MESSAGING_STYLE_USER)) {
+                return null;
             }
-            return style;
+
+            try {
+                MessagingStyle style = new MessagingStyle();
+                style.restoreFromCompatExtras(extras);
+                return style;
+            } catch (ClassCastException e) {
+                return null;
+            }
         }
 
         /**
@@ -2315,14 +2376,36 @@
 
             if (Build.VERSION.SDK_INT >= 24) {
                 Notification.MessagingStyle style =
-                        new Notification.MessagingStyle(mUserDisplayName)
-                                .setConversationTitle(mConversationTitle);
+                        new Notification.MessagingStyle(mUser.getName());
+
+                // In SDK < 28, base Android will assume a MessagingStyle notification is a group
+                // chat if the conversation title is set. In compat, this isn't the case as we've
+                // introduced #setGroupConversation. When we apply these settings to base Android
+                // notifications, we should only set base Android's MessagingStyle conversation
+                // title if it's a group conversation OR SDK >= 28. Otherwise we set the
+                // Notification content title so Android won't think it's a group conversation.
+                if (isGroupConversation() || Build.VERSION.SDK_INT >= 28) {
+                    // If group or non-legacy, set MessagingStyle#mConversationTitle.
+                    style.setConversationTitle(mConversationTitle);
+                } else {
+                    // Otherwise set Notification#mContentTitle.
+                    builder.getBuilder().setContentTitle(mConversationTitle);
+                }
+
+                // For SDK >= 28, we can simply denote the group conversation status regardless of
+                // if we set the conversation title or not.
+                if (Build.VERSION.SDK_INT >= 28) {
+                    style.setGroupConversation(mIsGroupConversation);
+                }
+
                 for (MessagingStyle.Message message : mMessages) {
+                    CharSequence name = null;
+                    if (message.getPerson() != null) {
+                        name = message.getPerson().getName();
+                    }
                     Notification.MessagingStyle.Message frameworkMessage =
                             new Notification.MessagingStyle.Message(
-                                    message.getText(),
-                                    message.getTimestamp(),
-                                    message.getSender());
+                                    message.getText(), message.getTimestamp(), name);
                     if (message.getDataMimeType() != null) {
                         frameworkMessage.setData(message.getDataMimeType(), message.getDataUri());
                     }
@@ -2335,7 +2418,11 @@
                 if (mConversationTitle != null) {
                     builder.getBuilder().setContentTitle(mConversationTitle);
                 } else if (latestIncomingMessage != null) {
-                    builder.getBuilder().setContentTitle(latestIncomingMessage.getSender());
+                    builder.getBuilder().setContentTitle("");
+                    if (latestIncomingMessage.getPerson() != null) {
+                        builder.getBuilder().setContentTitle(
+                                latestIncomingMessage.getPerson().getName());
+                    }
                 }
                 // Set the text
                 if (latestIncomingMessage != null) {
@@ -2369,7 +2456,8 @@
             for (int i = mMessages.size() - 1; i >= 0; i--) {
                 MessagingStyle.Message message = mMessages.get(i);
                 // Incoming messages have a non-empty sender.
-                if (!TextUtils.isEmpty(message.getSender())) {
+                if (message.getPerson() != null
+                        && !TextUtils.isEmpty(message.getPerson().getName())) {
                     return message;
                 }
             }
@@ -2383,7 +2471,7 @@
         private boolean hasMessagesWithoutSender() {
             for (int i = mMessages.size() - 1; i >= 0; i--) {
                 MessagingStyle.Message message = mMessages.get(i);
-                if (message.getSender() == null) {
+                if (message.getPerson() != null && message.getPerson().getName() == null) {
                     return true;
                 }
             }
@@ -2395,10 +2483,10 @@
             SpannableStringBuilder sb = new SpannableStringBuilder();
             final boolean afterLollipop = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
             int color = afterLollipop ? Color.BLACK : Color.WHITE;
-            CharSequence replyName = message.getSender();
-            if (TextUtils.isEmpty(message.getSender())) {
-                replyName = mUserDisplayName == null
-                        ? "" : mUserDisplayName;
+            CharSequence replyName =
+                    message.getPerson() == null ? "" : message.getPerson().getName();
+            if (TextUtils.isEmpty(replyName)) {
+                replyName = mUser.getName();
                 color = afterLollipop && mBuilder.getColor() != NotificationCompat.COLOR_DEFAULT
                         ? mBuilder.getColor()
                         : color;
@@ -2422,9 +2510,9 @@
         @Override
         public void addCompatExtras(Bundle extras) {
             super.addCompatExtras(extras);
-            if (mUserDisplayName != null) {
-                extras.putCharSequence(EXTRA_SELF_DISPLAY_NAME, mUserDisplayName);
-            }
+            extras.putCharSequence(EXTRA_SELF_DISPLAY_NAME, mUser.getName());
+            extras.putBundle(EXTRA_MESSAGING_STYLE_USER, mUser.toBundle());
+
             if (mConversationTitle != null) {
                 extras.putCharSequence(EXTRA_CONVERSATION_TITLE, mConversationTitle);
             }
@@ -2444,11 +2532,21 @@
         @Override
         protected void restoreFromCompatExtras(Bundle extras) {
             mMessages.clear();
-            mUserDisplayName = extras.getString(EXTRA_SELF_DISPLAY_NAME);
+            // Call to #restore requires that there either be a display name OR a user.
+            if (extras.containsKey(EXTRA_MESSAGING_STYLE_USER)) {
+                // New path simply unpacks Person, but checks if there's a valid name.
+                mUser = Person.fromBundle(extras.getBundle(EXTRA_MESSAGING_STYLE_USER));
+            } else {
+                // Legacy extra simply builds Person with a name.
+                mUser = new Person.Builder()
+                        .setName(extras.getString(EXTRA_SELF_DISPLAY_NAME))
+                        .build();
+            }
+
             mConversationTitle = extras.getString(EXTRA_CONVERSATION_TITLE);
             Parcelable[] parcelables = extras.getParcelableArray(EXTRA_MESSAGES);
             if (parcelables != null) {
-                mMessages = Message.getMessagesFromBundleArray(parcelables);
+                mMessages.addAll(Message.getMessagesFromBundleArray(parcelables));
             }
             if (extras.containsKey(EXTRA_IS_GROUP_CONVERSATION)) {
                 mIsGroupConversation = extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION);
@@ -2570,7 +2668,7 @@
             @Deprecated
             @Nullable
             public CharSequence getSender() {
-                return mPerson.getName();
+                return mPerson == null ? null : mPerson.getName();
             }
 
             /** Returns the {@link Person} sender of this message. */
@@ -2601,6 +2699,9 @@
                 }
                 bundle.putLong(KEY_TIMESTAMP, mTimestamp);
                 if (mPerson != null) {
+                    // We must add both as Frameworks depends on this extra directly in order to
+                    // render properly.
+                    bundle.putCharSequence(KEY_SENDER, mPerson.getName());
                     bundle.putBundle(KEY_PERSON, mPerson.toBundle());
                 }
                 if (mDataMimeType != null) {
@@ -2646,22 +2747,21 @@
                         return null;
                     }
 
-                    Message message;
-                    if (bundle.containsKey(KEY_SENDER)) {
-                        // Legacy sender
-                        message = new Message(
-                                bundle.getCharSequence(KEY_TEXT),
-                                bundle.getLong(KEY_TIMESTAMP),
-                                new Person.Builder()
-                                        .setName(bundle.getCharSequence(KEY_SENDER))
-                                        .build());
-                    } else {
-                        message = new Message(
-                                bundle.getCharSequence(KEY_TEXT),
-                                bundle.getLong(KEY_TIMESTAMP),
-                                Person.fromBundle(bundle.getBundle(KEY_PERSON)));
+                    Person person = null;
+                    if (bundle.containsKey(KEY_PERSON)) {
+                        person = Person.fromBundle(bundle.getBundle(KEY_PERSON));
+                    } else if (bundle.containsKey(KEY_SENDER)) {
+                        // Legacy person
+                        person = new Person.Builder()
+                                .setName(bundle.getCharSequence(KEY_SENDER))
+                                .build();
                     }
 
+                    Message message = new Message(
+                            bundle.getCharSequence(KEY_TEXT),
+                            bundle.getLong(KEY_TIMESTAMP),
+                            person);
+
                     if (bundle.containsKey(KEY_DATA_MIME_TYPE)
                             && bundle.containsKey(KEY_DATA_URI)) {
                         message.setData(bundle.getString(KEY_DATA_MIME_TYPE),
@@ -5005,6 +5105,12 @@
         return result;
     }
 
+    /** Returns the content title of a {@link Notification}. **/
+    @RequiresApi(19)
+    public static CharSequence getContentTitle(Notification notification) {
+        return notification.extras.getCharSequence(Notification.EXTRA_TITLE);
+    }
+
     /**
      * Get the category of this notification in a backwards compatible
      * manner.
diff --git a/compat/src/main/java/androidx/core/app/NotificationManagerCompat.java b/compat/src/main/java/androidx/core/app/NotificationManagerCompat.java
index 3660de3..e8b6188 100644
--- a/compat/src/main/java/androidx/core/app/NotificationManagerCompat.java
+++ b/compat/src/main/java/androidx/core/app/NotificationManagerCompat.java
@@ -267,7 +267,7 @@
             // Parse the string again if it is different from the last time this method was called.
             if (enabledNotificationListeners != null
                     && !enabledNotificationListeners.equals(sEnabledNotificationListeners)) {
-                final String[] components = enabledNotificationListeners.split(":");
+                final String[] components = enabledNotificationListeners.split(":", -1);
                 Set<String> packageNames = new HashSet<String>(components.length);
                 for (String component : components) {
                     ComponentName componentName = ComponentName.unflattenFromString(component);
diff --git a/compat/src/main/java/androidx/core/content/pm/PermissionInfoCompat.java b/compat/src/main/java/androidx/core/content/pm/PermissionInfoCompat.java
new file mode 100644
index 0000000..df095a6
--- /dev/null
+++ b/compat/src/main/java/androidx/core/content/pm/PermissionInfoCompat.java
@@ -0,0 +1,92 @@
+/*
+ * 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.core.content.pm;
+
+import android.annotation.SuppressLint;
+import android.content.pm.PermissionInfo;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.os.BuildCompat;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Helper for accessing features in {@link PermissionInfo}.
+ */
+public final class PermissionInfoCompat {
+    private PermissionInfoCompat() {
+    }
+
+    /** @hide */
+    @IntDef(flag = false, value = {
+            PermissionInfo.PROTECTION_NORMAL,
+            PermissionInfo.PROTECTION_DANGEROUS,
+            PermissionInfo.PROTECTION_SIGNATURE,
+            PermissionInfo.PROTECTION_SIGNATURE_OR_SYSTEM,
+    })
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Protection {}
+
+    /** @hide */
+    @SuppressLint("UniqueConstants") // because _SYSTEM and _PRIVILEGED are aliases.
+    @IntDef(flag = true, value = {
+            PermissionInfo.PROTECTION_FLAG_PRIVILEGED,
+            PermissionInfo.PROTECTION_FLAG_SYSTEM,
+            PermissionInfo.PROTECTION_FLAG_DEVELOPMENT,
+            PermissionInfo.PROTECTION_FLAG_APPOP,
+            PermissionInfo.PROTECTION_FLAG_PRE23,
+            PermissionInfo.PROTECTION_FLAG_INSTALLER,
+            PermissionInfo.PROTECTION_FLAG_VERIFIER,
+            PermissionInfo.PROTECTION_FLAG_PREINSTALLED,
+            PermissionInfo.PROTECTION_FLAG_SETUP,
+            PermissionInfo.PROTECTION_FLAG_INSTANT,
+            PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY,
+    })
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ProtectionFlags {}
+
+    /**
+     * Return the base permission type of a {@link PermissionInfo}.
+     */
+    @SuppressLint("WrongConstant") // for "PermissionInfo.PROTECTION_MASK_BASE"
+    @Protection
+    public static int getProtection(@NonNull PermissionInfo permissionInfo) {
+        if (BuildCompat.isAtLeastP()) {
+            return permissionInfo.getProtection();
+        } else {
+            return permissionInfo.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE;
+        }
+    }
+
+    /**
+     * Return the additional protection flags of a {@link PermissionInfo}.
+     */
+    @SuppressLint("WrongConstant") // for "~PermissionInfo.PROTECTION_MASK_BASE"
+    @ProtectionFlags
+    public static int getProtectionFlags(@NonNull PermissionInfo permissionInfo) {
+        if (BuildCompat.isAtLeastP()) {
+            return permissionInfo.getProtectionFlags();
+        } else {
+            return permissionInfo.protectionLevel & ~PermissionInfo.PROTECTION_MASK_BASE;
+        }
+    }
+}
diff --git a/compat/src/main/java/androidx/core/database/CursorWindowCompat.java b/compat/src/main/java/androidx/core/database/CursorWindowCompat.java
new file mode 100644
index 0000000..c675977
--- /dev/null
+++ b/compat/src/main/java/androidx/core/database/CursorWindowCompat.java
@@ -0,0 +1,51 @@
+/*
+ * 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.core.database;
+
+import android.database.CursorWindow;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.os.BuildCompat;
+
+/**
+ * Helper for accessing features in {@link android.database.CursorWindow}
+ */
+public final class CursorWindowCompat {
+
+    private CursorWindowCompat() {
+        /* Hide constructor */
+    }
+
+    /**
+     * Creates a CursorWindow of the specified size.
+     * <p>
+     * Prior to Android P, this method will return a CursorWindow of size defined by the platform.
+     */
+    @NonNull
+    public CursorWindow create(@Nullable String name, long windowSizeBytes) {
+        if (BuildCompat.isAtLeastP()) {
+            return new CursorWindow(name, windowSizeBytes);
+        } else if (Build.VERSION.SDK_INT >= 15) {
+            return new CursorWindow(name);
+        } else {
+            //noinspection deprecation
+            return new CursorWindow(false);
+        }
+    }
+}
diff --git a/compat/src/main/java/androidx/core/database/sqlite/SQLiteCursorCompat.java b/compat/src/main/java/androidx/core/database/sqlite/SQLiteCursorCompat.java
new file mode 100644
index 0000000..ebbc037
--- /dev/null
+++ b/compat/src/main/java/androidx/core/database/sqlite/SQLiteCursorCompat.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.database.sqlite;
+
+import android.database.sqlite.SQLiteCursor;
+
+import androidx.annotation.NonNull;
+import androidx.core.os.BuildCompat;
+
+/**
+ * Helper for accessing features in {@link android.database.AbstractWindowedCursor}
+ */
+public final class SQLiteCursorCompat {
+
+    private SQLiteCursorCompat() {
+        /* Hide constructor */
+    }
+
+    /**
+     * Controls whether the cursor is filled starting at the position passed to
+     * {@link SQLiteCursor#moveToPosition(int)}.
+     * <p>
+     * By default, SQLiteCursor will optimize for accesses around the requested row index by loading
+     * data on either side of it. Pass true to this method to disable that behavior, useful to
+     * optimize multi-window, continuous reads.
+     * <p>
+     * Prior to Android P, this method will do nothing.
+     */
+    public void setFillWindowForwardOnly(
+            @NonNull SQLiteCursor cursor, boolean fillWindowForwardOnly) {
+        if (BuildCompat.isAtLeastP()) {
+            cursor.setFillWindowForwardOnly(fillWindowForwardOnly);
+        }
+    }
+}
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 cd9968c..4c296ea 100644
--- a/compat/src/main/java/androidx/core/graphics/drawable/IconCompat.java
+++ b/compat/src/main/java/androidx/core/graphics/drawable/IconCompat.java
@@ -16,6 +16,12 @@
 
 package androidx.core.graphics.drawable;
 
+import static android.graphics.drawable.Icon.TYPE_ADAPTIVE_BITMAP;
+import static android.graphics.drawable.Icon.TYPE_BITMAP;
+import static android.graphics.drawable.Icon.TYPE_DATA;
+import static android.graphics.drawable.Icon.TYPE_RESOURCE;
+import static android.graphics.drawable.Icon.TYPE_URI;
+
 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
@@ -57,6 +63,7 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.core.content.ContextCompat;
 import androidx.core.content.res.ResourcesCompat;
+import androidx.core.os.BuildCompat;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -79,34 +86,6 @@
      */
     public static final int TYPE_UNKOWN = -1;
 
-    // TODO: Switch these to the public constants in the beta branch.
-    /**
-     * @hide
-     */
-    @RestrictTo(LIBRARY)
-    public static final int TYPE_BITMAP = 1;
-    /**
-     * @hide
-     */
-    @RestrictTo(LIBRARY)
-    public static final int TYPE_RESOURCE = 2;
-    /**
-     * @hide
-     */
-    @RestrictTo(LIBRARY)
-    public static final int TYPE_DATA = 3;
-    /**
-     * @hide
-     */
-    @RestrictTo(LIBRARY)
-    public static final int TYPE_URI = 4;
-    /**
-     * @hide
-     */
-    @RestrictTo(LIBRARY)
-    public static final int TYPE_ADAPTIVE_BITMAP = 5;
-
-
     /**
      * @hide
      */
@@ -590,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()}.
      */
@@ -645,8 +676,10 @@
      */
     @IconType
     @RequiresApi(23)
-    public static int getType(Icon icon) {
-        // TODO: Switch to public APIs on P+ in beta branch.
+    public static int getType(@NonNull Icon icon) {
+        if (BuildCompat.isAtLeastP()) {
+            return icon.getType();
+        }
         try {
             return (int) icon.getClass().getMethod("getType").invoke(icon);
         } catch (IllegalAccessException e) {
@@ -671,8 +704,10 @@
      */
     @Nullable
     @RequiresApi(23)
-    public static String getResPackage(Icon icon) {
-        // TODO: Switch to public APIs on P+ in beta branch.
+    public static String getResPackage(@NonNull Icon icon) {
+        if (BuildCompat.isAtLeastP()) {
+            return icon.getResPackage();
+        }
         try {
             return (String) icon.getClass().getMethod("getResPackage").invoke(icon);
         } catch (IllegalAccessException e) {
@@ -697,8 +732,10 @@
      */
     @IdRes
     @RequiresApi(23)
-    public static int getResId(Icon icon) {
-        // TODO: Switch to public APIs on P+ in beta branch.
+    public static int getResId(@NonNull Icon icon) {
+        if (BuildCompat.isAtLeastP()) {
+            return icon.getResId();
+        }
         try {
             return (int) icon.getClass().getMethod("getResId").invoke(icon);
         } catch (IllegalAccessException e) {
@@ -723,8 +760,10 @@
      */
     @Nullable
     @RequiresApi(23)
-    public Uri getUri(Icon icon) {
-        // TODO: Switch to public APIs on P+ in beta branch.
+    public Uri getUri(@NonNull Icon icon) {
+        if (BuildCompat.isAtLeastP()) {
+            return icon.getUri();
+        }
         try {
             return (Uri) icon.getClass().getMethod("getUri").invoke(icon);
         } catch (IllegalAccessException e) {
diff --git a/compat/src/main/java/androidx/core/os/BuildCompat.java b/compat/src/main/java/androidx/core/os/BuildCompat.java
index 277a724..19f85b5 100644
--- a/compat/src/main/java/androidx/core/os/BuildCompat.java
+++ b/compat/src/main/java/androidx/core/os/BuildCompat.java
@@ -81,14 +81,10 @@
     /**
      * Checks if the device is running on a pre-release version of Android P or newer.
      * <p>
-     * <strong>Note:</strong> This method will return {@code false} on devices running release
-     * versions of Android. When Android P is finalized for release, this method will be deprecated
-     * and all calls should be replaced with {@code Build.SDK_INT >= Build.VERSION_CODES.P}.
-     *
      * @return {@code true} if P APIs are available for use, {@code false} otherwise
      */
     public static boolean isAtLeastP() {
-        return VERSION.CODENAME.equals("P") || VERSION.CODENAME.equals("Q");
+        return VERSION.SDK_INT >= 28;
     }
 
     /**
@@ -101,6 +97,8 @@
      * @return {@code true} if Q APIs are available for use, {@code false} otherwise
      */
     public static boolean isAtLeastQ() {
-        return VERSION.CODENAME.equals("Q");
+        return VERSION.CODENAME.length() == 1
+            && VERSION.CODENAME.charAt(0) >= 'Q'
+            && VERSION.CODENAME.charAt(0) <= 'Z';
     }
 }
diff --git a/compat/src/main/java/androidx/core/os/LocaleHelper.java b/compat/src/main/java/androidx/core/os/LocaleHelper.java
index 001d657..5ce4c82 100644
--- a/compat/src/main/java/androidx/core/os/LocaleHelper.java
+++ b/compat/src/main/java/androidx/core/os/LocaleHelper.java
@@ -33,7 +33,7 @@
     // Simpleton implementation for Locale.forLanguageTag(...)
     static Locale forLanguageTag(String str) {
         if (str.contains("-")) {
-            String[] args = str.split("-");
+            String[] args = str.split("-", -1);
             if (args.length > 2) {
                 return new Locale(args[0], args[1], args[2]);
             } else if (args.length > 1) {
@@ -42,7 +42,7 @@
                 return new Locale(args[0]);
             }
         } else if (str.contains("_")) {
-            String[] args = str.split("_");
+            String[] args = str.split("_", -1);
             if (args.length > 2) {
                 return new Locale(args[0], args[1], args[2]);
             } else if (args.length > 1) {
diff --git a/compat/src/main/java/androidx/core/os/LocaleListCompat.java b/compat/src/main/java/androidx/core/os/LocaleListCompat.java
index a933877..0be1ff3 100644
--- a/compat/src/main/java/androidx/core/os/LocaleListCompat.java
+++ b/compat/src/main/java/androidx/core/os/LocaleListCompat.java
@@ -289,7 +289,7 @@
         if (list == null || list.isEmpty()) {
             return getEmptyLocaleList();
         } else {
-            final String[] tags = list.split(",");
+            final String[] tags = list.split(",", -1);
             final Locale[] localeArray = new Locale[tags.length];
             for (int i = 0; i < localeArray.length; i++) {
                 localeArray[i] = Build.VERSION.SDK_INT >= 21
diff --git a/compat/src/main/java/androidx/core/os/LocaleListHelper.java b/compat/src/main/java/androidx/core/os/LocaleListHelper.java
index 0a656bc..137d9cd 100644
--- a/compat/src/main/java/androidx/core/os/LocaleListHelper.java
+++ b/compat/src/main/java/androidx/core/os/LocaleListHelper.java
@@ -273,7 +273,7 @@
         if (list == null || list.isEmpty()) {
             return getEmptyLocaleList();
         } else {
-            final String[] tags = list.split(",");
+            final String[] tags = list.split(",", -1);
             final Locale[] localeArray = new Locale[tags.length];
             for (int i = 0; i < localeArray.length; i++) {
                 localeArray[i] = LocaleHelper.forLanguageTag(tags[i]);
diff --git a/compat/src/main/java/androidx/core/text/HtmlCompat.java b/compat/src/main/java/androidx/core/text/HtmlCompat.java
index b846805..a5b97ea 100644
--- a/compat/src/main/java/androidx/core/text/HtmlCompat.java
+++ b/compat/src/main/java/androidx/core/text/HtmlCompat.java
@@ -18,6 +18,8 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 import android.annotation.SuppressLint;
 import android.graphics.Color;
 import android.os.Build;
@@ -33,6 +35,8 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 
+import java.lang.annotation.Retention;
+
 /**
  * Backwards compatible version of {@link Html}.
  */
@@ -119,6 +123,7 @@
             FROM_HTML_MODE_LEGACY
     }, flag = true)
     @RestrictTo(LIBRARY)
+    @Retention(SOURCE)
     @interface FromHtmlFlags {
     }
 
@@ -128,6 +133,7 @@
             TO_HTML_PARAGRAPH_LINES_INDIVIDUAL
     })
     @RestrictTo(LIBRARY)
+    @Retention(SOURCE)
     @interface ToHtmlOptions {
     }
 
diff --git a/compat/src/main/java/androidx/core/text/PrecomputedTextCompat.java b/compat/src/main/java/androidx/core/text/PrecomputedTextCompat.java
new file mode 100644
index 0000000..dbf9ba3
--- /dev/null
+++ b/compat/src/main/java/androidx/core/text/PrecomputedTextCompat.java
@@ -0,0 +1,622 @@
+/*
+ * 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.core.text;
+
+import android.os.Build;
+import android.text.Layout;
+import android.text.PrecomputedText;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.StaticLayout;
+import android.text.TextDirectionHeuristic;
+import android.text.TextDirectionHeuristics;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.style.MetricAffectingSpan;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.os.BuildCompat;
+import androidx.core.util.ObjectsCompat;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+
+/**
+ * A text which has the character metrics data.
+ *
+ * A text object that contains the character metrics data and can be used to improve the performance
+ * of text layout operations. When a PrecomputedTextCompat is created with a given
+ * {@link CharSequence}, it will measure the text metrics during the creation. This PrecomputedText
+ * instance can be set on {@link android.widget.TextView} or {@link StaticLayout}. Since the text
+ * layout information will be included in this instance, {@link android.widget.TextView} or
+ * {@link StaticLayout} will not have to recalculate this information.
+ *
+ * On API 28 or later, there is full PrecomputedText support by framework. From API 21 to API 27,
+ * PrecomputedTextCompat relies on internal text layout cache. PrecomputedTextCompat immediately
+ * computes the text layout in the constuctor to warm up the internal text layout cache. On API 20
+ * or before, PrecomputedTextCompat does nothing.
+ *
+ * Note that any {@link android.text.NoCopySpan} attached to the original text won't be passed to
+ * PrecomputedText.
+ */
+public class PrecomputedTextCompat implements Spannable {
+    private static final char LINE_FEED = '\n';
+
+    /**
+     * The information required for building {@link PrecomputedTextCompat}.
+     *
+     * Contains information required for precomputing text measurement metadata, so it can be done
+     * in isolation of a {@link android.widget.TextView} or {@link StaticLayout}, when final layout
+     * constraints are not known.
+     */
+    public static final class Params {
+        private final @NonNull TextPaint mPaint;
+
+        // null on API 17 or before, non null on API 18 or later.
+        private final @Nullable TextDirectionHeuristic mTextDir;
+
+        private final int mBreakStrategy;
+
+        private final int mHyphenationFrequency;
+
+        private final PrecomputedText.Params mWrapped;
+
+        /**
+         * A builder for creating {@link Params}.
+         */
+        public static class Builder {
+            // The TextPaint used for measurement.
+            private final @NonNull TextPaint mPaint;
+
+            // The requested text direction.
+            private TextDirectionHeuristic mTextDir;
+
+            // The break strategy for this measured text.
+            private int mBreakStrategy;
+
+            // The hyphenation frequency for this measured text.
+            private int mHyphenationFrequency;
+
+            /**
+             * Builder constructor.
+             *
+             * @param paint the paint to be used for drawing
+             */
+            public Builder(@NonNull TextPaint paint) {
+                mPaint = paint;
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                    mBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
+                    mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NORMAL;
+                } else {
+                    mBreakStrategy = mHyphenationFrequency = 0;
+                }
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+                    mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
+                } else {
+                    mTextDir = null;
+                }
+            }
+
+            /**
+             * Set the line break strategy.
+             *
+             * The default value is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}.
+             *
+             * On API 22 and below, this has no effect as there is no line break strategy.
+             *
+             * @param strategy the break strategy
+             * @return PrecomputedTextCompat.Builder instance
+             * @see StaticLayout.Builder#setBreakStrategy
+             * @see android.widget.TextView#setBreakStrategy
+             */
+            @RequiresApi(23)
+            public Builder setBreakStrategy(int strategy) {
+                mBreakStrategy = strategy;
+                return this;
+            }
+
+            /**
+             * Set the hyphenation frequency.
+             *
+             * The default value is {@link Layout#HYPHENATION_FREQUENCY_NORMAL}.
+             *
+             * On API 22 and below, this has no effect as there is no hyphenation frequency.
+             *
+             * @param frequency the hyphenation frequency
+             * @return PrecomputedTextCompat.Builder instance
+             * @see StaticLayout.Builder#setHyphenationFrequency
+             * @see android.widget.TextView#setHyphenationFrequency
+             */
+            @RequiresApi(23)
+            public Builder setHyphenationFrequency(int frequency) {
+                mHyphenationFrequency = frequency;
+                return this;
+            }
+
+            /**
+             * Set the text direction heuristic.
+             *
+             * The default value is {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}.
+             *
+             * On API 17 or before, text direction heuristics cannot be modified, so this method
+             * does nothing.
+             *
+             * @param textDir the text direction heuristic for resolving bidi behavior
+             * @return PrecomputedTextCompat.Builder instance
+             * @see StaticLayout.Builder#setTextDirection
+             */
+            @RequiresApi(18)
+            public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) {
+                mTextDir = textDir;
+                return this;
+            }
+
+            /**
+             * Build the {@link Params}.
+             *
+             * @return the layout parameter
+             */
+            public @NonNull Params build() {
+                return new Params(mPaint, mTextDir, mBreakStrategy, mHyphenationFrequency);
+            }
+        }
+
+        private Params(@NonNull TextPaint paint, @NonNull TextDirectionHeuristic textDir,
+                int strategy, int frequency) {
+            if (BuildCompat.isAtLeastP()) {
+                mWrapped = new PrecomputedText.Params.Builder(paint).setBreakStrategy(strategy)
+                        .setHyphenationFrequency(frequency).setTextDirection(textDir).build();
+            } else {
+                mWrapped = null;
+            }
+            mPaint = paint;
+            mTextDir = textDir;
+            mBreakStrategy = strategy;
+            mHyphenationFrequency = frequency;
+        }
+
+        @RequiresApi(28)
+        public Params(@NonNull PrecomputedText.Params wrapped) {
+            mPaint = wrapped.getTextPaint();
+            mTextDir = wrapped.getTextDirection();
+            mBreakStrategy = wrapped.getBreakStrategy();
+            mHyphenationFrequency = wrapped.getHyphenationFrequency();
+            mWrapped = wrapped;
+
+        }
+
+        /**
+         * Returns the {@link TextPaint} for this text.
+         *
+         * @return A {@link TextPaint}
+         */
+        public @NonNull TextPaint getTextPaint() {
+            return mPaint;
+        }
+
+        /**
+         * Returns the {@link TextDirectionHeuristic} for this text.
+         *
+         * On API 17 and below, this returns null, otherwise returns non-null
+         * TextDirectionHeuristic.
+         *
+         * @return the {@link TextDirectionHeuristic}
+         */
+        @RequiresApi(18)
+        public @Nullable TextDirectionHeuristic getTextDirection() {
+            return mTextDir;
+        }
+
+        /**
+         * Returns the break strategy for this text.
+         *
+         * On API 22 and below, this returns 0.
+         *
+         * @return the line break strategy
+         */
+        @RequiresApi(23)
+        public int getBreakStrategy() {
+            return mBreakStrategy;
+        }
+
+        /**
+         * Returns the hyphenation frequency for this text.
+         *
+         * On API 22 and below, this returns 0.
+         *
+         * @return the hyphenation frequency
+         */
+        @RequiresApi(23)
+        public int getHyphenationFrequency() {
+            return mHyphenationFrequency;
+        }
+
+        /**
+         * Check if the same text layout.
+         *
+         * @return true if this and the given param result in the same text layout
+         */
+        @Override
+        public boolean equals(@Nullable Object o) {
+            if (o == this) {
+                return true;
+            }
+            if (o == null || !(o instanceof Params)) {
+                return false;
+            }
+            Params other = (Params) o;
+            if (mWrapped != null) {
+                return mWrapped.equals(other);
+            }
+
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                if (mBreakStrategy != other.getBreakStrategy()) {
+                    return false;
+                }
+                if (mHyphenationFrequency != other.getHyphenationFrequency()) {
+                    return false;
+                }
+            }
+
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+                if (mTextDir != other.getTextDirection()) {
+                    return false;
+                }
+            }
+
+            if (mPaint.getTextSize() != other.getTextPaint().getTextSize()) {
+                return false;
+            }
+            if (mPaint.getTextScaleX() != other.getTextPaint().getTextScaleX()) {
+                return false;
+            }
+            if (mPaint.getTextSkewX() != other.getTextPaint().getTextSkewX()) {
+                return false;
+            }
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                if (mPaint.getLetterSpacing() != other.getTextPaint().getLetterSpacing()) {
+                    return false;
+                }
+                if (!TextUtils.equals(mPaint.getFontFeatureSettings(),
+                        other.getTextPaint().getFontFeatureSettings())) {
+                    return false;
+                }
+            }
+            if (mPaint.getFlags() != other.getTextPaint().getFlags()) {
+                return false;
+            }
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                if (!mPaint.getTextLocales().equals(other.getTextPaint().getTextLocales())) {
+                    return false;
+                }
+            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+                if (!mPaint.getTextLocale().equals(other.getTextPaint().getTextLocale())) {
+                    return false;
+                }
+            }
+            if (mPaint.getTypeface() == null) {
+                if (other.getTextPaint().getTypeface() != null) {
+                    return false;
+                }
+            } else if (!mPaint.getTypeface().equals(other.getTextPaint().getTypeface())) {
+                return false;
+            }
+
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                return ObjectsCompat.hash(mPaint.getTextSize(), mPaint.getTextScaleX(),
+                        mPaint.getTextSkewX(), mPaint.getLetterSpacing(), mPaint.getFlags(),
+                        mPaint.getTextLocales(), mPaint.getTypeface(), mPaint.isElegantTextHeight(),
+                        mTextDir, mBreakStrategy, mHyphenationFrequency);
+            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                return ObjectsCompat.hash(mPaint.getTextSize(), mPaint.getTextScaleX(),
+                        mPaint.getTextSkewX(), mPaint.getLetterSpacing(), mPaint.getFlags(),
+                        mPaint.getTextLocale(), mPaint.getTypeface(), mPaint.isElegantTextHeight(),
+                        mTextDir, mBreakStrategy, mHyphenationFrequency);
+            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+                return ObjectsCompat.hash(mPaint.getTextSize(), mPaint.getTextScaleX(),
+                        mPaint.getTextSkewX(), mPaint.getFlags(), mPaint.getTextLocale(),
+                        mPaint.getTypeface(), mTextDir, mBreakStrategy, mHyphenationFrequency);
+            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+                return ObjectsCompat.hash(mPaint.getTextSize(), mPaint.getTextScaleX(),
+                        mPaint.getTextSkewX(), mPaint.getFlags(), mPaint.getTextLocale(),
+                        mPaint.getTypeface(), mTextDir, mBreakStrategy, mHyphenationFrequency);
+            } else {
+                return ObjectsCompat.hash(mPaint.getTextSize(), mPaint.getTextScaleX(),
+                        mPaint.getTextSkewX(), mPaint.getFlags(), mPaint.getTypeface(), mTextDir,
+                        mBreakStrategy, mHyphenationFrequency);
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder("{");
+            sb.append("textSize=" + mPaint.getTextSize());
+            sb.append(", textScaleX=" + mPaint.getTextScaleX());
+            sb.append(", textSkewX=" + mPaint.getTextSkewX());
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                sb.append(", letterSpacing=" + mPaint.getLetterSpacing());
+                sb.append(", elegantTextHeight=" + mPaint.isElegantTextHeight());
+            }
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                sb.append(", textLocale=" + mPaint.getTextLocales());
+            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+                sb.append(", textLocale=" + mPaint.getTextLocale());
+            }
+            sb.append(", typeface=" + mPaint.getTypeface());
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                sb.append(", variationSettings=" + mPaint.getFontVariationSettings());
+            }
+            sb.append(", textDir=" + mTextDir);
+            sb.append(", breakStrategy=" + mBreakStrategy);
+            sb.append(", hyphenationFrequency=" + mHyphenationFrequency);
+            sb.append("}");
+            return sb.toString();
+        }
+    };
+
+    // The original text.
+    private final @NonNull Spannable mText;
+
+    private final @NonNull Params mParams;
+
+    // The list of measured paragraph info.
+    private final @NonNull int[] mParagraphEnds;
+
+    // null on API 27 or before. Non-null on API 28 or later
+    private final @Nullable PrecomputedText mWrapped;
+
+    /**
+     * Create a new {@link PrecomputedText} which will pre-compute text measurement and glyph
+     * positioning information.
+     * <p>
+     * This can be expensive, so computing this on a background thread before your text will be
+     * presented can save work on the UI thread.
+     * </p>
+     *
+     * Note that any {@link android.text.NoCopySpan} attached to the text won't be passed to the
+     * created PrecomputedText.
+     *
+     * @param text the text to be measured
+     * @param params parameters that define how text will be precomputed
+     * @return A {@link PrecomputedText}
+     */
+    public static PrecomputedTextCompat create(@NonNull CharSequence text, @NonNull Params params) {
+        Preconditions.checkNotNull(text);
+        Preconditions.checkNotNull(params);
+
+        if (BuildCompat.isAtLeastP() && params.mWrapped != null) {
+            return new PrecomputedTextCompat(PrecomputedText.create(text, params.mWrapped), params);
+        }
+
+        ArrayList<Integer> ends = new ArrayList<>();
+
+        int paraEnd = 0;
+        int end = text.length();
+        for (int paraStart = 0; paraStart < end; paraStart = paraEnd) {
+            paraEnd = TextUtils.indexOf(text, LINE_FEED, paraStart, end);
+            if (paraEnd < 0) {
+                // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph
+                // end.
+                paraEnd = end;
+            } else {
+                paraEnd++;  // Includes LINE_FEED(U+000A) to the prev paragraph.
+            }
+
+            ends.add(paraEnd);
+        }
+        int[] result = new int[ends.size()];
+        for (int i = 0; i < ends.size(); ++i) {
+            result[i] = ends.get(i);
+        }
+
+        // No framework support for PrecomputedText
+        // Compute text layout and throw away StaticLayout for the purpose of warming up the
+        // internal text layout cache.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            StaticLayout.Builder.obtain(text, 0, text.length(), params.getTextPaint(),
+                    Integer.MAX_VALUE)
+                    .setBreakStrategy(params.getBreakStrategy())
+                    .setHyphenationFrequency(params.getHyphenationFrequency())
+                    .setTextDirection(params.getTextDirection())
+                    .build();
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            new StaticLayout(text, params.getTextPaint(), Integer.MAX_VALUE,
+                    Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
+        } else {
+            // There is no way of precomputing text layout on API 20 or before
+            // Do nothing
+        }
+
+        return new PrecomputedTextCompat(text, params, result);
+    }
+
+    // Use PrecomputedText.create instead.
+    private PrecomputedTextCompat(@NonNull CharSequence text, @NonNull Params params,
+            @NonNull int[] paraEnds) {
+        mText = new SpannableString(text);
+        mParams = params;
+        mParagraphEnds = paraEnds;
+        mWrapped = null;
+    }
+
+    @RequiresApi(28)
+    private PrecomputedTextCompat(@NonNull PrecomputedText precomputed, @NonNull Params params) {
+        mText = precomputed;
+        mParams = params;
+        mParagraphEnds = null;
+        mWrapped = precomputed;
+    }
+
+    /**
+     * Returns the layout parameters used to measure this text.
+     */
+    public @NonNull Params getParams() {
+        return mParams;
+    }
+
+    /**
+     * Returns the count of paragraphs.
+     */
+    public @IntRange(from = 0) int getParagraphCount() {
+        if (BuildCompat.isAtLeastP()) {
+            return mWrapped.getParagraphCount();
+        } else {
+            return mParagraphEnds.length;
+        }
+    }
+
+    /**
+     * Returns the paragraph start offset of the text.
+     */
+    public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) {
+        Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
+        if (BuildCompat.isAtLeastP()) {
+            return mWrapped.getParagraphStart(paraIndex);
+        } else {
+            return paraIndex == 0 ? 0 : mParagraphEnds[paraIndex - 1];
+        }
+    }
+
+    /**
+     * Returns the paragraph end offset of the text.
+     */
+    public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) {
+        Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
+        if (BuildCompat.isAtLeastP()) {
+            return mWrapped.getParagraphStart(paraIndex);
+        } else {
+            return mParagraphEnds[paraIndex];
+        }
+    }
+
+
+    private int findParaIndex(@IntRange(from = 0) int pos) {
+        for (int i = 0; i < mParagraphEnds.length; ++i) {
+            if (pos < mParagraphEnds[i]) {
+                return i;
+            }
+        }
+        throw new IndexOutOfBoundsException(
+                "pos must be less than " + mParagraphEnds[mParagraphEnds.length - 1]
+                        + ", gave " + pos);
+    }
+
+    ///////////////////////////////////////////////////////////////////////////////////////////////
+    // Spannable overrides
+    //
+    // Do not allow to modify MetricAffectingSpan
+
+    /**
+     * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified.
+     */
+    @Override
+    public void setSpan(Object what, int start, int end, int flags) {
+        if (what instanceof MetricAffectingSpan) {
+            throw new IllegalArgumentException(
+                    "MetricAffectingSpan can not be set to PrecomputedText.");
+        }
+        if (BuildCompat.isAtLeastP()) {
+            mWrapped.setSpan(what, start, end, flags);
+        } else {
+            mText.setSpan(what, start, end, flags);
+        }
+    }
+
+    /**
+     * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified.
+     */
+    @Override
+    public void removeSpan(Object what) {
+        if (what instanceof MetricAffectingSpan) {
+            throw new IllegalArgumentException(
+                    "MetricAffectingSpan can not be removed from PrecomputedText.");
+        }
+        if (BuildCompat.isAtLeastP()) {
+            mWrapped.removeSpan(what);
+        } else {
+            mText.removeSpan(what);
+        }
+    }
+
+    ///////////////////////////////////////////////////////////////////////////////////////////////
+    // Spanned overrides
+    //
+    // Just proxy for underlying mText if appropriate.
+
+    @Override
+    public <T> T[] getSpans(int start, int end, Class<T> type) {
+        if (BuildCompat.isAtLeastP()) {
+            return mWrapped.getSpans(start, end, type);
+        } else {
+            return mText.getSpans(start, end, type);
+        }
+
+    }
+
+    @Override
+    public int getSpanStart(Object tag) {
+        return mText.getSpanStart(tag);
+    }
+
+    @Override
+    public int getSpanEnd(Object tag) {
+        return mText.getSpanEnd(tag);
+    }
+
+    @Override
+    public int getSpanFlags(Object tag) {
+        return mText.getSpanFlags(tag);
+    }
+
+    @Override
+    public int nextSpanTransition(int start, int limit, Class type) {
+        return mText.nextSpanTransition(start, limit, type);
+    }
+
+    ///////////////////////////////////////////////////////////////////////////////////////////////
+    // CharSequence overrides.
+    //
+    // Just proxy for underlying mText.
+
+    @Override
+    public int length() {
+        return mText.length();
+    }
+
+    @Override
+    public char charAt(int index) {
+        return mText.charAt(index);
+    }
+
+    @Override
+    public CharSequence subSequence(int start, int end) {
+        return mText.subSequence(start, end);
+    }
+
+    @Override
+    public String toString() {
+        return mText.toString();
+    }
+}
diff --git a/compat/src/main/java/androidx/core/text/util/FindAddress.java b/compat/src/main/java/androidx/core/text/util/FindAddress.java
index 0602428..916db2b 100644
--- a/compat/src/main/java/androidx/core/text/util/FindAddress.java
+++ b/compat/src/main/java/androidx/core/text/util/FindAddress.java
@@ -466,20 +466,21 @@
 
                     // At this point we've matched a state; try to match a zip code after it.
                     Matcher zipMatcher = sWordRe.matcher(content);
-                    if (zipMatcher.find(stateMatch.end())
-                            && isValidZipCode(zipMatcher.group(0), stateMatch)) {
-                        return zipMatcher.end();
+                    if (zipMatcher.find(stateMatch.end())) {
+                        if (isValidZipCode(zipMatcher.group(0), stateMatch)) {
+                            return zipMatcher.end();
+                        }
+                    } else {
+                        // The content ends with a state but no zip
+                        // code. This is a legal match according to the
+                        // documentation. N.B. This is equivalent to the
+                        // original c++ implementation, which only allowed
+                        // the zip code to be optional at the end of the
+                        // string, which presumably is a bug.  We tried
+                        // relaxing this to work in other places but it
+                        // caused too many false positives.
+                        nonZipMatch = stateMatch.end();
                     }
-                    // The content ends with a state but no zip
-                    // code. This is a legal match according to the
-                    // documentation. N.B. This differs from the
-                    // original c++ implementation, which only allowed
-                    // the zip code to be optional at the end of the
-                    // string, which presumably is a bug.  Now we
-                    // prefer to find a match with a zip code, but
-                    // remember non-zip matches and return them if
-                    // necessary.
-                    nonZipMatch = stateMatch.end();
                 }
             }
         }
diff --git a/compat/src/main/java/androidx/core/view/DisplayCutoutCompat.java b/compat/src/main/java/androidx/core/view/DisplayCutoutCompat.java
new file mode 100644
index 0000000..0ce108c
--- /dev/null
+++ b/compat/src/main/java/androidx/core/view/DisplayCutoutCompat.java
@@ -0,0 +1,132 @@
+/*
+ * 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.core.view;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import android.graphics.Rect;
+import android.view.DisplayCutout;
+
+import java.util.List;
+
+
+/**
+ * Represents the area of the display that is not functional for displaying content.
+ *
+ * <p>{@code DisplayCutoutCompat} instances are immutable.
+ */
+public final class DisplayCutoutCompat {
+
+    private final Object mDisplayCutout;
+
+    /**
+     * Creates a DisplayCutout instance.
+     *
+     * @param safeInsets the insets from each edge which avoid the display cutout as returned by
+     *                   {@link #getSafeInsetTop()} etc.
+     * @param boundingRects the bounding rects of the display cutouts as returned by
+     *               {@link #getBoundingRects()} ()}.
+     */
+    // TODO(b/73953958): @VisibleForTesting(visibility = PRIVATE)
+    public DisplayCutoutCompat(Rect safeInsets, List<Rect> boundingRects) {
+        this(SDK_INT >= 28 ? new DisplayCutout(safeInsets, boundingRects) : null);
+    }
+
+    private DisplayCutoutCompat(Object displayCutout) {
+        mDisplayCutout = displayCutout;
+    }
+
+    /** Returns the inset from the top which avoids the display cutout in pixels. */
+    public int getSafeInsetTop() {
+        if (SDK_INT >= 28) {
+            return ((DisplayCutout) mDisplayCutout).getSafeInsetTop();
+        } else {
+            return 0;
+        }
+    }
+
+    /** Returns the inset from the bottom which avoids the display cutout in pixels. */
+    public int getSafeInsetBottom() {
+        if (SDK_INT >= 28) {
+            return ((DisplayCutout) mDisplayCutout).getSafeInsetBottom();
+        } else {
+            return 0;
+        }
+    }
+
+    /** Returns the inset from the left which avoids the display cutout in pixels. */
+    public int getSafeInsetLeft() {
+        if (SDK_INT >= 28) {
+            return ((DisplayCutout) mDisplayCutout).getSafeInsetLeft();
+        } else {
+            return 0;
+        }
+    }
+
+    /** Returns the inset from the right which avoids the display cutout in pixels. */
+    public int getSafeInsetRight() {
+        if (SDK_INT >= 28) {
+            return ((DisplayCutout) mDisplayCutout).getSafeInsetRight();
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * Returns a list of {@code Rect}s, each of which is the bounding rectangle for a non-functional
+     * area on the display.
+     *
+     * There will be at most one non-functional area per short edge of the device, and none on
+     * the long edges.
+     *
+     * @return a list of bounding {@code Rect}s, one for each display cutout area.
+     */
+    public List<Rect> getBoundingRects() {
+        if (SDK_INT >= 28) {
+            return ((DisplayCutout) mDisplayCutout).getBoundingRects();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        DisplayCutoutCompat other = (DisplayCutoutCompat) o;
+        return mDisplayCutout == null ? other.mDisplayCutout == null
+                : mDisplayCutout.equals(other.mDisplayCutout);
+    }
+
+    @Override
+    public int hashCode() {
+        return mDisplayCutout == null ? 0 : mDisplayCutout.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return "DisplayCutoutCompat{" + mDisplayCutout + "}";
+    }
+
+    static DisplayCutoutCompat wrap(Object displayCutout) {
+        return displayCutout == null ? null : new DisplayCutoutCompat(displayCutout);
+    }
+}
diff --git a/compat/src/main/java/androidx/core/view/ViewConfigurationCompat.java b/compat/src/main/java/androidx/core/view/ViewConfigurationCompat.java
index 89c3ca5..2e47e55 100644
--- a/compat/src/main/java/androidx/core/view/ViewConfigurationCompat.java
+++ b/compat/src/main/java/androidx/core/view/ViewConfigurationCompat.java
@@ -17,6 +17,7 @@
 package androidx.core.view;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.os.Build;
 import android.util.Log;
 import android.util.TypedValue;
@@ -130,5 +131,21 @@
         return config.getScaledTouchSlop() / 2;
     }
 
+    /**
+     * Check if shortcuts should be displayed in menus.
+     *
+     * @return {@code True} if shortcuts should be displayed in menus.
+     */
+    public static boolean shouldShowMenuShortcutsWhenKeyboardPresent(ViewConfiguration config,
+            @NonNull Context context) {
+        if (android.os.Build.VERSION.SDK_INT >= 28) {
+            return config.shouldShowMenuShortcutsWhenKeyboardPresent();
+        }
+        final Resources res = context.getResources();
+        final int platformResId = res.getIdentifier(
+                "config_showMenuShortcutsWhenKeyboardPresent", "bool", "android");
+        return platformResId != 0 && res.getBoolean(platformResId);
+    }
+
     private ViewConfigurationCompat() {}
 }
diff --git a/compat/src/main/java/androidx/core/view/WindowInsetsCompat.java b/compat/src/main/java/androidx/core/view/WindowInsetsCompat.java
index eef9b12..003044a 100644
--- a/compat/src/main/java/androidx/core/view/WindowInsetsCompat.java
+++ b/compat/src/main/java/androidx/core/view/WindowInsetsCompat.java
@@ -20,6 +20,7 @@
 
 import android.graphics.Rect;
 import android.view.WindowInsets;
+import androidx.annotation.Nullable;
 
 /**
  * Describes a set of insets for window content.
@@ -343,6 +344,34 @@
         }
     }
 
+    /**
+     * Returns the display cutout if there is one.
+     *
+     * @return the display cutout or null if there is none
+     * @see DisplayCutoutCompat
+     */
+    @Nullable
+    public DisplayCutoutCompat getDisplayCutout() {
+        if (SDK_INT >= 28) {
+            return DisplayCutoutCompat.wrap(((WindowInsets) mInsets).getDisplayCutout());
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns a copy of this WindowInsets with the cutout fully consumed.
+     *
+     * @return A modified copy of this WindowInsets
+     */
+    public WindowInsetsCompat consumeDisplayCutout() {
+        if (SDK_INT >= 28) {
+            return new WindowInsetsCompat(((WindowInsets) mInsets).consumeDisplayCutout());
+        } else {
+            return null;
+        }
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) {
diff --git a/compat/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java b/compat/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
index 16b570f..015a77f 100644
--- a/compat/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
+++ b/compat/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
@@ -642,7 +642,9 @@
          * @param rowSpan The number of rows the item spans.
          * @param columnIndex The column index at which the item is located.
          * @param columnSpan The number of columns the item spans.
-         * @param heading Whether the item is a heading.
+         * @param heading Whether the item is a heading. This should be set to false and the newer
+         *                {@link AccessibilityNodeInfoCompat#setHeading(boolean)} used to identify
+         *                headings.
          * @param selected Whether the item is selected.
          * @return An instance.
          */
@@ -666,7 +668,9 @@
          * @param rowSpan The number of rows the item spans.
          * @param columnIndex The column index at which the item is located.
          * @param columnSpan The number of columns the item spans.
-         * @param heading Whether the item is a heading.
+         * @param heading Whether the item is a heading. This should be set to false and the newer
+         *                {@link AccessibilityNodeInfoCompat#setHeading(boolean)} used to identify
+         *                headings.
          * @return An instance.
          */
         public static CollectionItemInfoCompat obtain(int rowIndex, int rowSpan,
@@ -740,6 +744,7 @@
          * heading, table header, etc.
          *
          * @return If the item is a heading.
+         * @deprecated Use {@link AccessibilityNodeInfoCompat#isHeading()}
          */
         public boolean isHeading() {
             if (Build.VERSION.SDK_INT >= 19) {
@@ -3313,11 +3318,16 @@
 
     /**
      * Returns whether node represents a heading.
+     * <p><strong>Note:</strong> Returns {@code true} if either {@link #setHeading(boolean)}
+     * marks this node as a heading or if the node has a {@link CollectionItemInfoCompat} that marks
+     * it as such, to accomodate apps that use the now-deprecated API.</p>
      *
      * @return {@code true} if the node is a heading, {@code false} otherwise.
      */
     public boolean isHeading() {
-        return getBooleanProperty(BOOLEAN_PROPERTY_IS_HEADING);
+        if (getBooleanProperty(BOOLEAN_PROPERTY_IS_HEADING)) return true;
+        CollectionItemInfoCompat collectionItemInfo = getCollectionItemInfo();
+        return (collectionItemInfo != null) && (collectionItemInfo.isHeading());
     }
 
     /**
diff --git a/core/ktx/OWNERS b/core/ktx/OWNERS
new file mode 100644
index 0000000..e450f4c
--- /dev/null
+++ b/core/ktx/OWNERS
@@ -0,0 +1 @@
+jakew@google.com
diff --git a/core/ktx/api/0.1.txt b/core/ktx/api/0.1.txt
new file mode 100644
index 0000000..e324352
--- /dev/null
+++ b/core/ktx/api/0.1.txt
@@ -0,0 +1,673 @@
+package androidx.animation {
+
+  public final class AnimatorKt {
+    ctor public AnimatorKt();
+    method public static final android.animation.Animator.AnimatorListener addListener(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onEnd = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onStart = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onCancel = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onRepeat = "null");
+    method @RequiresApi(19) public static final android.animation.Animator.AnimatorPauseListener addPauseListener(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onResume = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onPause = "null");
+    method public static final android.animation.Animator.AnimatorListener doOnCancel(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+    method public static final android.animation.Animator.AnimatorListener doOnEnd(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+    method @RequiresApi(19) public static final android.animation.Animator.AnimatorPauseListener doOnPause(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+    method public static final android.animation.Animator.AnimatorListener doOnRepeat(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+    method @RequiresApi(19) public static final android.animation.Animator.AnimatorPauseListener doOnResume(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+    method public static final android.animation.Animator.AnimatorListener doOnStart(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+  }
+
+}
+
+package androidx.content {
+
+  public final class ContentValuesKt {
+    ctor public ContentValuesKt();
+    method public static final error.NonExistentClass contentValuesOf(kotlin.Pair<String,?>... pairs);
+  }
+
+  public final class ContextKt {
+    ctor public ContextKt();
+    method public static final void withStyledAttributes(android.content.Context, android.util.AttributeSet? set = "null", int[] attrs, @AttrRes int defStyleAttr = "0", @StyleRes int defStyleRes = "0", kotlin.jvm.functions.Function1<? super android.content.res.TypedArray,kotlin.Unit> block);
+    method public static final void withStyledAttributes(android.content.Context, @StyleRes int resourceId, int[] attrs, kotlin.jvm.functions.Function1<? super android.content.res.TypedArray,kotlin.Unit> block);
+  }
+
+  public final class SharedPreferencesKt {
+    ctor public SharedPreferencesKt();
+    method public static final void edit(android.content.SharedPreferences, kotlin.jvm.functions.Function1<? super android.content.SharedPreferences.Editor,kotlin.Unit> action);
+  }
+
+}
+
+package androidx.content.res {
+
+  public final class TypedArrayKt {
+    ctor public TypedArrayKt();
+    method public static final boolean getBooleanOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method @ColorInt public static final int getColorOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static final android.content.res.ColorStateList getColorStateListOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static final float getDimensionOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method @Dimension public static final int getDimensionPixelOffsetOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method @Dimension public static final int getDimensionPixelSizeOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static final android.graphics.drawable.Drawable getDrawableOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static final float getFloatOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method @RequiresApi(26) public static final android.graphics.Typeface getFontOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static final int getIntOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static final int getIntegerOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static final String getStringOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static final CharSequence[] getTextArrayOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static final CharSequence getTextOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+  }
+
+}
+
+package androidx.database {
+
+  public final class CursorKt {
+    ctor public CursorKt();
+    method public static final byte[] getBlob(android.database.Cursor, String columnName);
+    method public static final byte[]? getBlobOrNull(android.database.Cursor, int index);
+    method public static final error.NonExistentClass getBlobOrNull(android.database.Cursor, String columnName);
+    method public static final double getDouble(android.database.Cursor, String columnName);
+    method public static final Double? getDoubleOrNull(android.database.Cursor, int index);
+    method public static final error.NonExistentClass getDoubleOrNull(android.database.Cursor, String columnName);
+    method public static final float getFloat(android.database.Cursor, String columnName);
+    method public static final Float? getFloatOrNull(android.database.Cursor, int index);
+    method public static final error.NonExistentClass getFloatOrNull(android.database.Cursor, String columnName);
+    method public static final int getInt(android.database.Cursor, String columnName);
+    method public static final Integer? getIntOrNull(android.database.Cursor, int index);
+    method public static final error.NonExistentClass getIntOrNull(android.database.Cursor, String columnName);
+    method public static final long getLong(android.database.Cursor, String columnName);
+    method public static final Long? getLongOrNull(android.database.Cursor, int index);
+    method public static final error.NonExistentClass getLongOrNull(android.database.Cursor, String columnName);
+    method public static final short getShort(android.database.Cursor, String columnName);
+    method public static final Short? getShortOrNull(android.database.Cursor, int index);
+    method public static final error.NonExistentClass getShortOrNull(android.database.Cursor, String columnName);
+    method public static final String getString(android.database.Cursor, String columnName);
+    method public static final String? getStringOrNull(android.database.Cursor, int index);
+    method public static final error.NonExistentClass getStringOrNull(android.database.Cursor, String columnName);
+  }
+
+}
+
+package androidx.database.sqlite {
+
+  public final class SQLiteDatabaseKt {
+    ctor public SQLiteDatabaseKt();
+    method public static final <T> T! transaction(android.database.sqlite.SQLiteDatabase, boolean exclusive = "true", kotlin.jvm.functions.Function1<? super android.database.sqlite.SQLiteDatabase,? extends T> body);
+  }
+
+}
+
+package androidx.graphics {
+
+  public final class BitmapKt {
+    ctor public BitmapKt();
+    method public static final android.graphics.Bitmap applyCanvas(android.graphics.Bitmap, kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+    method public static final android.graphics.Bitmap createBitmap(int width, int height, android.graphics.Bitmap.Config config = "Bitmap.Config.ARGB_8888");
+    method @RequiresApi(26) public static final android.graphics.Bitmap createBitmap(int width, int height, android.graphics.Bitmap.Config config = "Bitmap.Config.ARGB_8888", boolean hasAlpha = "true", android.graphics.ColorSpace colorSpace = "ColorSpace.get(ColorSpace.Named.SRGB)");
+    method public static final operator int get(android.graphics.Bitmap, int x, int y);
+    method public static final android.graphics.Bitmap scale(android.graphics.Bitmap, int width, int height, boolean filter = "true");
+    method public static final operator void set(android.graphics.Bitmap, int x, int y, @ColorInt int color);
+  }
+
+  public final class CanvasKt {
+    ctor public CanvasKt();
+    method public static final void withRotation(android.graphics.Canvas, float degrees = "0.0f", float pivotX = "0.0f", float pivotY = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+    method public static final void withSave(android.graphics.Canvas, kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+    method public static final void withScale(android.graphics.Canvas, float x = "1.0f", float y = "1.0f", float pivotX = "0.0f", float pivotY = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+    method public static final void withSkew(android.graphics.Canvas, float x = "0.0f", float y = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+    method public static final void withTranslation(android.graphics.Canvas, float x = "0.0f", float y = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+  }
+
+  public final class ColorKt {
+    ctor public ColorKt();
+    method @RequiresApi(26) public static final operator float component1(android.graphics.Color);
+    method public static final operator int component1(int);
+    method @RequiresApi(26) public static final operator float component1(long);
+    method @RequiresApi(26) public static final operator float component2(android.graphics.Color);
+    method public static final operator int component2(int);
+    method @RequiresApi(26) public static final operator float component2(long);
+    method @RequiresApi(26) public static final operator float component3(android.graphics.Color);
+    method public static final operator int component3(int);
+    method @RequiresApi(26) public static final operator float component3(long);
+    method @RequiresApi(26) public static final operator float component4(android.graphics.Color);
+    method public static final operator int component4(int);
+    method @RequiresApi(26) public static final operator float component4(long);
+    method public static final int getAlpha(int);
+    method @RequiresApi(26) public static final float getAlpha(long);
+    method public static final int getBlue(int);
+    method @RequiresApi(26) public static final float getBlue(long);
+    method @RequiresApi(26) public static final android.graphics.ColorSpace getColorSpace(long);
+    method public static final int getGreen(int);
+    method @RequiresApi(26) public static final float getGreen(long);
+    method @RequiresApi(26) public static final float getLuminance(int);
+    method @RequiresApi(26) public static final float getLuminance(long);
+    method public static final int getRed(int);
+    method @RequiresApi(26) public static final float getRed(long);
+    method @RequiresApi(26) public static final boolean isSrgb(long);
+    method @RequiresApi(26) public static final boolean isWideGamut(long);
+    method @RequiresApi(26) public static final operator android.graphics.Color plus(android.graphics.Color, android.graphics.Color c);
+    method @RequiresApi(26) public static final android.graphics.Color toColor(int);
+    method @RequiresApi(26) public static final android.graphics.Color toColor(long);
+    method @RequiresApi(26) @ColorInt public static final int toColorInt(long);
+    method @RequiresApi(26) @ColorLong public static final long toColorLong(int);
+  }
+
+  public final class MatrixKt {
+    ctor public MatrixKt();
+    method public static final error.NonExistentClass rotationMatrix(float degrees, float px = "0.0f", float py = "0.0f");
+    method public static final error.NonExistentClass scaleMatrix(float sx = "1.0f", float sy = "1.0f");
+    method public static final operator error.NonExistentClass times(android.graphics.Matrix, android.graphics.Matrix m);
+    method public static final error.NonExistentClass translationMatrix(float tx = "0.0f", float ty = "0.0f");
+    method public static final error.NonExistentClass values(android.graphics.Matrix);
+  }
+
+  public final class PathKt {
+    ctor public PathKt();
+    method @RequiresApi(19) public static final infix android.graphics.Path and(android.graphics.Path, android.graphics.Path p);
+    method @RequiresApi(26) public static final Iterable<androidx.graphics.PathSegment> flatten(android.graphics.Path, float error = "0.5f");
+    method @RequiresApi(19) public static final operator android.graphics.Path minus(android.graphics.Path, android.graphics.Path p);
+    method @RequiresApi(19) public static final infix android.graphics.Path or(android.graphics.Path, android.graphics.Path p);
+    method @RequiresApi(19) public static final operator android.graphics.Path plus(android.graphics.Path, android.graphics.Path p);
+    method @RequiresApi(19) public static final infix android.graphics.Path xor(android.graphics.Path, android.graphics.Path p);
+  }
+
+  public final class PathSegment {
+    ctor public PathSegment(android.graphics.PointF start, float startFraction, android.graphics.PointF end, float endFraction);
+    method public final android.graphics.PointF component1();
+    method public final float component2();
+    method public final android.graphics.PointF component3();
+    method public final float component4();
+    method public final androidx.graphics.PathSegment copy(android.graphics.PointF start, float startFraction, android.graphics.PointF end, float endFraction);
+    method public final android.graphics.PointF getEnd();
+    method public final float getEndFraction();
+    method public final android.graphics.PointF getStart();
+    method public final float getStartFraction();
+  }
+
+  public final class PictureKt {
+    ctor public PictureKt();
+    method public static final android.graphics.Picture record(android.graphics.Picture, int width, int height, kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+  }
+
+  public final class PointKt {
+    ctor public PointKt();
+    method public static final operator int component1(android.graphics.Point);
+    method public static final operator float component1(android.graphics.PointF);
+    method public static final operator int component2(android.graphics.Point);
+    method public static final operator float component2(android.graphics.PointF);
+    method public static final operator android.graphics.Point minus(android.graphics.Point, android.graphics.Point p);
+    method public static final operator android.graphics.PointF minus(android.graphics.PointF, android.graphics.PointF p);
+    method public static final operator android.graphics.Point minus(android.graphics.Point, int xy);
+    method public static final operator android.graphics.PointF minus(android.graphics.PointF, float xy);
+    method public static final operator android.graphics.Point plus(android.graphics.Point, android.graphics.Point p);
+    method public static final operator android.graphics.PointF plus(android.graphics.PointF, android.graphics.PointF p);
+    method public static final operator android.graphics.Point plus(android.graphics.Point, int xy);
+    method public static final operator android.graphics.PointF plus(android.graphics.PointF, float xy);
+    method public static final android.graphics.Point toPoint(android.graphics.PointF);
+    method public static final android.graphics.PointF toPointF(android.graphics.Point);
+    method public static final operator android.graphics.Point unaryMinus(android.graphics.Point);
+    method public static final operator android.graphics.PointF unaryMinus(android.graphics.PointF);
+  }
+
+  public final class PorterDuffKt {
+    ctor public PorterDuffKt();
+    method public static final android.graphics.PorterDuffColorFilter toColorFilter(android.graphics.PorterDuff.Mode, int color);
+    method public static final android.graphics.PorterDuffXfermode toXfermode(android.graphics.PorterDuff.Mode);
+  }
+
+  public final class RectKt {
+    ctor public RectKt();
+    method public static final infix android.graphics.Rect and(android.graphics.Rect, android.graphics.Rect r);
+    method public static final infix android.graphics.RectF and(android.graphics.RectF, android.graphics.RectF r);
+    method public static final operator int component1(android.graphics.Rect);
+    method public static final operator float component1(android.graphics.RectF);
+    method public static final operator int component2(android.graphics.Rect);
+    method public static final operator float component2(android.graphics.RectF);
+    method public static final operator int component3(android.graphics.Rect);
+    method public static final operator float component3(android.graphics.RectF);
+    method public static final operator int component4(android.graphics.Rect);
+    method public static final operator float component4(android.graphics.RectF);
+    method public static final operator boolean contains(android.graphics.Rect, android.graphics.Point p);
+    method public static final operator boolean contains(android.graphics.RectF, android.graphics.PointF p);
+    method public static final operator android.graphics.Region minus(android.graphics.Rect, android.graphics.Rect r);
+    method public static final operator android.graphics.Region minus(android.graphics.RectF, android.graphics.RectF r);
+    method public static final operator android.graphics.Rect minus(android.graphics.Rect, int xy);
+    method public static final operator android.graphics.RectF minus(android.graphics.RectF, float xy);
+    method public static final operator android.graphics.Rect minus(android.graphics.Rect, android.graphics.Point xy);
+    method public static final operator android.graphics.RectF minus(android.graphics.RectF, android.graphics.PointF xy);
+    method public static final infix android.graphics.Rect or(android.graphics.Rect, android.graphics.Rect r);
+    method public static final infix android.graphics.RectF or(android.graphics.RectF, android.graphics.RectF r);
+    method public static final operator android.graphics.Rect plus(android.graphics.Rect, android.graphics.Rect r);
+    method public static final operator android.graphics.RectF plus(android.graphics.RectF, android.graphics.RectF r);
+    method public static final operator android.graphics.Rect plus(android.graphics.Rect, int xy);
+    method public static final operator android.graphics.RectF plus(android.graphics.RectF, float xy);
+    method public static final operator android.graphics.Rect plus(android.graphics.Rect, android.graphics.Point xy);
+    method public static final operator android.graphics.RectF plus(android.graphics.RectF, android.graphics.PointF xy);
+    method public static final android.graphics.Rect toRect(android.graphics.RectF);
+    method public static final android.graphics.RectF toRectF(android.graphics.Rect);
+    method public static final android.graphics.Region toRegion(android.graphics.Rect);
+    method public static final android.graphics.Region toRegion(android.graphics.RectF);
+    method public static final error.NonExistentClass transform(android.graphics.RectF, android.graphics.Matrix m);
+    method public static final infix android.graphics.Region xor(android.graphics.Rect, android.graphics.Rect r);
+    method public static final infix android.graphics.Region xor(android.graphics.RectF, android.graphics.RectF r);
+  }
+
+  public final class RegionKt {
+    ctor public RegionKt();
+    method public static final infix android.graphics.Region and(android.graphics.Region, android.graphics.Rect r);
+    method public static final infix android.graphics.Region and(android.graphics.Region, android.graphics.Region r);
+    method public static final operator boolean contains(android.graphics.Region, android.graphics.Point p);
+    method public static final void forEach(android.graphics.Region, kotlin.jvm.functions.Function1<? super android.graphics.Rect,kotlin.Unit> action);
+    method public static final operator java.util.Iterator<android.graphics.Rect> iterator(android.graphics.Region);
+    method public static final operator android.graphics.Region minus(android.graphics.Region, android.graphics.Rect r);
+    method public static final operator android.graphics.Region minus(android.graphics.Region, android.graphics.Region r);
+    method public static final operator android.graphics.Region not(android.graphics.Region);
+    method public static final infix android.graphics.Region or(android.graphics.Region, android.graphics.Rect r);
+    method public static final infix android.graphics.Region or(android.graphics.Region, android.graphics.Region r);
+    method public static final operator android.graphics.Region plus(android.graphics.Region, android.graphics.Rect r);
+    method public static final operator android.graphics.Region plus(android.graphics.Region, android.graphics.Region r);
+    method public static final operator android.graphics.Region unaryMinus(android.graphics.Region);
+    method public static final infix android.graphics.Region xor(android.graphics.Region, android.graphics.Rect r);
+    method public static final infix android.graphics.Region xor(android.graphics.Region, android.graphics.Region r);
+  }
+
+  public final class ShaderKt {
+    ctor public ShaderKt();
+    method public static final void transform(android.graphics.Shader, kotlin.jvm.functions.Function1<? super android.graphics.Matrix,kotlin.Unit> block);
+  }
+
+}
+
+package androidx.graphics.drawable {
+
+  public final class BitmapDrawableKt {
+    ctor public BitmapDrawableKt();
+    method public static final android.graphics.drawable.BitmapDrawable toDrawable(android.graphics.Bitmap, android.content.res.Resources resources);
+  }
+
+  public final class ColorDrawableKt {
+    ctor public ColorDrawableKt();
+    method public static final android.graphics.drawable.ColorDrawable toDrawable(int);
+    method @RequiresApi(26) public static final android.graphics.drawable.ColorDrawable toDrawable(android.graphics.Color);
+  }
+
+  public final class DrawableKt {
+    ctor public DrawableKt();
+    method public static final android.graphics.Bitmap toBitmap(android.graphics.drawable.Drawable, int width = "intrinsicWidth", int height = "intrinsicHeight", android.graphics.Bitmap.Config? config = "null");
+    method public static final void updateBounds(android.graphics.drawable.Drawable, int left = "bounds.left", int top = "bounds.top", int right = "bounds.right", int bottom = "bounds.bottom");
+  }
+
+  public final class IconKt {
+    ctor public IconKt();
+    method @RequiresApi(26) public static final android.graphics.drawable.Icon toAdaptiveIcon(android.graphics.Bitmap);
+    method @RequiresApi(26) public static final android.graphics.drawable.Icon toIcon(android.graphics.Bitmap);
+    method @RequiresApi(26) public static final android.graphics.drawable.Icon toIcon(android.net.Uri);
+    method @RequiresApi(26) public static final android.graphics.drawable.Icon toIcon(byte[]);
+  }
+
+}
+
+package androidx.net {
+
+  public final class UriKt {
+    ctor public UriKt();
+    method public static final android.net.Uri toUri(String);
+  }
+
+}
+
+package androidx.os {
+
+  public final class BundleKt {
+    ctor public BundleKt();
+    method public static final error.NonExistentClass bundleOf(kotlin.Pair<String,?>... pairs);
+  }
+
+  public final class HandlerKt {
+    ctor public HandlerKt();
+    method public static final error.NonExistentClass postAtTime(android.os.Handler, long uptimeMillis, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+    method public static final void postDelayed(android.os.Handler, Runnable runnable, Object? token, long delayInMillis);
+    method public static final error.NonExistentClass postDelayed(android.os.Handler, long delayInMillis, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+    method public static final error.NonExistentClass postDelayed(android.os.Handler, long amount, java.util.concurrent.TimeUnit unit, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+    method @RequiresApi(26) public static final error.NonExistentClass postDelayed(android.os.Handler, java.time.Duration duration, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+  }
+
+  public final class PersistableBundleKt {
+    ctor public PersistableBundleKt();
+    method @RequiresApi(21) public static final error.NonExistentClass persistableBundleOf(kotlin.Pair<String,?>... pairs);
+  }
+
+  public final class TraceKt {
+    ctor public TraceKt();
+    method public static final <T> T! trace(String sectionName, kotlin.jvm.functions.Function0<? extends T> block);
+  }
+
+}
+
+package androidx.text {
+
+  public final class SpannableStringBuilderKt {
+    ctor public SpannableStringBuilderKt();
+    method public static final android.text.SpannableStringBuilder backgroundColor(android.text.SpannableStringBuilder, @ColorInt int color, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static final android.text.SpannableStringBuilder bold(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static final android.text.SpannedString buildSpannedString(kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static final android.text.SpannableStringBuilder color(android.text.SpannableStringBuilder, @ColorInt int color, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static final android.text.SpannableStringBuilder inSpans(android.text.SpannableStringBuilder, Object[] spans, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static final android.text.SpannableStringBuilder italic(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static final android.text.SpannableStringBuilder underline(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+  }
+
+}
+
+package androidx.time {
+
+  public final class DayOfWeekKt {
+    ctor public DayOfWeekKt();
+    method @RequiresApi(26) public static final java.time.DayOfWeek asDayOfWeek(int);
+    method @RequiresApi(26) public static final int asInt(java.time.DayOfWeek);
+  }
+
+  public final class DurationKt {
+    ctor public DurationKt();
+    method @RequiresApi(26) public static final operator long component1(java.time.Duration);
+    method @RequiresApi(26) public static final operator int component2(java.time.Duration);
+    method @RequiresApi(26) public static final operator java.time.Duration div(java.time.Duration, long divisor);
+    method @RequiresApi(260) public static final java.time.Duration hours(int);
+    method @RequiresApi(260) public static final java.time.Duration hours(long);
+    method @RequiresApi(26) public static final java.time.Duration millis(int);
+    method @RequiresApi(26) public static final java.time.Duration millis(long);
+    method @RequiresApi(260) public static final java.time.Duration minutes(int);
+    method @RequiresApi(260) public static final java.time.Duration minutes(long);
+    method @RequiresApi(26) public static final java.time.Duration nanos(int);
+    method @RequiresApi(26) public static final java.time.Duration nanos(long);
+    method @RequiresApi(26) public static final java.time.Duration seconds(int);
+    method @RequiresApi(26) public static final java.time.Duration seconds(long);
+    method @RequiresApi(26) public static final operator java.time.Duration times(java.time.Duration, long multiplicand);
+    method @RequiresApi(26) public static final operator java.time.Duration unaryMinus(java.time.Duration);
+  }
+
+  public final class InstantKt {
+    ctor public InstantKt();
+    method @RequiresApi(26) public static final java.time.Instant asEpochMillis(long);
+    method @RequiresApi(26) public static final java.time.Instant asEpochSeconds(long);
+    method @RequiresApi(26) public static final operator long component1(java.time.Instant);
+    method @RequiresApi(26) public static final operator int component2(java.time.Instant);
+  }
+
+  public final class LocalDateKt {
+    ctor public LocalDateKt();
+    method @RequiresApi(26) public static final operator int component1(java.time.LocalDate);
+    method @RequiresApi(26) public static final operator java.time.Month component2(java.time.LocalDate);
+    method @RequiresApi(26) public static final operator int component3(java.time.LocalDate);
+  }
+
+  public final class LocalDateTimeKt {
+    ctor public LocalDateTimeKt();
+    method @RequiresApi(26) public static final operator java.time.LocalDate component1(java.time.LocalDateTime);
+    method @RequiresApi(26) public static final operator java.time.LocalTime component2(java.time.LocalDateTime);
+  }
+
+  public final class LocalTimeKt {
+    ctor public LocalTimeKt();
+    method @RequiresApi(26) public static final operator int component1(java.time.LocalTime);
+    method @RequiresApi(26) public static final operator int component2(java.time.LocalTime);
+    method @RequiresApi(26) public static final operator int component3(java.time.LocalTime);
+    method @RequiresApi(26) public static final operator int component4(java.time.LocalTime);
+  }
+
+  public final class MonthDayKt {
+    ctor public MonthDayKt();
+    method @RequiresApi(26) public static final operator java.time.Month component1(java.time.MonthDay);
+    method @RequiresApi(26) public static final operator int component2(java.time.MonthDay);
+  }
+
+  public final class MonthKt {
+    ctor public MonthKt();
+    method @RequiresApi(26) public static final int asInt(java.time.Month);
+    method @RequiresApi(26) public static final java.time.Month asMonth(int);
+  }
+
+  public final class OffsetDateTimeKt {
+    ctor public OffsetDateTimeKt();
+    method @RequiresApi(26) public static final operator java.time.LocalDateTime component1(java.time.OffsetDateTime);
+    method @RequiresApi(26) public static final operator java.time.ZoneOffset component2(java.time.OffsetDateTime);
+  }
+
+  public final class OffsetTimeKt {
+    ctor public OffsetTimeKt();
+    method @RequiresApi(26) public static final operator java.time.LocalTime component1(java.time.OffsetTime);
+    method @RequiresApi(26) public static final operator java.time.ZoneOffset component2(java.time.OffsetTime);
+  }
+
+  public final class PeriodKt {
+    ctor public PeriodKt();
+    method @RequiresApi(26) public static final operator int component1(java.time.Period);
+    method @RequiresApi(26) public static final operator int component2(java.time.Period);
+    method @RequiresApi(26) public static final operator int component3(java.time.Period);
+    method @RequiresApi(26) public static final java.time.Period days(int);
+    method @RequiresApi(26) public static final java.time.Period months(int);
+    method @RequiresApi(26) public static final operator java.time.Period times(java.time.Period, int multiplicand);
+    method @RequiresApi(26) public static final operator java.time.Period unaryMinus(java.time.Period);
+    method @RequiresApi(26) public static final java.time.Period years(int);
+  }
+
+  public final class YearKt {
+    ctor public YearKt();
+    method @RequiresApi(26) public static final int asInt(java.time.Year);
+    method @RequiresApi(26) public static final java.time.Year asYear(int);
+  }
+
+  public final class YearMonthKt {
+    ctor public YearMonthKt();
+    method @RequiresApi(26) public static final operator int component1(java.time.YearMonth);
+    method @RequiresApi(26) public static final operator java.time.Month component2(java.time.YearMonth);
+  }
+
+  public final class ZonedDateTimeKt {
+    ctor public ZonedDateTimeKt();
+    method @RequiresApi(26) public static final operator java.time.LocalDateTime component1(java.time.ZonedDateTime);
+    method @RequiresApi(26) public static final operator java.time.ZoneId component2(java.time.ZonedDateTime);
+  }
+
+}
+
+package androidx.transition {
+
+  public final class TransitionKt {
+    ctor public TransitionKt();
+    method @RequiresApi(19) public static final void addListener(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onEnd = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onStart = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onCancel = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onResume = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onPause = "null");
+    method @RequiresApi(19) public static final void doOnCancel(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+    method @RequiresApi(19) public static final void doOnEnd(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+    method @RequiresApi(19) public static final void doOnPause(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+    method @RequiresApi(19) public static final void doOnResume(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+    method @RequiresApi(19) public static final void doOnStart(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+  }
+
+}
+
+package androidx.util {
+
+  public final class ArrayMapKt {
+    ctor public ArrayMapKt();
+    method @RequiresApi(19) public static final <K, V> android.util.ArrayMap<K,V> arrayMapOf();
+    method @RequiresApi(19) public static final <K, V> android.util.ArrayMap<K,V> arrayMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
+  }
+
+  public final class ArraySetKt {
+    ctor public ArraySetKt();
+    method @RequiresApi(23) public static final <T> android.util.ArraySet<T> arraySetOf();
+    method @RequiresApi(23) public static final <T> android.util.ArraySet<T> arraySetOf(T... values);
+  }
+
+  public final class AtomicFileKt {
+    ctor public AtomicFileKt();
+    method @RequiresApi(17) public static final byte[] readBytes(android.util.AtomicFile);
+    method @RequiresApi(17) public static final String readText(android.util.AtomicFile, java.nio.charset.Charset charset = "Charsets.UTF_8");
+    method @RequiresApi(17) public static final void tryWrite(android.util.AtomicFile, kotlin.jvm.functions.Function1<? super java.io.FileOutputStream,kotlin.Unit> block);
+    method @RequiresApi(17) public static final void writeBytes(android.util.AtomicFile, byte[] array);
+    method @RequiresApi(17) public static final void writeText(android.util.AtomicFile, String text, java.nio.charset.Charset charset = "Charsets.UTF_8");
+  }
+
+  public final class HalfKt {
+    ctor public HalfKt();
+    method @RequiresApi(26) public static final android.util.Half toHalf(short);
+    method @RequiresApi(26) public static final android.util.Half toHalf(float);
+    method @RequiresApi(26) public static final android.util.Half toHalf(double);
+    method @RequiresApi(26) public static final android.util.Half toHalf(String);
+  }
+
+  public final class LongSparseArrayKt {
+    ctor public LongSparseArrayKt();
+    method @RequiresApi(16) public static final operator <T> boolean contains(android.util.LongSparseArray<T>, long key);
+    method @RequiresApi(16) public static final <T> boolean containsKey(android.util.LongSparseArray<T>, long key);
+    method @RequiresApi(16) public static final <T> boolean containsValue(android.util.LongSparseArray<T>, T! value);
+    method @RequiresApi(16) public static final <T> void forEach(android.util.LongSparseArray<T>, kotlin.jvm.functions.Function2<? super Long,? super T,kotlin.Unit> action);
+    method @RequiresApi(16) public static final <T> T! getOrDefault(android.util.LongSparseArray<T>, long key, T! defaultValue);
+    method @RequiresApi(16) public static final <T> T! getOrElse(android.util.LongSparseArray<T>, long key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
+    method @RequiresApi(16) public static final <T> int getSize(android.util.LongSparseArray<T>);
+    method @RequiresApi(16) public static final <T> boolean isEmpty(android.util.LongSparseArray<T>);
+    method @RequiresApi(16) public static final <T> boolean isNotEmpty(android.util.LongSparseArray<T>);
+    method @RequiresApi(16) public static final <T> kotlin.collections.LongIterator keyIterator(android.util.LongSparseArray<T>);
+    method @RequiresApi(16) public static final operator <T> android.util.LongSparseArray<T> plus(android.util.LongSparseArray<T>, android.util.LongSparseArray<T> other);
+    method @RequiresApi(16) public static final <T> void putAll(android.util.LongSparseArray<T>, android.util.LongSparseArray<T> other);
+    method @RequiresApi(16) public static final <T> boolean remove(android.util.LongSparseArray<T>, long key, T! value);
+    method @RequiresApi(16) public static final operator <T> void set(android.util.LongSparseArray<T>, long key, T! value);
+    method @RequiresApi(16) public static final <T> java.util.Iterator<T> valueIterator(android.util.LongSparseArray<T>);
+  }
+
+  public final class PairKt {
+    ctor public PairKt();
+    method public static final operator <F, S> F! component1(android.util.Pair<F,S>);
+    method public static final operator <F, S> S! component2(android.util.Pair<F,S>);
+    method public static final <F, S> android.util.Pair<F,S> toAndroidPair(kotlin.Pair<? extends F,? extends S>);
+    method public static final <F, S> kotlin.Pair<F,S> toKotlinPair(android.util.Pair<F,S>);
+  }
+
+  public final class RangeKt {
+    ctor public RangeKt();
+    method @RequiresApi(21) public static final infix <T extends java.lang.Comparable<? super T>> android.util.Range<T> and(android.util.Range<T>, android.util.Range<T> other);
+    method @RequiresApi(21) public static final operator <T extends java.lang.Comparable<? super T>> android.util.Range<T> plus(android.util.Range<T>, T value);
+    method @RequiresApi(21) public static final operator <T extends java.lang.Comparable<? super T>> android.util.Range<T> plus(android.util.Range<T>, android.util.Range<T> other);
+    method @RequiresApi(21) public static final infix <T extends java.lang.Comparable<? super T>> android.util.Range<T> rangeTo(T, T that);
+    method @RequiresApi(21) public static final <T extends java.lang.Comparable<? super T>> kotlin.ranges.ClosedRange<T> toClosedRange(android.util.Range<T>);
+    method @RequiresApi(21) public static final <T extends java.lang.Comparable<? super T>> android.util.Range<T> toRange(kotlin.ranges.ClosedRange<T>);
+  }
+
+  public final class SizeKt {
+    ctor public SizeKt();
+    method @RequiresApi(21) public static final operator int component1(android.util.Size);
+    method @RequiresApi(21) public static final operator float component1(android.util.SizeF);
+    method @RequiresApi(21) public static final operator int component2(android.util.Size);
+    method @RequiresApi(21) public static final operator float component2(android.util.SizeF);
+  }
+
+  public final class SparseArrayKt {
+    ctor public SparseArrayKt();
+    method public static final operator <T> boolean contains(android.util.SparseArray<T>, int key);
+    method public static final <T> boolean containsKey(android.util.SparseArray<T>, int key);
+    method public static final <T> boolean containsValue(android.util.SparseArray<T>, T! value);
+    method public static final <T> void forEach(android.util.SparseArray<T>, kotlin.jvm.functions.Function2<? super Integer,? super T,kotlin.Unit> action);
+    method public static final <T> T! getOrDefault(android.util.SparseArray<T>, int key, T! defaultValue);
+    method public static final <T> T! getOrElse(android.util.SparseArray<T>, int key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
+    method public static final <T> int getSize(android.util.SparseArray<T>);
+    method public static final <T> boolean isEmpty(android.util.SparseArray<T>);
+    method public static final <T> boolean isNotEmpty(android.util.SparseArray<T>);
+    method public static final <T> kotlin.collections.IntIterator keyIterator(android.util.SparseArray<T>);
+    method public static final operator <T> android.util.SparseArray<T> plus(android.util.SparseArray<T>, android.util.SparseArray<T> other);
+    method public static final <T> void putAll(android.util.SparseArray<T>, android.util.SparseArray<T> other);
+    method public static final <T> boolean remove(android.util.SparseArray<T>, int key, T! value);
+    method public static final operator <T> void set(android.util.SparseArray<T>, int key, T! value);
+    method public static final <T> java.util.Iterator<T> valueIterator(android.util.SparseArray<T>);
+  }
+
+  public final class SparseBooleanArrayKt {
+    ctor public SparseBooleanArrayKt();
+    method public static final operator boolean contains(android.util.SparseBooleanArray, int key);
+    method public static final boolean containsKey(android.util.SparseBooleanArray, int key);
+    method public static final boolean containsValue(android.util.SparseBooleanArray, boolean value);
+    method public static final void forEach(android.util.SparseBooleanArray, kotlin.jvm.functions.Function2<? super Integer,? super Boolean,kotlin.Unit> action);
+    method public static final boolean getOrDefault(android.util.SparseBooleanArray, int key, boolean defaultValue);
+    method public static final error.NonExistentClass getOrElse(android.util.SparseBooleanArray, int key, kotlin.jvm.functions.Function0<Boolean> defaultValue);
+    method public static final int getSize(android.util.SparseBooleanArray);
+    method public static final boolean isEmpty(android.util.SparseBooleanArray);
+    method public static final boolean isNotEmpty(android.util.SparseBooleanArray);
+    method public static final kotlin.collections.IntIterator keyIterator(android.util.SparseBooleanArray);
+    method public static final operator android.util.SparseBooleanArray plus(android.util.SparseBooleanArray, android.util.SparseBooleanArray other);
+    method public static final void putAll(android.util.SparseBooleanArray, android.util.SparseBooleanArray other);
+    method public static final boolean remove(android.util.SparseBooleanArray, int key, boolean value);
+    method public static final void removeAt(android.util.SparseBooleanArray, int index);
+    method public static final operator void set(android.util.SparseBooleanArray, int key, boolean value);
+    method public static final kotlin.collections.BooleanIterator valueIterator(android.util.SparseBooleanArray);
+  }
+
+  public final class SparseIntArrayKt {
+    ctor public SparseIntArrayKt();
+    method public static final operator boolean contains(android.util.SparseIntArray, int key);
+    method public static final boolean containsKey(android.util.SparseIntArray, int key);
+    method public static final boolean containsValue(android.util.SparseIntArray, int value);
+    method public static final void forEach(android.util.SparseIntArray, kotlin.jvm.functions.Function2<? super Integer,? super Integer,kotlin.Unit> action);
+    method public static final int getOrDefault(android.util.SparseIntArray, int key, int defaultValue);
+    method public static final error.NonExistentClass getOrElse(android.util.SparseIntArray, int key, kotlin.jvm.functions.Function0<Integer> defaultValue);
+    method public static final int getSize(android.util.SparseIntArray);
+    method public static final boolean isEmpty(android.util.SparseIntArray);
+    method public static final boolean isNotEmpty(android.util.SparseIntArray);
+    method public static final kotlin.collections.IntIterator keyIterator(android.util.SparseIntArray);
+    method public static final operator android.util.SparseIntArray plus(android.util.SparseIntArray, android.util.SparseIntArray other);
+    method public static final void putAll(android.util.SparseIntArray, android.util.SparseIntArray other);
+    method public static final boolean remove(android.util.SparseIntArray, int key, int value);
+    method public static final operator void set(android.util.SparseIntArray, int key, int value);
+    method public static final kotlin.collections.IntIterator valueIterator(android.util.SparseIntArray);
+  }
+
+  public final class SparseLongArrayKt {
+    ctor public SparseLongArrayKt();
+    method @RequiresApi(18) public static final operator boolean contains(android.util.SparseLongArray, int key);
+    method @RequiresApi(18) public static final boolean containsKey(android.util.SparseLongArray, int key);
+    method @RequiresApi(18) public static final boolean containsValue(android.util.SparseLongArray, long value);
+    method @RequiresApi(18) public static final void forEach(android.util.SparseLongArray, kotlin.jvm.functions.Function2<? super Integer,? super Long,kotlin.Unit> action);
+    method @RequiresApi(18) public static final long getOrDefault(android.util.SparseLongArray, int key, long defaultValue);
+    method @RequiresApi(18) public static final error.NonExistentClass getOrElse(android.util.SparseLongArray, int key, kotlin.jvm.functions.Function0<Long> defaultValue);
+    method @RequiresApi(18) public static final int getSize(android.util.SparseLongArray);
+    method @RequiresApi(18) public static final boolean isEmpty(android.util.SparseLongArray);
+    method @RequiresApi(18) public static final boolean isNotEmpty(android.util.SparseLongArray);
+    method @RequiresApi(18) public static final kotlin.collections.IntIterator keyIterator(android.util.SparseLongArray);
+    method @RequiresApi(18) public static final operator android.util.SparseLongArray plus(android.util.SparseLongArray, android.util.SparseLongArray other);
+    method @RequiresApi(18) public static final void putAll(android.util.SparseLongArray, android.util.SparseLongArray other);
+    method @RequiresApi(18) public static final boolean remove(android.util.SparseLongArray, int key, long value);
+    method @RequiresApi(18) public static final operator void set(android.util.SparseLongArray, int key, long value);
+    method @RequiresApi(18) public static final kotlin.collections.LongIterator valueIterator(android.util.SparseLongArray);
+  }
+
+}
+
+package androidx.view {
+
+  public final class ViewGroupKt {
+    ctor public ViewGroupKt();
+    method public static final operator boolean contains(android.view.ViewGroup, android.view.View view);
+    method public static final void forEach(android.view.ViewGroup, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+    method public static final void forEachIndexed(android.view.ViewGroup, kotlin.jvm.functions.Function2<? super Integer,? super android.view.View,kotlin.Unit> action);
+    method public static final operator android.view.View get(android.view.ViewGroup, int index);
+    method public static final int getSize(android.view.ViewGroup);
+    method public static final boolean isEmpty(android.view.ViewGroup);
+    method public static final boolean isNotEmpty(android.view.ViewGroup);
+    method public static final operator java.util.Iterator<android.view.View> iterator(android.view.ViewGroup);
+    method public static final operator void minusAssign(android.view.ViewGroup, android.view.View view);
+    method public static final operator void plusAssign(android.view.ViewGroup, android.view.View view);
+    method public static final void setMargins(android.view.ViewGroup.MarginLayoutParams, int size);
+    method public static final void updateMargins(android.view.ViewGroup.MarginLayoutParams, int left = "leftMargin", int top = "topMargin", int right = "rightMargin", int bottom = "bottomMargin");
+    method @RequiresApi(17) public static final void updateMarginsRelative(android.view.ViewGroup.MarginLayoutParams, int start = "marginStart", int top = "topMargin", int end = "marginEnd", int bottom = "bottomMargin");
+  }
+
+  public final class ViewKt {
+    ctor public ViewKt();
+    method public static final void doOnLayout(android.view.View, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+    method public static final void doOnNextLayout(android.view.View, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+    method public static final void doOnPreDraw(android.view.View, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+    method public static final Runnable postDelayed(android.view.View, long delayInMillis, kotlin.jvm.functions.Function0<kotlin.Unit> action);
+    method @RequiresApi(16) public static final Runnable postOnAnimationDelayed(android.view.View, long delayInMillis, kotlin.jvm.functions.Function0<kotlin.Unit> action);
+    method public static final void setPadding(android.view.View, int size);
+    method public static final android.graphics.Bitmap toBitmap(android.view.View, android.graphics.Bitmap.Config config = "Bitmap.Config.ARGB_8888");
+    method public static final void updatePadding(android.view.View, int left = "paddingLeft", int top = "paddingTop", int right = "paddingRight", int bottom = "paddingBottom");
+    method @RequiresApi(17) public static final void updatePaddingRelative(android.view.View, int start = "paddingStart", int top = "paddingTop", int end = "paddingEnd", int bottom = "paddingBottom");
+  }
+
+}
+
diff --git a/core/ktx/api/0.2.txt b/core/ktx/api/0.2.txt
new file mode 100644
index 0000000..5fbfe40
--- /dev/null
+++ b/core/ktx/api/0.2.txt
@@ -0,0 +1,742 @@
+package androidx.animation {
+
+  public final class AnimatorKt {
+    ctor public AnimatorKt();
+    method public static android.animation.Animator.AnimatorListener addListener(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onEnd = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onStart = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onCancel = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onRepeat = "null");
+    method @RequiresApi(19) public static android.animation.Animator.AnimatorPauseListener addPauseListener(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onResume = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onPause = "null");
+    method public static android.animation.Animator.AnimatorListener doOnCancel(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+    method public static android.animation.Animator.AnimatorListener doOnEnd(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+    method @RequiresApi(19) public static android.animation.Animator.AnimatorPauseListener doOnPause(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+    method public static android.animation.Animator.AnimatorListener doOnRepeat(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+    method @RequiresApi(19) public static android.animation.Animator.AnimatorPauseListener doOnResume(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+    method public static android.animation.Animator.AnimatorListener doOnStart(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+  }
+
+}
+
+package androidx.content {
+
+  public final class ContentValuesKt {
+    ctor public ContentValuesKt();
+    method public static error.NonExistentClass contentValuesOf(kotlin.Pair<String,?>... pairs);
+  }
+
+  public final class ContextKt {
+    ctor public ContextKt();
+    method public static void withStyledAttributes(android.content.Context, android.util.AttributeSet? set = "null", int[] attrs, @AttrRes int defStyleAttr = "0", @StyleRes int defStyleRes = "0", kotlin.jvm.functions.Function1<? super android.content.res.TypedArray,kotlin.Unit> block);
+    method public static void withStyledAttributes(android.content.Context, @StyleRes int resourceId, int[] attrs, kotlin.jvm.functions.Function1<? super android.content.res.TypedArray,kotlin.Unit> block);
+  }
+
+  public final class SharedPreferencesKt {
+    ctor public SharedPreferencesKt();
+    method public static void edit(android.content.SharedPreferences, boolean commit = "false", kotlin.jvm.functions.Function1<? super android.content.SharedPreferences.Editor,kotlin.Unit> action);
+  }
+
+}
+
+package androidx.content.res {
+
+  public final class TypedArrayKt {
+    ctor public TypedArrayKt();
+    method public static boolean getBooleanOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method @ColorInt public static int getColorOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static android.content.res.ColorStateList getColorStateListOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static float getDimensionOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method @Dimension public static int getDimensionPixelOffsetOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method @Dimension public static int getDimensionPixelSizeOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static android.graphics.drawable.Drawable getDrawableOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static float getFloatOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method @RequiresApi(26) public static android.graphics.Typeface getFontOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static int getIntOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static int getIntegerOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method @AnyRes public static int getResourceIdOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static String getStringOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static CharSequence[] getTextArrayOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static CharSequence getTextOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static <R> R! use(android.content.res.TypedArray, kotlin.jvm.functions.Function1<? super android.content.res.TypedArray,? extends R> block);
+  }
+
+}
+
+package androidx.database {
+
+  public final class CursorKt {
+    ctor public CursorKt();
+    method public static byte[] getBlob(android.database.Cursor, String columnName);
+    method public static byte[]? getBlobOrNull(android.database.Cursor, int index);
+    method public static error.NonExistentClass getBlobOrNull(android.database.Cursor, String columnName);
+    method public static double getDouble(android.database.Cursor, String columnName);
+    method public static Double? getDoubleOrNull(android.database.Cursor, int index);
+    method public static error.NonExistentClass getDoubleOrNull(android.database.Cursor, String columnName);
+    method public static float getFloat(android.database.Cursor, String columnName);
+    method public static Float? getFloatOrNull(android.database.Cursor, int index);
+    method public static error.NonExistentClass getFloatOrNull(android.database.Cursor, String columnName);
+    method public static int getInt(android.database.Cursor, String columnName);
+    method public static Integer? getIntOrNull(android.database.Cursor, int index);
+    method public static error.NonExistentClass getIntOrNull(android.database.Cursor, String columnName);
+    method public static long getLong(android.database.Cursor, String columnName);
+    method public static Long? getLongOrNull(android.database.Cursor, int index);
+    method public static error.NonExistentClass getLongOrNull(android.database.Cursor, String columnName);
+    method public static short getShort(android.database.Cursor, String columnName);
+    method public static Short? getShortOrNull(android.database.Cursor, int index);
+    method public static error.NonExistentClass getShortOrNull(android.database.Cursor, String columnName);
+    method public static String getString(android.database.Cursor, String columnName);
+    method public static String? getStringOrNull(android.database.Cursor, int index);
+    method public static error.NonExistentClass getStringOrNull(android.database.Cursor, String columnName);
+  }
+
+}
+
+package androidx.database.sqlite {
+
+  public final class SQLiteDatabaseKt {
+    ctor public SQLiteDatabaseKt();
+    method public static <T> T! transaction(android.database.sqlite.SQLiteDatabase, boolean exclusive = "true", kotlin.jvm.functions.Function1<? super android.database.sqlite.SQLiteDatabase,? extends T> body);
+  }
+
+}
+
+package androidx.graphics {
+
+  public final class BitmapKt {
+    ctor public BitmapKt();
+    method public static android.graphics.Bitmap applyCanvas(android.graphics.Bitmap, kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+    method public static android.graphics.Bitmap createBitmap(int width, int height, android.graphics.Bitmap.Config config = "Bitmap.Config.ARGB_8888");
+    method @RequiresApi(26) public static android.graphics.Bitmap createBitmap(int width, int height, android.graphics.Bitmap.Config config = "Bitmap.Config.ARGB_8888", boolean hasAlpha = "true", android.graphics.ColorSpace colorSpace = "ColorSpace.get(ColorSpace.Named.SRGB)");
+    method public static operator int get(android.graphics.Bitmap, int x, int y);
+    method public static android.graphics.Bitmap scale(android.graphics.Bitmap, int width, int height, boolean filter = "true");
+    method public static operator void set(android.graphics.Bitmap, int x, int y, @ColorInt int color);
+  }
+
+  public final class CanvasKt {
+    ctor public CanvasKt();
+    method public static void withRotation(android.graphics.Canvas, float degrees = "0.0f", float pivotX = "0.0f", float pivotY = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+    method public static void withSave(android.graphics.Canvas, kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+    method public static void withScale(android.graphics.Canvas, float x = "1.0f", float y = "1.0f", float pivotX = "0.0f", float pivotY = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+    method public static void withSkew(android.graphics.Canvas, float x = "0.0f", float y = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+    method public static void withTranslation(android.graphics.Canvas, float x = "0.0f", float y = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+  }
+
+  public final class ColorKt {
+    ctor public ColorKt();
+    method @RequiresApi(26) public static operator float component1(android.graphics.Color);
+    method public static operator int component1(int);
+    method @RequiresApi(26) public static operator float component1(long);
+    method @RequiresApi(26) public static operator float component2(android.graphics.Color);
+    method public static operator int component2(int);
+    method @RequiresApi(26) public static operator float component2(long);
+    method @RequiresApi(26) public static operator float component3(android.graphics.Color);
+    method public static operator int component3(int);
+    method @RequiresApi(26) public static operator float component3(long);
+    method @RequiresApi(26) public static operator float component4(android.graphics.Color);
+    method public static operator int component4(int);
+    method @RequiresApi(26) public static operator float component4(long);
+    method public static int getAlpha(int);
+    method @RequiresApi(26) public static float getAlpha(long);
+    method public static int getBlue(int);
+    method @RequiresApi(26) public static float getBlue(long);
+    method @RequiresApi(26) public static android.graphics.ColorSpace getColorSpace(long);
+    method public static int getGreen(int);
+    method @RequiresApi(26) public static float getGreen(long);
+    method @RequiresApi(26) public static float getLuminance(int);
+    method @RequiresApi(26) public static float getLuminance(long);
+    method public static int getRed(int);
+    method @RequiresApi(26) public static float getRed(long);
+    method @RequiresApi(26) public static boolean isSrgb(long);
+    method @RequiresApi(26) public static boolean isWideGamut(long);
+    method @RequiresApi(26) public static operator android.graphics.Color plus(android.graphics.Color, android.graphics.Color c);
+    method @RequiresApi(26) public static android.graphics.Color toColor(int);
+    method @RequiresApi(26) public static android.graphics.Color toColor(long);
+    method @RequiresApi(26) @ColorInt public static int toColorInt(long);
+    method @ColorInt public static int toColorInt(String);
+    method @RequiresApi(26) @ColorLong public static long toColorLong(int);
+  }
+
+  public final class MatrixKt {
+    ctor public MatrixKt();
+    method public static error.NonExistentClass rotationMatrix(float degrees, float px = "0.0f", float py = "0.0f");
+    method public static error.NonExistentClass scaleMatrix(float sx = "1.0f", float sy = "1.0f");
+    method public static operator error.NonExistentClass times(android.graphics.Matrix, android.graphics.Matrix m);
+    method public static error.NonExistentClass translationMatrix(float tx = "0.0f", float ty = "0.0f");
+    method public static error.NonExistentClass values(android.graphics.Matrix);
+  }
+
+  public final class PathKt {
+    ctor public PathKt();
+    method @RequiresApi(19) public static infix android.graphics.Path and(android.graphics.Path, android.graphics.Path p);
+    method @RequiresApi(26) public static Iterable<androidx.graphics.PathSegment> flatten(android.graphics.Path, float error = "0.5f");
+    method @RequiresApi(19) public static operator android.graphics.Path minus(android.graphics.Path, android.graphics.Path p);
+    method @RequiresApi(19) public static infix android.graphics.Path or(android.graphics.Path, android.graphics.Path p);
+    method @RequiresApi(19) public static operator android.graphics.Path plus(android.graphics.Path, android.graphics.Path p);
+    method @RequiresApi(19) public static infix android.graphics.Path xor(android.graphics.Path, android.graphics.Path p);
+  }
+
+  public final class PathSegment {
+    ctor public PathSegment(android.graphics.PointF start, float startFraction, android.graphics.PointF end, float endFraction);
+    method public android.graphics.PointF component1();
+    method public float component2();
+    method public android.graphics.PointF component3();
+    method public float component4();
+    method public androidx.graphics.PathSegment copy(android.graphics.PointF start, float startFraction, android.graphics.PointF end, float endFraction);
+    method public android.graphics.PointF getEnd();
+    method public float getEndFraction();
+    method public android.graphics.PointF getStart();
+    method public float getStartFraction();
+  }
+
+  public final class PictureKt {
+    ctor public PictureKt();
+    method public static android.graphics.Picture record(android.graphics.Picture, int width, int height, kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+  }
+
+  public final class PointKt {
+    ctor public PointKt();
+    method public static operator int component1(android.graphics.Point);
+    method public static operator float component1(android.graphics.PointF);
+    method public static operator int component2(android.graphics.Point);
+    method public static operator float component2(android.graphics.PointF);
+    method public static operator android.graphics.Point minus(android.graphics.Point, android.graphics.Point p);
+    method public static operator android.graphics.PointF minus(android.graphics.PointF, android.graphics.PointF p);
+    method public static operator android.graphics.Point minus(android.graphics.Point, int xy);
+    method public static operator android.graphics.PointF minus(android.graphics.PointF, float xy);
+    method public static operator android.graphics.Point plus(android.graphics.Point, android.graphics.Point p);
+    method public static operator android.graphics.PointF plus(android.graphics.PointF, android.graphics.PointF p);
+    method public static operator android.graphics.Point plus(android.graphics.Point, int xy);
+    method public static operator android.graphics.PointF plus(android.graphics.PointF, float xy);
+    method public static android.graphics.Point toPoint(android.graphics.PointF);
+    method public static android.graphics.PointF toPointF(android.graphics.Point);
+    method public static operator android.graphics.Point unaryMinus(android.graphics.Point);
+    method public static operator android.graphics.PointF unaryMinus(android.graphics.PointF);
+  }
+
+  public final class PorterDuffKt {
+    ctor public PorterDuffKt();
+    method public static android.graphics.PorterDuffColorFilter toColorFilter(android.graphics.PorterDuff.Mode, int color);
+    method public static android.graphics.PorterDuffXfermode toXfermode(android.graphics.PorterDuff.Mode);
+  }
+
+  public final class RectKt {
+    ctor public RectKt();
+    method public static infix android.graphics.Rect and(android.graphics.Rect, android.graphics.Rect r);
+    method public static infix android.graphics.RectF and(android.graphics.RectF, android.graphics.RectF r);
+    method public static operator int component1(android.graphics.Rect);
+    method public static operator float component1(android.graphics.RectF);
+    method public static operator int component2(android.graphics.Rect);
+    method public static operator float component2(android.graphics.RectF);
+    method public static operator int component3(android.graphics.Rect);
+    method public static operator float component3(android.graphics.RectF);
+    method public static operator int component4(android.graphics.Rect);
+    method public static operator float component4(android.graphics.RectF);
+    method public static operator boolean contains(android.graphics.Rect, android.graphics.Point p);
+    method public static operator boolean contains(android.graphics.RectF, android.graphics.PointF p);
+    method public static operator android.graphics.Region minus(android.graphics.Rect, android.graphics.Rect r);
+    method public static operator android.graphics.Region minus(android.graphics.RectF, android.graphics.RectF r);
+    method public static operator android.graphics.Rect minus(android.graphics.Rect, int xy);
+    method public static operator android.graphics.RectF minus(android.graphics.RectF, float xy);
+    method public static operator android.graphics.Rect minus(android.graphics.Rect, android.graphics.Point xy);
+    method public static operator android.graphics.RectF minus(android.graphics.RectF, android.graphics.PointF xy);
+    method public static infix android.graphics.Rect or(android.graphics.Rect, android.graphics.Rect r);
+    method public static infix android.graphics.RectF or(android.graphics.RectF, android.graphics.RectF r);
+    method public static operator android.graphics.Rect plus(android.graphics.Rect, android.graphics.Rect r);
+    method public static operator android.graphics.RectF plus(android.graphics.RectF, android.graphics.RectF r);
+    method public static operator android.graphics.Rect plus(android.graphics.Rect, int xy);
+    method public static operator android.graphics.RectF plus(android.graphics.RectF, float xy);
+    method public static operator android.graphics.Rect plus(android.graphics.Rect, android.graphics.Point xy);
+    method public static operator android.graphics.RectF plus(android.graphics.RectF, android.graphics.PointF xy);
+    method public static android.graphics.Rect toRect(android.graphics.RectF);
+    method public static android.graphics.RectF toRectF(android.graphics.Rect);
+    method public static android.graphics.Region toRegion(android.graphics.Rect);
+    method public static android.graphics.Region toRegion(android.graphics.RectF);
+    method public static error.NonExistentClass transform(android.graphics.RectF, android.graphics.Matrix m);
+    method public static infix android.graphics.Region xor(android.graphics.Rect, android.graphics.Rect r);
+    method public static infix android.graphics.Region xor(android.graphics.RectF, android.graphics.RectF r);
+  }
+
+  public final class RegionKt {
+    ctor public RegionKt();
+    method public static infix android.graphics.Region and(android.graphics.Region, android.graphics.Rect r);
+    method public static infix android.graphics.Region and(android.graphics.Region, android.graphics.Region r);
+    method public static operator boolean contains(android.graphics.Region, android.graphics.Point p);
+    method public static void forEach(android.graphics.Region, kotlin.jvm.functions.Function1<? super android.graphics.Rect,kotlin.Unit> action);
+    method public static operator java.util.Iterator<android.graphics.Rect> iterator(android.graphics.Region);
+    method public static operator android.graphics.Region minus(android.graphics.Region, android.graphics.Rect r);
+    method public static operator android.graphics.Region minus(android.graphics.Region, android.graphics.Region r);
+    method public static operator android.graphics.Region not(android.graphics.Region);
+    method public static infix android.graphics.Region or(android.graphics.Region, android.graphics.Rect r);
+    method public static infix android.graphics.Region or(android.graphics.Region, android.graphics.Region r);
+    method public static operator android.graphics.Region plus(android.graphics.Region, android.graphics.Rect r);
+    method public static operator android.graphics.Region plus(android.graphics.Region, android.graphics.Region r);
+    method public static operator android.graphics.Region unaryMinus(android.graphics.Region);
+    method public static infix android.graphics.Region xor(android.graphics.Region, android.graphics.Rect r);
+    method public static infix android.graphics.Region xor(android.graphics.Region, android.graphics.Region r);
+  }
+
+  public final class ShaderKt {
+    ctor public ShaderKt();
+    method public static void transform(android.graphics.Shader, kotlin.jvm.functions.Function1<? super android.graphics.Matrix,kotlin.Unit> block);
+  }
+
+}
+
+package androidx.graphics.drawable {
+
+  public final class BitmapDrawableKt {
+    ctor public BitmapDrawableKt();
+    method public static android.graphics.drawable.BitmapDrawable toDrawable(android.graphics.Bitmap, android.content.res.Resources resources);
+  }
+
+  public final class ColorDrawableKt {
+    ctor public ColorDrawableKt();
+    method public static android.graphics.drawable.ColorDrawable toDrawable(int);
+    method @RequiresApi(26) public static android.graphics.drawable.ColorDrawable toDrawable(android.graphics.Color);
+  }
+
+  public final class DrawableKt {
+    ctor public DrawableKt();
+    method public static android.graphics.Bitmap toBitmap(android.graphics.drawable.Drawable, @Px int width = "intrinsicWidth", @Px int height = "intrinsicHeight", android.graphics.Bitmap.Config? config = "null");
+    method public static void updateBounds(android.graphics.drawable.Drawable, @Px int left = "bounds.left", @Px int top = "bounds.top", @Px int right = "bounds.right", @Px int bottom = "bounds.bottom");
+  }
+
+  public final class IconKt {
+    ctor public IconKt();
+    method @RequiresApi(26) public static android.graphics.drawable.Icon toAdaptiveIcon(android.graphics.Bitmap);
+    method @RequiresApi(26) public static android.graphics.drawable.Icon toIcon(android.graphics.Bitmap);
+    method @RequiresApi(26) public static android.graphics.drawable.Icon toIcon(android.net.Uri);
+    method @RequiresApi(26) public static android.graphics.drawable.Icon toIcon(byte[]);
+  }
+
+}
+
+package androidx.net {
+
+  public final class UriKt {
+    ctor public UriKt();
+    method public static android.net.Uri toUri(String);
+  }
+
+}
+
+package androidx.os {
+
+  public final class BundleKt {
+    ctor public BundleKt();
+    method public static error.NonExistentClass bundleOf(kotlin.Pair<String,?>... pairs);
+  }
+
+  public final class FileKt {
+    ctor public FileKt();
+    method public static android.net.Uri toUri(java.io.File);
+  }
+
+  public final class HandlerKt {
+    ctor public HandlerKt();
+    method public static error.NonExistentClass postAtTime(android.os.Handler, long uptimeMillis, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+    method public static void postDelayed(android.os.Handler, Runnable runnable, Object? token, long delayInMillis);
+    method public static error.NonExistentClass postDelayed(android.os.Handler, long delayInMillis, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+    method public static error.NonExistentClass postDelayed(android.os.Handler, long amount, java.util.concurrent.TimeUnit unit, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+    method @RequiresApi(26) public static error.NonExistentClass postDelayed(android.os.Handler, java.time.Duration duration, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+  }
+
+  public final class PersistableBundleKt {
+    ctor public PersistableBundleKt();
+    method @RequiresApi(21) public static error.NonExistentClass persistableBundleOf(kotlin.Pair<String,?>... pairs);
+  }
+
+  public final class TraceKt {
+    ctor public TraceKt();
+    method public static <T> T! trace(String sectionName, kotlin.jvm.functions.Function0<? extends T> block);
+  }
+
+}
+
+package androidx.text {
+
+  public final class CharSequenceKt {
+    ctor public CharSequenceKt();
+    method public static boolean isDigitsOnly(CharSequence);
+    method public static int trimmedLength(CharSequence);
+  }
+
+  public final class SpannableStringBuilderKt {
+    ctor public SpannableStringBuilderKt();
+    method public static android.text.SpannableStringBuilder backgroundColor(android.text.SpannableStringBuilder, @ColorInt int color, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannableStringBuilder bold(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannedString buildSpannedString(kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannableStringBuilder color(android.text.SpannableStringBuilder, @ColorInt int color, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannableStringBuilder inSpans(android.text.SpannableStringBuilder, Object[] spans, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannableStringBuilder inSpans(android.text.SpannableStringBuilder, Object span, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannableStringBuilder italic(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannableStringBuilder scale(android.text.SpannableStringBuilder, float proportion, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannableStringBuilder strikeThrough(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannableStringBuilder underline(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+  }
+
+  public final class SpannableStringKt {
+    ctor public SpannableStringKt();
+    method public static error.NonExistentClass clearSpans(android.text.Spannable);
+    method public static operator void minusAssign(android.text.Spannable, Object span);
+    method public static operator void plusAssign(android.text.Spannable, Object span);
+    method public static android.text.Spannable toSpannable(CharSequence);
+  }
+
+  public final class SpannedStringKt {
+    ctor public SpannedStringKt();
+    method public static android.text.Spanned toSpanned(CharSequence);
+  }
+
+  public final class StringKt {
+    ctor public StringKt();
+    method public static String htmlEncode(String);
+  }
+
+}
+
+package androidx.time {
+
+  public final class DayOfWeekKt {
+    ctor public DayOfWeekKt();
+    method @RequiresApi(26) deprecated public static java.time.DayOfWeek asDayOfWeek(int);
+    method @RequiresApi(26) deprecated public static int asInt(java.time.DayOfWeek);
+  }
+
+  public final class DeprecationKt {
+    ctor public DeprecationKt();
+  }
+
+  public final class DurationKt {
+    ctor public DurationKt();
+    method @RequiresApi(26) deprecated public static operator long component1(java.time.Duration);
+    method @RequiresApi(26) deprecated public static operator int component2(java.time.Duration);
+    method @RequiresApi(26) deprecated public static operator java.time.Duration div(java.time.Duration, long divisor);
+    method @RequiresApi(26) deprecated public static java.time.Duration hours(int);
+    method @RequiresApi(26) deprecated public static java.time.Duration hours(long);
+    method @RequiresApi(26) deprecated public static java.time.Duration millis(int);
+    method @RequiresApi(26) deprecated public static java.time.Duration millis(long);
+    method @RequiresApi(26) deprecated public static java.time.Duration minutes(int);
+    method @RequiresApi(26) deprecated public static java.time.Duration minutes(long);
+    method @RequiresApi(26) deprecated public static java.time.Duration nanos(int);
+    method @RequiresApi(26) deprecated public static java.time.Duration nanos(long);
+    method @RequiresApi(26) deprecated public static java.time.Duration seconds(int);
+    method @RequiresApi(26) deprecated public static java.time.Duration seconds(long);
+    method @RequiresApi(26) deprecated public static operator java.time.Duration times(java.time.Duration, long multiplicand);
+    method @RequiresApi(26) deprecated public static operator java.time.Duration unaryMinus(java.time.Duration);
+  }
+
+  public final class InstantKt {
+    ctor public InstantKt();
+    method @RequiresApi(26) deprecated public static java.time.Instant asEpochMillis(long);
+    method @RequiresApi(26) deprecated public static java.time.Instant asEpochSeconds(long);
+    method @RequiresApi(26) deprecated public static operator long component1(java.time.Instant);
+    method @RequiresApi(26) deprecated public static operator int component2(java.time.Instant);
+  }
+
+  public final class LocalDateKt {
+    ctor public LocalDateKt();
+    method @RequiresApi(26) deprecated public static operator int component1(java.time.LocalDate);
+    method @RequiresApi(26) deprecated public static operator java.time.Month component2(java.time.LocalDate);
+    method @RequiresApi(26) deprecated public static operator int component3(java.time.LocalDate);
+  }
+
+  public final class LocalDateTimeKt {
+    ctor public LocalDateTimeKt();
+    method @RequiresApi(26) deprecated public static operator java.time.LocalDate component1(java.time.LocalDateTime);
+    method @RequiresApi(26) deprecated public static operator java.time.LocalTime component2(java.time.LocalDateTime);
+  }
+
+  public final class LocalTimeKt {
+    ctor public LocalTimeKt();
+    method @RequiresApi(26) deprecated public static operator int component1(java.time.LocalTime);
+    method @RequiresApi(26) deprecated public static operator int component2(java.time.LocalTime);
+    method @RequiresApi(26) deprecated public static operator int component3(java.time.LocalTime);
+    method @RequiresApi(26) deprecated public static operator int component4(java.time.LocalTime);
+  }
+
+  public final class MonthDayKt {
+    ctor public MonthDayKt();
+    method @RequiresApi(26) deprecated public static operator java.time.Month component1(java.time.MonthDay);
+    method @RequiresApi(26) deprecated public static operator int component2(java.time.MonthDay);
+  }
+
+  public final class MonthKt {
+    ctor public MonthKt();
+    method @RequiresApi(26) deprecated public static int asInt(java.time.Month);
+    method @RequiresApi(26) deprecated public static java.time.Month asMonth(int);
+  }
+
+  public final class OffsetDateTimeKt {
+    ctor public OffsetDateTimeKt();
+    method @RequiresApi(26) deprecated public static operator java.time.LocalDateTime component1(java.time.OffsetDateTime);
+    method @RequiresApi(26) deprecated public static operator java.time.ZoneOffset component2(java.time.OffsetDateTime);
+  }
+
+  public final class OffsetTimeKt {
+    ctor public OffsetTimeKt();
+    method @RequiresApi(26) deprecated public static operator java.time.LocalTime component1(java.time.OffsetTime);
+    method @RequiresApi(26) deprecated public static operator java.time.ZoneOffset component2(java.time.OffsetTime);
+  }
+
+  public final class PeriodKt {
+    ctor public PeriodKt();
+    method @RequiresApi(26) deprecated public static operator int component1(java.time.Period);
+    method @RequiresApi(26) deprecated public static operator int component2(java.time.Period);
+    method @RequiresApi(26) deprecated public static operator int component3(java.time.Period);
+    method @RequiresApi(26) deprecated public static java.time.Period days(int);
+    method @RequiresApi(26) deprecated public static java.time.Period months(int);
+    method @RequiresApi(26) deprecated public static operator java.time.Period times(java.time.Period, int multiplicand);
+    method @RequiresApi(26) deprecated public static operator java.time.Period unaryMinus(java.time.Period);
+    method @RequiresApi(26) deprecated public static java.time.Period years(int);
+  }
+
+  public final class YearKt {
+    ctor public YearKt();
+    method @RequiresApi(26) deprecated public static int asInt(java.time.Year);
+    method @RequiresApi(26) deprecated public static java.time.Year asYear(int);
+  }
+
+  public final class YearMonthKt {
+    ctor public YearMonthKt();
+    method @RequiresApi(26) deprecated public static operator int component1(java.time.YearMonth);
+    method @RequiresApi(26) deprecated public static operator java.time.Month component2(java.time.YearMonth);
+  }
+
+  public final class ZonedDateTimeKt {
+    ctor public ZonedDateTimeKt();
+    method @RequiresApi(26) deprecated public static operator java.time.LocalDateTime component1(java.time.ZonedDateTime);
+    method @RequiresApi(26) deprecated public static operator java.time.ZoneId component2(java.time.ZonedDateTime);
+  }
+
+}
+
+package androidx.transition {
+
+  public final class TransitionKt {
+    ctor public TransitionKt();
+    method @RequiresApi(19) public static void addListener(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onEnd = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onStart = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onCancel = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onResume = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onPause = "null");
+    method @RequiresApi(19) public static void doOnCancel(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+    method @RequiresApi(19) public static void doOnEnd(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+    method @RequiresApi(19) public static void doOnPause(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+    method @RequiresApi(19) public static void doOnResume(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+    method @RequiresApi(19) public static void doOnStart(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+  }
+
+}
+
+package androidx.util {
+
+  public final class ArrayMapKt {
+    ctor public ArrayMapKt();
+    method @RequiresApi(19) public static <K, V> android.util.ArrayMap<K,V> arrayMapOf();
+    method @RequiresApi(19) public static <K, V> android.util.ArrayMap<K,V> arrayMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
+  }
+
+  public final class ArraySetKt {
+    ctor public ArraySetKt();
+    method @RequiresApi(23) public static <T> android.util.ArraySet<T> arraySetOf();
+    method @RequiresApi(23) public static <T> android.util.ArraySet<T> arraySetOf(T... values);
+  }
+
+  public final class AtomicFileKt {
+    ctor public AtomicFileKt();
+    method @RequiresApi(17) public static byte[] readBytes(android.util.AtomicFile);
+    method @RequiresApi(17) public static String readText(android.util.AtomicFile, java.nio.charset.Charset charset = "Charsets.UTF_8");
+    method @RequiresApi(17) public static void tryWrite(android.util.AtomicFile, kotlin.jvm.functions.Function1<? super java.io.FileOutputStream,kotlin.Unit> block);
+    method @RequiresApi(17) public static void writeBytes(android.util.AtomicFile, byte[] array);
+    method @RequiresApi(17) public static void writeText(android.util.AtomicFile, String text, java.nio.charset.Charset charset = "Charsets.UTF_8");
+  }
+
+  public final class HalfKt {
+    ctor public HalfKt();
+    method @RequiresApi(26) public static android.util.Half toHalf(short);
+    method @RequiresApi(26) public static android.util.Half toHalf(float);
+    method @RequiresApi(26) public static android.util.Half toHalf(double);
+    method @RequiresApi(26) public static android.util.Half toHalf(String);
+  }
+
+  public final class LocaleKt {
+    ctor public LocaleKt();
+    method @RequiresApi(17) public static int getLayoutDirection(java.util.Locale);
+  }
+
+  public final class LongSparseArrayKt {
+    ctor public LongSparseArrayKt();
+    method @RequiresApi(16) public static operator <T> boolean contains(android.util.LongSparseArray<T>, long key);
+    method @RequiresApi(16) public static <T> boolean containsKey(android.util.LongSparseArray<T>, long key);
+    method @RequiresApi(16) public static <T> boolean containsValue(android.util.LongSparseArray<T>, T! value);
+    method @RequiresApi(16) public static <T> void forEach(android.util.LongSparseArray<T>, kotlin.jvm.functions.Function2<? super Long,? super T,kotlin.Unit> action);
+    method @RequiresApi(16) public static <T> T! getOrDefault(android.util.LongSparseArray<T>, long key, T! defaultValue);
+    method @RequiresApi(16) public static <T> T! getOrElse(android.util.LongSparseArray<T>, long key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
+    method @RequiresApi(16) public static <T> int getSize(android.util.LongSparseArray<T>);
+    method @RequiresApi(16) public static <T> boolean isEmpty(android.util.LongSparseArray<T>);
+    method @RequiresApi(16) public static <T> boolean isNotEmpty(android.util.LongSparseArray<T>);
+    method @RequiresApi(16) public static <T> kotlin.collections.LongIterator keyIterator(android.util.LongSparseArray<T>);
+    method @RequiresApi(16) public static operator <T> android.util.LongSparseArray<T> plus(android.util.LongSparseArray<T>, android.util.LongSparseArray<T> other);
+    method @RequiresApi(16) public static <T> void putAll(android.util.LongSparseArray<T>, android.util.LongSparseArray<T> other);
+    method @RequiresApi(16) public static <T> boolean remove(android.util.LongSparseArray<T>, long key, T! value);
+    method @RequiresApi(16) public static operator <T> void set(android.util.LongSparseArray<T>, long key, T! value);
+    method @RequiresApi(16) public static <T> java.util.Iterator<T> valueIterator(android.util.LongSparseArray<T>);
+  }
+
+  public final class LruCacheKt {
+    ctor public LruCacheKt();
+    method public static <K, V> android.util.LruCache<K,V> lruCache(int maxSize, kotlin.jvm.functions.Function2<? super K,? super V,Integer> sizeOf = "{ _, _ -> 1 }", kotlin.jvm.functions.Function1<? super K,? extends V> create = "{ null as V? }", kotlin.jvm.functions.Function4<? super Boolean,? super K,? super V,? super V,kotlin.Unit> onEntryRemoved = "{ _, _, _, _ -> }");
+  }
+
+  public final class PairKt {
+    ctor public PairKt();
+    method public static operator <F, S> F! component1(android.util.Pair<F,S>);
+    method public static operator <F, S> S! component2(android.util.Pair<F,S>);
+    method public static <F, S> android.util.Pair<F,S> toAndroidPair(kotlin.Pair<? extends F,? extends S>);
+    method public static <F, S> kotlin.Pair<F,S> toKotlinPair(android.util.Pair<F,S>);
+  }
+
+  public final class RangeKt {
+    ctor public RangeKt();
+    method @RequiresApi(21) public static infix <T extends java.lang.Comparable<? super T>> android.util.Range<T> and(android.util.Range<T>, android.util.Range<T> other);
+    method @RequiresApi(21) public static operator <T extends java.lang.Comparable<? super T>> android.util.Range<T> plus(android.util.Range<T>, T value);
+    method @RequiresApi(21) public static operator <T extends java.lang.Comparable<? super T>> android.util.Range<T> plus(android.util.Range<T>, android.util.Range<T> other);
+    method @RequiresApi(21) public static infix <T extends java.lang.Comparable<? super T>> android.util.Range<T> rangeTo(T, T that);
+    method @RequiresApi(21) public static <T extends java.lang.Comparable<? super T>> kotlin.ranges.ClosedRange<T> toClosedRange(android.util.Range<T>);
+    method @RequiresApi(21) public static <T extends java.lang.Comparable<? super T>> android.util.Range<T> toRange(kotlin.ranges.ClosedRange<T>);
+  }
+
+  public final class SizeKt {
+    ctor public SizeKt();
+    method @RequiresApi(21) public static operator int component1(android.util.Size);
+    method @RequiresApi(21) public static operator float component1(android.util.SizeF);
+    method @RequiresApi(21) public static operator int component2(android.util.Size);
+    method @RequiresApi(21) public static operator float component2(android.util.SizeF);
+  }
+
+  public final class SparseArrayKt {
+    ctor public SparseArrayKt();
+    method public static operator <T> boolean contains(android.util.SparseArray<T>, int key);
+    method public static <T> boolean containsKey(android.util.SparseArray<T>, int key);
+    method public static <T> boolean containsValue(android.util.SparseArray<T>, T! value);
+    method public static <T> void forEach(android.util.SparseArray<T>, kotlin.jvm.functions.Function2<? super Integer,? super T,kotlin.Unit> action);
+    method public static <T> T! getOrDefault(android.util.SparseArray<T>, int key, T! defaultValue);
+    method public static <T> T! getOrElse(android.util.SparseArray<T>, int key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
+    method public static <T> int getSize(android.util.SparseArray<T>);
+    method public static <T> boolean isEmpty(android.util.SparseArray<T>);
+    method public static <T> boolean isNotEmpty(android.util.SparseArray<T>);
+    method public static <T> kotlin.collections.IntIterator keyIterator(android.util.SparseArray<T>);
+    method public static operator <T> android.util.SparseArray<T> plus(android.util.SparseArray<T>, android.util.SparseArray<T> other);
+    method public static <T> void putAll(android.util.SparseArray<T>, android.util.SparseArray<T> other);
+    method public static <T> boolean remove(android.util.SparseArray<T>, int key, T! value);
+    method public static operator <T> void set(android.util.SparseArray<T>, int key, T! value);
+    method public static <T> java.util.Iterator<T> valueIterator(android.util.SparseArray<T>);
+  }
+
+  public final class SparseBooleanArrayKt {
+    ctor public SparseBooleanArrayKt();
+    method public static operator boolean contains(android.util.SparseBooleanArray, int key);
+    method public static boolean containsKey(android.util.SparseBooleanArray, int key);
+    method public static boolean containsValue(android.util.SparseBooleanArray, boolean value);
+    method public static void forEach(android.util.SparseBooleanArray, kotlin.jvm.functions.Function2<? super Integer,? super Boolean,kotlin.Unit> action);
+    method public static boolean getOrDefault(android.util.SparseBooleanArray, int key, boolean defaultValue);
+    method public static error.NonExistentClass getOrElse(android.util.SparseBooleanArray, int key, kotlin.jvm.functions.Function0<Boolean> defaultValue);
+    method public static int getSize(android.util.SparseBooleanArray);
+    method public static boolean isEmpty(android.util.SparseBooleanArray);
+    method public static boolean isNotEmpty(android.util.SparseBooleanArray);
+    method public static kotlin.collections.IntIterator keyIterator(android.util.SparseBooleanArray);
+    method public static operator android.util.SparseBooleanArray plus(android.util.SparseBooleanArray, android.util.SparseBooleanArray other);
+    method public static void putAll(android.util.SparseBooleanArray, android.util.SparseBooleanArray other);
+    method public static boolean remove(android.util.SparseBooleanArray, int key, boolean value);
+    method public static void removeAt(android.util.SparseBooleanArray, int index);
+    method public static operator void set(android.util.SparseBooleanArray, int key, boolean value);
+    method public static kotlin.collections.BooleanIterator valueIterator(android.util.SparseBooleanArray);
+  }
+
+  public final class SparseIntArrayKt {
+    ctor public SparseIntArrayKt();
+    method public static operator boolean contains(android.util.SparseIntArray, int key);
+    method public static boolean containsKey(android.util.SparseIntArray, int key);
+    method public static boolean containsValue(android.util.SparseIntArray, int value);
+    method public static void forEach(android.util.SparseIntArray, kotlin.jvm.functions.Function2<? super Integer,? super Integer,kotlin.Unit> action);
+    method public static int getOrDefault(android.util.SparseIntArray, int key, int defaultValue);
+    method public static error.NonExistentClass getOrElse(android.util.SparseIntArray, int key, kotlin.jvm.functions.Function0<Integer> defaultValue);
+    method public static int getSize(android.util.SparseIntArray);
+    method public static boolean isEmpty(android.util.SparseIntArray);
+    method public static boolean isNotEmpty(android.util.SparseIntArray);
+    method public static kotlin.collections.IntIterator keyIterator(android.util.SparseIntArray);
+    method public static operator android.util.SparseIntArray plus(android.util.SparseIntArray, android.util.SparseIntArray other);
+    method public static void putAll(android.util.SparseIntArray, android.util.SparseIntArray other);
+    method public static boolean remove(android.util.SparseIntArray, int key, int value);
+    method public static operator void set(android.util.SparseIntArray, int key, int value);
+    method public static kotlin.collections.IntIterator valueIterator(android.util.SparseIntArray);
+  }
+
+  public final class SparseLongArrayKt {
+    ctor public SparseLongArrayKt();
+    method @RequiresApi(18) public static operator boolean contains(android.util.SparseLongArray, int key);
+    method @RequiresApi(18) public static boolean containsKey(android.util.SparseLongArray, int key);
+    method @RequiresApi(18) public static boolean containsValue(android.util.SparseLongArray, long value);
+    method @RequiresApi(18) public static void forEach(android.util.SparseLongArray, kotlin.jvm.functions.Function2<? super Integer,? super Long,kotlin.Unit> action);
+    method @RequiresApi(18) public static long getOrDefault(android.util.SparseLongArray, int key, long defaultValue);
+    method @RequiresApi(18) public static error.NonExistentClass getOrElse(android.util.SparseLongArray, int key, kotlin.jvm.functions.Function0<Long> defaultValue);
+    method @RequiresApi(18) public static int getSize(android.util.SparseLongArray);
+    method @RequiresApi(18) public static boolean isEmpty(android.util.SparseLongArray);
+    method @RequiresApi(18) public static boolean isNotEmpty(android.util.SparseLongArray);
+    method @RequiresApi(18) public static kotlin.collections.IntIterator keyIterator(android.util.SparseLongArray);
+    method @RequiresApi(18) public static operator android.util.SparseLongArray plus(android.util.SparseLongArray, android.util.SparseLongArray other);
+    method @RequiresApi(18) public static void putAll(android.util.SparseLongArray, android.util.SparseLongArray other);
+    method @RequiresApi(18) public static boolean remove(android.util.SparseLongArray, int key, long value);
+    method @RequiresApi(18) public static operator void set(android.util.SparseLongArray, int key, long value);
+    method @RequiresApi(18) public static kotlin.collections.LongIterator valueIterator(android.util.SparseLongArray);
+  }
+
+}
+
+package androidx.view {
+
+  public final class MenuKt {
+    ctor public MenuKt();
+    method public static operator boolean contains(android.view.Menu, android.view.MenuItem item);
+    method public static void forEach(android.view.Menu, kotlin.jvm.functions.Function1<? super android.view.MenuItem,kotlin.Unit> action);
+    method public static void forEachIndexed(android.view.Menu, kotlin.jvm.functions.Function2<? super Integer,? super android.view.MenuItem,kotlin.Unit> action);
+    method public static operator android.view.MenuItem get(android.view.Menu, int index);
+    method public static int getSize(android.view.Menu);
+    method public static boolean isEmpty(android.view.Menu);
+    method public static boolean isNotEmpty(android.view.Menu);
+    method public static operator java.util.Iterator<android.view.MenuItem> iterator(android.view.Menu);
+  }
+
+  public final class ViewGroupKt {
+    ctor public ViewGroupKt();
+    method public static operator boolean contains(android.view.ViewGroup, android.view.View view);
+    method public static void forEach(android.view.ViewGroup, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+    method public static void forEachIndexed(android.view.ViewGroup, kotlin.jvm.functions.Function2<? super Integer,? super android.view.View,kotlin.Unit> action);
+    method public static operator android.view.View get(android.view.ViewGroup, int index);
+    method public static kotlin.sequences.Sequence<android.view.View> getChildren(android.view.ViewGroup);
+    method public static int getSize(android.view.ViewGroup);
+    method public static boolean isEmpty(android.view.ViewGroup);
+    method public static boolean isNotEmpty(android.view.ViewGroup);
+    method public static operator java.util.Iterator<android.view.View> iterator(android.view.ViewGroup);
+    method public static operator void minusAssign(android.view.ViewGroup, android.view.View view);
+    method public static operator void plusAssign(android.view.ViewGroup, android.view.View view);
+    method public static void setMargins(android.view.ViewGroup.MarginLayoutParams, @Px int size);
+    method public static void updateLayoutParams(android.view.ViewGroup, kotlin.jvm.functions.Function1<? super android.view.ViewGroup.LayoutParams,kotlin.Unit> block);
+    method public static void updateMargins(android.view.ViewGroup.MarginLayoutParams, @Px int left = "leftMargin", @Px int top = "topMargin", @Px int right = "rightMargin", @Px int bottom = "bottomMargin");
+    method @RequiresApi(17) public static void updateMarginsRelative(android.view.ViewGroup.MarginLayoutParams, @Px int start = "marginStart", @Px int top = "topMargin", @Px int end = "marginEnd", @Px int bottom = "bottomMargin");
+  }
+
+  public final class ViewKt {
+    ctor public ViewKt();
+    method public static void doOnLayout(android.view.View, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+    method public static void doOnNextLayout(android.view.View, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+    method public static void doOnPreDraw(android.view.View, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+    method public static boolean isGone(android.view.View);
+    method public static boolean isInvisible(android.view.View);
+    method public static boolean isVisible(android.view.View);
+    method public static Runnable postDelayed(android.view.View, long delayInMillis, kotlin.jvm.functions.Function0<kotlin.Unit> action);
+    method @RequiresApi(16) public static Runnable postOnAnimationDelayed(android.view.View, long delayInMillis, kotlin.jvm.functions.Function0<kotlin.Unit> action);
+    method public static void setGone(android.view.View, boolean value);
+    method public static void setInvisible(android.view.View, boolean value);
+    method public static void setPadding(android.view.View, @Px int size);
+    method public static void setVisible(android.view.View, boolean value);
+    method public static android.graphics.Bitmap toBitmap(android.view.View, android.graphics.Bitmap.Config config = "Bitmap.Config.ARGB_8888");
+    method public static void updatePadding(android.view.View, @Px int left = "paddingLeft", @Px int top = "paddingTop", @Px int right = "paddingRight", @Px int bottom = "paddingBottom");
+    method @RequiresApi(17) public static void updatePaddingRelative(android.view.View, @Px int start = "paddingStart", @Px int top = "paddingTop", @Px int end = "paddingEnd", @Px int bottom = "paddingBottom");
+  }
+
+}
+
diff --git a/core/ktx/api/current.txt b/core/ktx/api/current.txt
new file mode 100644
index 0000000..4c9c96a
--- /dev/null
+++ b/core/ktx/api/current.txt
@@ -0,0 +1,673 @@
+package androidx.core.animation {
+
+  public final class AnimatorKt {
+    ctor public AnimatorKt();
+    method public static android.animation.Animator.AnimatorListener addListener(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onEnd = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onStart = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onCancel = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onRepeat = "null");
+    method @RequiresApi(19) public static android.animation.Animator.AnimatorPauseListener addPauseListener(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onResume = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onPause = "null");
+    method public static android.animation.Animator.AnimatorListener doOnCancel(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+    method public static android.animation.Animator.AnimatorListener doOnEnd(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+    method @RequiresApi(19) public static android.animation.Animator.AnimatorPauseListener doOnPause(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+    method public static android.animation.Animator.AnimatorListener doOnRepeat(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+    method @RequiresApi(19) public static android.animation.Animator.AnimatorPauseListener doOnResume(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+    method public static android.animation.Animator.AnimatorListener doOnStart(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+  }
+
+}
+
+package androidx.core.content {
+
+  public final class ContentValuesKt {
+    ctor public ContentValuesKt();
+    method public static error.NonExistentClass contentValuesOf(kotlin.Pair<java.lang.String,?>... pairs);
+  }
+
+  public final class ContextKt {
+    ctor public ContextKt();
+    method public static void withStyledAttributes(android.content.Context, android.util.AttributeSet? set = "null", int[] attrs, @AttrRes int defStyleAttr = "0", @StyleRes int defStyleRes = "0", kotlin.jvm.functions.Function1<? super android.content.res.TypedArray,kotlin.Unit> block);
+    method public static void withStyledAttributes(android.content.Context, @StyleRes int resourceId, int[] attrs, kotlin.jvm.functions.Function1<? super android.content.res.TypedArray,kotlin.Unit> block);
+  }
+
+  public final class SharedPreferencesKt {
+    ctor public SharedPreferencesKt();
+    method public static void edit(android.content.SharedPreferences, boolean commit = "false", kotlin.jvm.functions.Function1<? super android.content.SharedPreferences.Editor,kotlin.Unit> action);
+  }
+
+}
+
+package androidx.core.content.res {
+
+  public final class TypedArrayKt {
+    ctor public TypedArrayKt();
+    method public static boolean getBooleanOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method @ColorInt public static int getColorOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static android.content.res.ColorStateList getColorStateListOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static float getDimensionOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method @Dimension public static int getDimensionPixelOffsetOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method @Dimension public static int getDimensionPixelSizeOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static android.graphics.drawable.Drawable getDrawableOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static float getFloatOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method @RequiresApi(26) public static android.graphics.Typeface getFontOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static int getIntOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static int getIntegerOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method @AnyRes public static int getResourceIdOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static String getStringOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static CharSequence[] getTextArrayOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static CharSequence getTextOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+    method public static <R> R! use(android.content.res.TypedArray, kotlin.jvm.functions.Function1<? super android.content.res.TypedArray,? extends R> block);
+  }
+
+}
+
+package androidx.core.database {
+
+  public final class CursorKt {
+    ctor public CursorKt();
+    method public static byte[] getBlob(android.database.Cursor, String columnName);
+    method public static byte[]? getBlobOrNull(android.database.Cursor, int index);
+    method public static error.NonExistentClass getBlobOrNull(android.database.Cursor, String columnName);
+    method public static double getDouble(android.database.Cursor, String columnName);
+    method public static Double? getDoubleOrNull(android.database.Cursor, int index);
+    method public static error.NonExistentClass getDoubleOrNull(android.database.Cursor, String columnName);
+    method public static float getFloat(android.database.Cursor, String columnName);
+    method public static Float? getFloatOrNull(android.database.Cursor, int index);
+    method public static error.NonExistentClass getFloatOrNull(android.database.Cursor, String columnName);
+    method public static int getInt(android.database.Cursor, String columnName);
+    method public static Integer? getIntOrNull(android.database.Cursor, int index);
+    method public static error.NonExistentClass getIntOrNull(android.database.Cursor, String columnName);
+    method public static long getLong(android.database.Cursor, String columnName);
+    method public static Long? getLongOrNull(android.database.Cursor, int index);
+    method public static error.NonExistentClass getLongOrNull(android.database.Cursor, String columnName);
+    method public static short getShort(android.database.Cursor, String columnName);
+    method public static Short? getShortOrNull(android.database.Cursor, int index);
+    method public static error.NonExistentClass getShortOrNull(android.database.Cursor, String columnName);
+    method public static String getString(android.database.Cursor, String columnName);
+    method public static String? getStringOrNull(android.database.Cursor, int index);
+    method public static error.NonExistentClass getStringOrNull(android.database.Cursor, String columnName);
+  }
+
+}
+
+package androidx.core.database.sqlite {
+
+  public final class SQLiteDatabaseKt {
+    ctor public SQLiteDatabaseKt();
+    method public static <T> T! transaction(android.database.sqlite.SQLiteDatabase, boolean exclusive = "true", kotlin.jvm.functions.Function1<? super android.database.sqlite.SQLiteDatabase,? extends T> body);
+  }
+
+}
+
+package androidx.core.graphics {
+
+  public final class BitmapKt {
+    ctor public BitmapKt();
+    method public static android.graphics.Bitmap applyCanvas(android.graphics.Bitmap, kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+    method public static android.graphics.Bitmap createBitmap(int width, int height, android.graphics.Bitmap.Config config = "Bitmap.Config.ARGB_8888");
+    method @RequiresApi(26) public static android.graphics.Bitmap createBitmap(int width, int height, android.graphics.Bitmap.Config config = "Bitmap.Config.ARGB_8888", boolean hasAlpha = "true", android.graphics.ColorSpace colorSpace = "ColorSpace.get(ColorSpace.Named.SRGB)");
+    method public static operator int get(android.graphics.Bitmap, int x, int y);
+    method public static android.graphics.Bitmap scale(android.graphics.Bitmap, int width, int height, boolean filter = "true");
+    method public static operator void set(android.graphics.Bitmap, int x, int y, @ColorInt int color);
+  }
+
+  public final class CanvasKt {
+    ctor public CanvasKt();
+    method public static void withMatrix(android.graphics.Canvas, android.graphics.Matrix matrix = "Matrix()", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+    method public static void withRotation(android.graphics.Canvas, float degrees = "0.0f", float pivotX = "0.0f", float pivotY = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+    method public static void withSave(android.graphics.Canvas, kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+    method public static void withScale(android.graphics.Canvas, float x = "1.0f", float y = "1.0f", float pivotX = "0.0f", float pivotY = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+    method public static void withSkew(android.graphics.Canvas, float x = "0.0f", float y = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+    method public static void withTranslation(android.graphics.Canvas, float x = "0.0f", float y = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+  }
+
+  public final class ColorKt {
+    ctor public ColorKt();
+    method @RequiresApi(26) public static operator float component1(android.graphics.Color);
+    method public static operator int component1(int);
+    method @RequiresApi(26) public static operator float component1(long);
+    method @RequiresApi(26) public static operator float component2(android.graphics.Color);
+    method public static operator int component2(int);
+    method @RequiresApi(26) public static operator float component2(long);
+    method @RequiresApi(26) public static operator float component3(android.graphics.Color);
+    method public static operator int component3(int);
+    method @RequiresApi(26) public static operator float component3(long);
+    method @RequiresApi(26) public static operator float component4(android.graphics.Color);
+    method public static operator int component4(int);
+    method @RequiresApi(26) public static operator float component4(long);
+    method public static int getAlpha(int);
+    method @RequiresApi(26) public static float getAlpha(long);
+    method public static int getBlue(int);
+    method @RequiresApi(26) public static float getBlue(long);
+    method @RequiresApi(26) public static android.graphics.ColorSpace getColorSpace(long);
+    method public static int getGreen(int);
+    method @RequiresApi(26) public static float getGreen(long);
+    method @RequiresApi(26) public static float getLuminance(int);
+    method @RequiresApi(26) public static float getLuminance(long);
+    method public static int getRed(int);
+    method @RequiresApi(26) public static float getRed(long);
+    method @RequiresApi(26) public static boolean isSrgb(long);
+    method @RequiresApi(26) public static boolean isWideGamut(long);
+    method @RequiresApi(26) public static operator android.graphics.Color plus(android.graphics.Color, android.graphics.Color c);
+    method @RequiresApi(26) public static android.graphics.Color toColor(int);
+    method @RequiresApi(26) public static android.graphics.Color toColor(long);
+    method @RequiresApi(26) @ColorInt public static int toColorInt(long);
+    method @ColorInt public static int toColorInt(String);
+    method @RequiresApi(26) @ColorLong public static long toColorLong(int);
+  }
+
+  public final class MatrixKt {
+    ctor public MatrixKt();
+    method public static error.NonExistentClass rotationMatrix(float degrees, float px = "0.0f", float py = "0.0f");
+    method public static error.NonExistentClass scaleMatrix(float sx = "1.0f", float sy = "1.0f");
+    method public static operator error.NonExistentClass times(android.graphics.Matrix, android.graphics.Matrix m);
+    method public static error.NonExistentClass translationMatrix(float tx = "0.0f", float ty = "0.0f");
+    method public static error.NonExistentClass values(android.graphics.Matrix);
+  }
+
+  public final class PathKt {
+    ctor public PathKt();
+    method @RequiresApi(19) public static infix android.graphics.Path and(android.graphics.Path, android.graphics.Path p);
+    method @RequiresApi(26) public static Iterable<androidx.core.graphics.PathSegment> flatten(android.graphics.Path, float error = "0.5f");
+    method @RequiresApi(19) public static operator android.graphics.Path minus(android.graphics.Path, android.graphics.Path p);
+    method @RequiresApi(19) public static infix android.graphics.Path or(android.graphics.Path, android.graphics.Path p);
+    method @RequiresApi(19) public static operator android.graphics.Path plus(android.graphics.Path, android.graphics.Path p);
+    method @RequiresApi(19) public static infix android.graphics.Path xor(android.graphics.Path, android.graphics.Path p);
+  }
+
+  public final class PathSegment {
+    ctor public PathSegment(android.graphics.PointF start, float startFraction, android.graphics.PointF end, float endFraction);
+    method public android.graphics.PointF component1();
+    method public float component2();
+    method public android.graphics.PointF component3();
+    method public float component4();
+    method public androidx.core.graphics.PathSegment copy(android.graphics.PointF start, float startFraction, android.graphics.PointF end, float endFraction);
+    method public android.graphics.PointF getEnd();
+    method public float getEndFraction();
+    method public android.graphics.PointF getStart();
+    method public float getStartFraction();
+  }
+
+  public final class PictureKt {
+    ctor public PictureKt();
+    method public static android.graphics.Picture record(android.graphics.Picture, int width, int height, kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+  }
+
+  public final class PointKt {
+    ctor public PointKt();
+    method public static operator int component1(android.graphics.Point);
+    method public static operator float component1(android.graphics.PointF);
+    method public static operator int component2(android.graphics.Point);
+    method public static operator float component2(android.graphics.PointF);
+    method public static operator android.graphics.Point minus(android.graphics.Point, android.graphics.Point p);
+    method public static operator android.graphics.PointF minus(android.graphics.PointF, android.graphics.PointF p);
+    method public static operator android.graphics.Point minus(android.graphics.Point, int xy);
+    method public static operator android.graphics.PointF minus(android.graphics.PointF, float xy);
+    method public static operator android.graphics.Point plus(android.graphics.Point, android.graphics.Point p);
+    method public static operator android.graphics.PointF plus(android.graphics.PointF, android.graphics.PointF p);
+    method public static operator android.graphics.Point plus(android.graphics.Point, int xy);
+    method public static operator android.graphics.PointF plus(android.graphics.PointF, float xy);
+    method public static android.graphics.Point toPoint(android.graphics.PointF);
+    method public static android.graphics.PointF toPointF(android.graphics.Point);
+    method public static operator android.graphics.Point unaryMinus(android.graphics.Point);
+    method public static operator android.graphics.PointF unaryMinus(android.graphics.PointF);
+  }
+
+  public final class PorterDuffKt {
+    ctor public PorterDuffKt();
+    method public static android.graphics.PorterDuffColorFilter toColorFilter(android.graphics.PorterDuff.Mode, int color);
+    method public static android.graphics.PorterDuffXfermode toXfermode(android.graphics.PorterDuff.Mode);
+  }
+
+  public final class RectKt {
+    ctor public RectKt();
+    method public static infix android.graphics.Rect and(android.graphics.Rect, android.graphics.Rect r);
+    method public static infix android.graphics.RectF and(android.graphics.RectF, android.graphics.RectF r);
+    method public static operator int component1(android.graphics.Rect);
+    method public static operator float component1(android.graphics.RectF);
+    method public static operator int component2(android.graphics.Rect);
+    method public static operator float component2(android.graphics.RectF);
+    method public static operator int component3(android.graphics.Rect);
+    method public static operator float component3(android.graphics.RectF);
+    method public static operator int component4(android.graphics.Rect);
+    method public static operator float component4(android.graphics.RectF);
+    method public static operator boolean contains(android.graphics.Rect, android.graphics.Point p);
+    method public static operator boolean contains(android.graphics.RectF, android.graphics.PointF p);
+    method public static operator android.graphics.Region minus(android.graphics.Rect, android.graphics.Rect r);
+    method public static operator android.graphics.Region minus(android.graphics.RectF, android.graphics.RectF r);
+    method public static operator android.graphics.Rect minus(android.graphics.Rect, int xy);
+    method public static operator android.graphics.RectF minus(android.graphics.RectF, float xy);
+    method public static operator android.graphics.Rect minus(android.graphics.Rect, android.graphics.Point xy);
+    method public static operator android.graphics.RectF minus(android.graphics.RectF, android.graphics.PointF xy);
+    method public static infix android.graphics.Rect or(android.graphics.Rect, android.graphics.Rect r);
+    method public static infix android.graphics.RectF or(android.graphics.RectF, android.graphics.RectF r);
+    method public static operator android.graphics.Rect plus(android.graphics.Rect, android.graphics.Rect r);
+    method public static operator android.graphics.RectF plus(android.graphics.RectF, android.graphics.RectF r);
+    method public static operator android.graphics.Rect plus(android.graphics.Rect, int xy);
+    method public static operator android.graphics.RectF plus(android.graphics.RectF, float xy);
+    method public static operator android.graphics.Rect plus(android.graphics.Rect, android.graphics.Point xy);
+    method public static operator android.graphics.RectF plus(android.graphics.RectF, android.graphics.PointF xy);
+    method public static android.graphics.Rect toRect(android.graphics.RectF);
+    method public static android.graphics.RectF toRectF(android.graphics.Rect);
+    method public static android.graphics.Region toRegion(android.graphics.Rect);
+    method public static android.graphics.Region toRegion(android.graphics.RectF);
+    method public static error.NonExistentClass transform(android.graphics.RectF, android.graphics.Matrix m);
+    method public static infix android.graphics.Region xor(android.graphics.Rect, android.graphics.Rect r);
+    method public static infix android.graphics.Region xor(android.graphics.RectF, android.graphics.RectF r);
+  }
+
+  public final class RegionKt {
+    ctor public RegionKt();
+    method public static infix android.graphics.Region and(android.graphics.Region, android.graphics.Rect r);
+    method public static infix android.graphics.Region and(android.graphics.Region, android.graphics.Region r);
+    method public static operator boolean contains(android.graphics.Region, android.graphics.Point p);
+    method public static void forEach(android.graphics.Region, kotlin.jvm.functions.Function1<? super android.graphics.Rect,kotlin.Unit> action);
+    method public static operator java.util.Iterator<android.graphics.Rect> iterator(android.graphics.Region);
+    method public static operator android.graphics.Region minus(android.graphics.Region, android.graphics.Rect r);
+    method public static operator android.graphics.Region minus(android.graphics.Region, android.graphics.Region r);
+    method public static operator android.graphics.Region not(android.graphics.Region);
+    method public static infix android.graphics.Region or(android.graphics.Region, android.graphics.Rect r);
+    method public static infix android.graphics.Region or(android.graphics.Region, android.graphics.Region r);
+    method public static operator android.graphics.Region plus(android.graphics.Region, android.graphics.Rect r);
+    method public static operator android.graphics.Region plus(android.graphics.Region, android.graphics.Region r);
+    method public static operator android.graphics.Region unaryMinus(android.graphics.Region);
+    method public static infix android.graphics.Region xor(android.graphics.Region, android.graphics.Rect r);
+    method public static infix android.graphics.Region xor(android.graphics.Region, android.graphics.Region r);
+  }
+
+  public final class ShaderKt {
+    ctor public ShaderKt();
+    method public static void transform(android.graphics.Shader, kotlin.jvm.functions.Function1<? super android.graphics.Matrix,kotlin.Unit> block);
+  }
+
+}
+
+package androidx.core.graphics.drawable {
+
+  public final class BitmapDrawableKt {
+    ctor public BitmapDrawableKt();
+    method public static android.graphics.drawable.BitmapDrawable toDrawable(android.graphics.Bitmap, android.content.res.Resources resources);
+  }
+
+  public final class ColorDrawableKt {
+    ctor public ColorDrawableKt();
+    method public static android.graphics.drawable.ColorDrawable toDrawable(int);
+    method @RequiresApi(26) public static android.graphics.drawable.ColorDrawable toDrawable(android.graphics.Color);
+  }
+
+  public final class DrawableKt {
+    ctor public DrawableKt();
+    method public static android.graphics.Bitmap toBitmap(android.graphics.drawable.Drawable, @Px int width = "intrinsicWidth", @Px int height = "intrinsicHeight", android.graphics.Bitmap.Config? config = "null");
+    method public static void updateBounds(android.graphics.drawable.Drawable, @Px int left = "bounds.left", @Px int top = "bounds.top", @Px int right = "bounds.right", @Px int bottom = "bounds.bottom");
+  }
+
+  public final class IconKt {
+    ctor public IconKt();
+    method @RequiresApi(26) public static android.graphics.drawable.Icon toAdaptiveIcon(android.graphics.Bitmap);
+    method @RequiresApi(26) public static android.graphics.drawable.Icon toIcon(android.graphics.Bitmap);
+    method @RequiresApi(26) public static android.graphics.drawable.Icon toIcon(android.net.Uri);
+    method @RequiresApi(26) public static android.graphics.drawable.Icon toIcon(byte[]);
+  }
+
+}
+
+package androidx.core.location {
+
+  public final class LocationKt {
+    ctor public LocationKt();
+    method public static operator double component1(android.location.Location);
+    method public static operator double component2(android.location.Location);
+  }
+
+}
+
+package androidx.core.net {
+
+  public final class UriKt {
+    ctor public UriKt();
+    method public static java.io.File toFile(android.net.Uri);
+    method public static android.net.Uri toUri(String);
+    method public static android.net.Uri toUri(java.io.File);
+  }
+
+}
+
+package androidx.core.os {
+
+  public final class BundleKt {
+    ctor public BundleKt();
+    method public static error.NonExistentClass bundleOf(kotlin.Pair<java.lang.String,?>... pairs);
+  }
+
+  public final class HandlerKt {
+    ctor public HandlerKt();
+    method public static Runnable postAtTime(android.os.Handler, long uptimeMillis, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+    method public static Runnable postDelayed(android.os.Handler, long delayInMillis, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+  }
+
+  public final class PersistableBundleKt {
+    ctor public PersistableBundleKt();
+    method @RequiresApi(21) public static error.NonExistentClass persistableBundleOf(kotlin.Pair<java.lang.String,?>... pairs);
+  }
+
+  public final class TraceKt {
+    ctor public TraceKt();
+    method public static <T> T! trace(String sectionName, kotlin.jvm.functions.Function0<? extends T> block);
+  }
+
+}
+
+package androidx.core.preference {
+
+  public final class PreferenceGroupKt {
+    ctor public PreferenceGroupKt();
+    method public static operator boolean contains(android.preference.PreferenceGroup, android.preference.Preference preference);
+    method public static void forEach(android.preference.PreferenceGroup, kotlin.jvm.functions.Function1<? super android.preference.Preference,kotlin.Unit> action);
+    method public static void forEachIndexed(android.preference.PreferenceGroup, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super android.preference.Preference,kotlin.Unit> action);
+    method public static operator android.preference.Preference get(android.preference.PreferenceGroup, CharSequence key);
+    method public static operator android.preference.Preference get(android.preference.PreferenceGroup, int index);
+    method public static kotlin.sequences.Sequence<android.preference.Preference> getChildren(android.preference.PreferenceGroup);
+    method public static int getSize(android.preference.PreferenceGroup);
+    method public static boolean isEmpty(android.preference.PreferenceGroup);
+    method public static boolean isNotEmpty(android.preference.PreferenceGroup);
+    method public static operator java.util.Iterator<android.preference.Preference> iterator(android.preference.PreferenceGroup);
+    method public static operator void minusAssign(android.preference.PreferenceGroup, android.preference.Preference preference);
+    method public static operator void plusAssign(android.preference.PreferenceGroup, android.preference.Preference preference);
+  }
+
+}
+
+package androidx.core.text {
+
+  public final class CharSequenceKt {
+    ctor public CharSequenceKt();
+    method public static boolean isDigitsOnly(CharSequence);
+    method public static int trimmedLength(CharSequence);
+  }
+
+  public final class HtmlKt {
+    ctor public HtmlKt();
+    method public static android.text.Spanned parseAsHtml(String, int flags = "FROM_HTML_MODE_LEGACY", android.text.Html.ImageGetter? imageGetter = "null", android.text.Html.TagHandler? tagHandler = "null");
+    method public static String toHtml(android.text.Spanned, int option = "TO_HTML_PARAGRAPH_LINES_CONSECUTIVE");
+  }
+
+  public final class SpannableStringBuilderKt {
+    ctor public SpannableStringBuilderKt();
+    method public static android.text.SpannableStringBuilder backgroundColor(android.text.SpannableStringBuilder, @ColorInt int color, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannableStringBuilder bold(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannedString buildSpannedString(kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannableStringBuilder color(android.text.SpannableStringBuilder, @ColorInt int color, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannableStringBuilder inSpans(android.text.SpannableStringBuilder, Object[] spans, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannableStringBuilder inSpans(android.text.SpannableStringBuilder, Object span, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannableStringBuilder italic(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannableStringBuilder scale(android.text.SpannableStringBuilder, float proportion, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannableStringBuilder strikeThrough(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannableStringBuilder subscript(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannableStringBuilder superscript(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+    method public static android.text.SpannableStringBuilder underline(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+  }
+
+  public final class SpannableStringKt {
+    ctor public SpannableStringKt();
+    method public static error.NonExistentClass clearSpans(android.text.Spannable);
+    method public static operator void minusAssign(android.text.Spannable, Object span);
+    method public static operator void plusAssign(android.text.Spannable, Object span);
+    method public static operator void set(android.text.Spannable, int start, int end, Object span);
+    method public static operator void set(android.text.Spannable, kotlin.ranges.IntRange range, Object span);
+    method public static android.text.Spannable toSpannable(CharSequence);
+  }
+
+  public final class SpannedStringKt {
+    ctor public SpannedStringKt();
+    method public static android.text.Spanned toSpanned(CharSequence);
+  }
+
+  public final class StringKt {
+    ctor public StringKt();
+    method public static String htmlEncode(String);
+  }
+
+}
+
+package androidx.core.transition {
+
+  public final class TransitionKt {
+    ctor public TransitionKt();
+    method @RequiresApi(19) public static void addListener(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onEnd = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onStart = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onCancel = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onResume = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onPause = "null");
+    method @RequiresApi(19) public static void doOnCancel(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+    method @RequiresApi(19) public static void doOnEnd(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+    method @RequiresApi(19) public static void doOnPause(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+    method @RequiresApi(19) public static void doOnResume(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+    method @RequiresApi(19) public static void doOnStart(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+  }
+
+}
+
+package androidx.core.util {
+
+  public final class ArrayMapKt {
+    ctor public ArrayMapKt();
+    method @RequiresApi(19) public static <K, V> android.util.ArrayMap<K,V> arrayMapOf();
+    method @RequiresApi(19) public static <K, V> android.util.ArrayMap<K,V> arrayMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
+  }
+
+  public final class ArraySetKt {
+    ctor public ArraySetKt();
+    method @RequiresApi(23) public static <T> android.util.ArraySet<T> arraySetOf();
+    method @RequiresApi(23) public static <T> android.util.ArraySet<T> arraySetOf(T... values);
+  }
+
+  public final class AtomicFileKt {
+    ctor public AtomicFileKt();
+    method @RequiresApi(17) public static byte[] readBytes(android.util.AtomicFile);
+    method @RequiresApi(17) public static String readText(android.util.AtomicFile, java.nio.charset.Charset charset = "Charsets.UTF_8");
+    method @RequiresApi(17) public static void tryWrite(android.util.AtomicFile, kotlin.jvm.functions.Function1<? super java.io.FileOutputStream,kotlin.Unit> block);
+    method @RequiresApi(17) public static void writeBytes(android.util.AtomicFile, byte[] array);
+    method @RequiresApi(17) public static void writeText(android.util.AtomicFile, String text, java.nio.charset.Charset charset = "Charsets.UTF_8");
+  }
+
+  public final class HalfKt {
+    ctor public HalfKt();
+    method @RequiresApi(26) public static android.util.Half toHalf(short);
+    method @RequiresApi(26) public static android.util.Half toHalf(float);
+    method @RequiresApi(26) public static android.util.Half toHalf(double);
+    method @RequiresApi(26) public static android.util.Half toHalf(String);
+  }
+
+  public final class LocaleKt {
+    ctor public LocaleKt();
+    method @RequiresApi(17) public static int getLayoutDirection(java.util.Locale);
+  }
+
+  public final class LongSparseArrayKt {
+    ctor public LongSparseArrayKt();
+    method @RequiresApi(16) public static operator <T> boolean contains(android.util.LongSparseArray<T>, long key);
+    method @RequiresApi(16) public static <T> boolean containsKey(android.util.LongSparseArray<T>, long key);
+    method @RequiresApi(16) public static <T> boolean containsValue(android.util.LongSparseArray<T>, T! value);
+    method @RequiresApi(16) public static <T> void forEach(android.util.LongSparseArray<T>, kotlin.jvm.functions.Function2<? super java.lang.Long,? super T,kotlin.Unit> action);
+    method @RequiresApi(16) public static <T> T! getOrDefault(android.util.LongSparseArray<T>, long key, T! defaultValue);
+    method @RequiresApi(16) public static <T> T! getOrElse(android.util.LongSparseArray<T>, long key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
+    method @RequiresApi(16) public static <T> int getSize(android.util.LongSparseArray<T>);
+    method @RequiresApi(16) public static <T> boolean isEmpty(android.util.LongSparseArray<T>);
+    method @RequiresApi(16) public static <T> boolean isNotEmpty(android.util.LongSparseArray<T>);
+    method @RequiresApi(16) public static <T> kotlin.collections.LongIterator keyIterator(android.util.LongSparseArray<T>);
+    method @RequiresApi(16) public static operator <T> android.util.LongSparseArray<T> plus(android.util.LongSparseArray<T>, android.util.LongSparseArray<T> other);
+    method @RequiresApi(16) public static <T> void putAll(android.util.LongSparseArray<T>, android.util.LongSparseArray<T> other);
+    method @RequiresApi(16) public static <T> boolean remove(android.util.LongSparseArray<T>, long key, T! value);
+    method @RequiresApi(16) public static operator <T> void set(android.util.LongSparseArray<T>, long key, T! value);
+    method @RequiresApi(16) public static <T> java.util.Iterator<T> valueIterator(android.util.LongSparseArray<T>);
+  }
+
+  public final class LruCacheKt {
+    ctor public LruCacheKt();
+    method public static <K, V> android.util.LruCache<K,V> lruCache(int maxSize, kotlin.jvm.functions.Function2<? super K,? super V,java.lang.Integer> sizeOf = "{ _, _ -> 1 }", kotlin.jvm.functions.Function1<? super K,? extends V> create = "{ null as V? }", kotlin.jvm.functions.Function4<? super java.lang.Boolean,? super K,? super V,? super V,kotlin.Unit> onEntryRemoved = "{ _, _, _, _ -> }");
+  }
+
+  public final class PairKt {
+    ctor public PairKt();
+    method public static operator <F, S> F! component1(android.util.Pair<F,S>);
+    method public static operator <F, S> S! component2(android.util.Pair<F,S>);
+    method public static <F, S> android.util.Pair<F,S> toAndroidPair(kotlin.Pair<? extends F,? extends S>);
+    method public static <F, S> kotlin.Pair<F,S> toKotlinPair(android.util.Pair<F,S>);
+  }
+
+  public final class RangeKt {
+    ctor public RangeKt();
+    method @RequiresApi(21) public static infix <T extends java.lang.Comparable<? super T>> android.util.Range<T> and(android.util.Range<T>, android.util.Range<T> other);
+    method @RequiresApi(21) public static operator <T extends java.lang.Comparable<? super T>> android.util.Range<T> plus(android.util.Range<T>, T value);
+    method @RequiresApi(21) public static operator <T extends java.lang.Comparable<? super T>> android.util.Range<T> plus(android.util.Range<T>, android.util.Range<T> other);
+    method @RequiresApi(21) public static infix <T extends java.lang.Comparable<? super T>> android.util.Range<T> rangeTo(T, T that);
+    method @RequiresApi(21) public static <T extends java.lang.Comparable<? super T>> kotlin.ranges.ClosedRange<T> toClosedRange(android.util.Range<T>);
+    method @RequiresApi(21) public static <T extends java.lang.Comparable<? super T>> android.util.Range<T> toRange(kotlin.ranges.ClosedRange<T>);
+  }
+
+  public final class SizeKt {
+    ctor public SizeKt();
+    method @RequiresApi(21) public static operator int component1(android.util.Size);
+    method @RequiresApi(21) public static operator float component1(android.util.SizeF);
+    method @RequiresApi(21) public static operator int component2(android.util.Size);
+    method @RequiresApi(21) public static operator float component2(android.util.SizeF);
+  }
+
+  public final class SparseArrayKt {
+    ctor public SparseArrayKt();
+    method public static operator <T> boolean contains(android.util.SparseArray<T>, int key);
+    method public static <T> boolean containsKey(android.util.SparseArray<T>, int key);
+    method public static <T> boolean containsValue(android.util.SparseArray<T>, T! value);
+    method public static <T> void forEach(android.util.SparseArray<T>, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,kotlin.Unit> action);
+    method public static <T> T! getOrDefault(android.util.SparseArray<T>, int key, T! defaultValue);
+    method public static <T> T! getOrElse(android.util.SparseArray<T>, int key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
+    method public static <T> int getSize(android.util.SparseArray<T>);
+    method public static <T> boolean isEmpty(android.util.SparseArray<T>);
+    method public static <T> boolean isNotEmpty(android.util.SparseArray<T>);
+    method public static <T> kotlin.collections.IntIterator keyIterator(android.util.SparseArray<T>);
+    method public static operator <T> android.util.SparseArray<T> plus(android.util.SparseArray<T>, android.util.SparseArray<T> other);
+    method public static <T> void putAll(android.util.SparseArray<T>, android.util.SparseArray<T> other);
+    method public static <T> boolean remove(android.util.SparseArray<T>, int key, T! value);
+    method public static operator <T> void set(android.util.SparseArray<T>, int key, T! value);
+    method public static <T> java.util.Iterator<T> valueIterator(android.util.SparseArray<T>);
+  }
+
+  public final class SparseBooleanArrayKt {
+    ctor public SparseBooleanArrayKt();
+    method public static operator boolean contains(android.util.SparseBooleanArray, int key);
+    method public static boolean containsKey(android.util.SparseBooleanArray, int key);
+    method public static boolean containsValue(android.util.SparseBooleanArray, boolean value);
+    method public static void forEach(android.util.SparseBooleanArray, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Boolean,kotlin.Unit> action);
+    method public static boolean getOrDefault(android.util.SparseBooleanArray, int key, boolean defaultValue);
+    method public static error.NonExistentClass getOrElse(android.util.SparseBooleanArray, int key, kotlin.jvm.functions.Function0<java.lang.Boolean> defaultValue);
+    method public static int getSize(android.util.SparseBooleanArray);
+    method public static boolean isEmpty(android.util.SparseBooleanArray);
+    method public static boolean isNotEmpty(android.util.SparseBooleanArray);
+    method public static kotlin.collections.IntIterator keyIterator(android.util.SparseBooleanArray);
+    method public static operator android.util.SparseBooleanArray plus(android.util.SparseBooleanArray, android.util.SparseBooleanArray other);
+    method public static void putAll(android.util.SparseBooleanArray, android.util.SparseBooleanArray other);
+    method public static boolean remove(android.util.SparseBooleanArray, int key, boolean value);
+    method public static operator void set(android.util.SparseBooleanArray, int key, boolean value);
+    method public static kotlin.collections.BooleanIterator valueIterator(android.util.SparseBooleanArray);
+  }
+
+  public final class SparseIntArrayKt {
+    ctor public SparseIntArrayKt();
+    method public static operator boolean contains(android.util.SparseIntArray, int key);
+    method public static boolean containsKey(android.util.SparseIntArray, int key);
+    method public static boolean containsValue(android.util.SparseIntArray, int value);
+    method public static void forEach(android.util.SparseIntArray, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,kotlin.Unit> action);
+    method public static int getOrDefault(android.util.SparseIntArray, int key, int defaultValue);
+    method public static error.NonExistentClass getOrElse(android.util.SparseIntArray, int key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public static int getSize(android.util.SparseIntArray);
+    method public static boolean isEmpty(android.util.SparseIntArray);
+    method public static boolean isNotEmpty(android.util.SparseIntArray);
+    method public static kotlin.collections.IntIterator keyIterator(android.util.SparseIntArray);
+    method public static operator android.util.SparseIntArray plus(android.util.SparseIntArray, android.util.SparseIntArray other);
+    method public static void putAll(android.util.SparseIntArray, android.util.SparseIntArray other);
+    method public static boolean remove(android.util.SparseIntArray, int key, int value);
+    method public static operator void set(android.util.SparseIntArray, int key, int value);
+    method public static kotlin.collections.IntIterator valueIterator(android.util.SparseIntArray);
+  }
+
+  public final class SparseLongArrayKt {
+    ctor public SparseLongArrayKt();
+    method @RequiresApi(18) public static operator boolean contains(android.util.SparseLongArray, int key);
+    method @RequiresApi(18) public static boolean containsKey(android.util.SparseLongArray, int key);
+    method @RequiresApi(18) public static boolean containsValue(android.util.SparseLongArray, long value);
+    method @RequiresApi(18) public static void forEach(android.util.SparseLongArray, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,kotlin.Unit> action);
+    method @RequiresApi(18) public static long getOrDefault(android.util.SparseLongArray, int key, long defaultValue);
+    method @RequiresApi(18) public static error.NonExistentClass getOrElse(android.util.SparseLongArray, int key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method @RequiresApi(18) public static int getSize(android.util.SparseLongArray);
+    method @RequiresApi(18) public static boolean isEmpty(android.util.SparseLongArray);
+    method @RequiresApi(18) public static boolean isNotEmpty(android.util.SparseLongArray);
+    method @RequiresApi(18) public static kotlin.collections.IntIterator keyIterator(android.util.SparseLongArray);
+    method @RequiresApi(18) public static operator android.util.SparseLongArray plus(android.util.SparseLongArray, android.util.SparseLongArray other);
+    method @RequiresApi(18) public static void putAll(android.util.SparseLongArray, android.util.SparseLongArray other);
+    method @RequiresApi(18) public static boolean remove(android.util.SparseLongArray, int key, long value);
+    method @RequiresApi(18) public static operator void set(android.util.SparseLongArray, int key, long value);
+    method @RequiresApi(18) public static kotlin.collections.LongIterator valueIterator(android.util.SparseLongArray);
+  }
+
+}
+
+package androidx.core.view {
+
+  public final class MenuKt {
+    ctor public MenuKt();
+    method public static operator boolean contains(android.view.Menu, android.view.MenuItem item);
+    method public static void forEach(android.view.Menu, kotlin.jvm.functions.Function1<? super android.view.MenuItem,kotlin.Unit> action);
+    method public static void forEachIndexed(android.view.Menu, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super android.view.MenuItem,kotlin.Unit> action);
+    method public static operator android.view.MenuItem get(android.view.Menu, int index);
+    method public static kotlin.sequences.Sequence<android.view.MenuItem> getChildren(android.view.Menu);
+    method public static int getSize(android.view.Menu);
+    method public static boolean isEmpty(android.view.Menu);
+    method public static boolean isNotEmpty(android.view.Menu);
+    method public static operator java.util.Iterator<android.view.MenuItem> iterator(android.view.Menu);
+    method public static operator void minusAssign(android.view.Menu, android.view.MenuItem item);
+  }
+
+  public final class ViewGroupKt {
+    ctor public ViewGroupKt();
+    method public static operator boolean contains(android.view.ViewGroup, android.view.View view);
+    method public static void forEach(android.view.ViewGroup, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+    method public static void forEachIndexed(android.view.ViewGroup, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super android.view.View,kotlin.Unit> action);
+    method public static operator android.view.View get(android.view.ViewGroup, int index);
+    method public static kotlin.sequences.Sequence<android.view.View> getChildren(android.view.ViewGroup);
+    method public static int getSize(android.view.ViewGroup);
+    method public static boolean isEmpty(android.view.ViewGroup);
+    method public static boolean isNotEmpty(android.view.ViewGroup);
+    method public static operator java.util.Iterator<android.view.View> iterator(android.view.ViewGroup);
+    method public static operator void minusAssign(android.view.ViewGroup, android.view.View view);
+    method public static operator void plusAssign(android.view.ViewGroup, android.view.View view);
+    method public static void setMargins(android.view.ViewGroup.MarginLayoutParams, @Px int size);
+    method public static void updateMargins(android.view.ViewGroup.MarginLayoutParams, @Px int left = "leftMargin", @Px int top = "topMargin", @Px int right = "rightMargin", @Px int bottom = "bottomMargin");
+    method @RequiresApi(17) public static void updateMarginsRelative(android.view.ViewGroup.MarginLayoutParams, @Px int start = "marginStart", @Px int top = "topMargin", @Px int end = "marginEnd", @Px int bottom = "bottomMargin");
+  }
+
+  public final class ViewKt {
+    ctor public ViewKt();
+    method @RequiresApi(16) public static void announceForAccessibility(android.view.View, @StringRes int resource);
+    method public static void doOnLayout(android.view.View, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+    method public static void doOnNextLayout(android.view.View, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+    method public static void doOnPreDraw(android.view.View, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+    method public static boolean isGone(android.view.View);
+    method public static boolean isInvisible(android.view.View);
+    method public static boolean isVisible(android.view.View);
+    method public static Runnable postDelayed(android.view.View, long delayInMillis, kotlin.jvm.functions.Function0<kotlin.Unit> action);
+    method @RequiresApi(16) public static Runnable postOnAnimationDelayed(android.view.View, long delayInMillis, kotlin.jvm.functions.Function0<kotlin.Unit> action);
+    method public static void setGone(android.view.View, boolean value);
+    method public static void setInvisible(android.view.View, boolean value);
+    method public static void setPadding(android.view.View, @Px int size);
+    method public static void setVisible(android.view.View, boolean value);
+    method public static android.graphics.Bitmap toBitmap(android.view.View, android.graphics.Bitmap.Config config = "Bitmap.Config.ARGB_8888");
+    method public static void updateLayoutParams(android.view.View, kotlin.jvm.functions.Function1<? super android.view.ViewGroup.LayoutParams,kotlin.Unit> block);
+    method public static void updatePadding(android.view.View, @Px int left = "paddingLeft", @Px int top = "paddingTop", @Px int right = "paddingRight", @Px int bottom = "paddingBottom");
+    method @RequiresApi(17) public static void updatePaddingRelative(android.view.View, @Px int start = "paddingStart", @Px int top = "paddingTop", @Px int end = "paddingEnd", @Px int bottom = "paddingBottom");
+  }
+
+}
+
+package androidx.core.widget {
+
+  public final class ToastKt {
+    ctor public ToastKt();
+    method public static android.widget.Toast toast(android.content.Context, CharSequence text, int duration = "Toast.LENGTH_SHORT");
+    method public static android.widget.Toast toast(android.content.Context, @StringRes int resId, int duration = "Toast.LENGTH_SHORT");
+  }
+
+}
+
diff --git a/core/ktx/build.gradle b/core/ktx/build.gradle
new file mode 100644
index 0000000..bce0757
--- /dev/null
+++ b/core/ktx/build.gradle
@@ -0,0 +1,37 @@
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+    id("org.jetbrains.kotlin.android")
+}
+
+android {
+    buildTypes {
+        debug {
+            testCoverageEnabled = false // Breaks Kotlin compiler.
+        }
+    }
+}
+
+dependencies {
+    api(KOTLIN_STDLIB)
+    api(project(":annotation"))
+    api(project(":core"))
+
+    androidTestImplementation(JUNIT)
+    androidTestImplementation(TEST_RUNNER)
+    androidTestImplementation(TEST_RULES)
+    androidTestImplementation(TRUTH)
+    androidTestImplementation(project(":internal-testutils-ktx"))
+}
+
+supportLibrary {
+    name = "Core Kotlin Extensions"
+    publish = true
+    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenGroup = LibraryGroups.CORE
+    inceptionYear = "2018"
+    description = "Kotlin extensions for 'core' artifact"
+}
diff --git a/core/ktx/src/androidTest/AndroidManifest.xml b/core/ktx/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..259b616
--- /dev/null
+++ b/core/ktx/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="androidx.core.ktx.test">
+    <application>
+        <activity android:name="androidx.core.TestActivity"/>
+        <activity android:name="androidx.core.TestPreferenceActivity"/>
+    </application>
+</manifest>
diff --git a/core/ktx/src/androidTest/assets/red.png b/core/ktx/src/androidTest/assets/red.png
new file mode 100644
index 0000000..6292f4b
--- /dev/null
+++ b/core/ktx/src/androidTest/assets/red.png
Binary files differ
diff --git a/core/ktx/src/androidTest/font_licenses.txt b/core/ktx/src/androidTest/font_licenses.txt
new file mode 100644
index 0000000..0b83a9a
--- /dev/null
+++ b/core/ktx/src/androidTest/font_licenses.txt
@@ -0,0 +1,93 @@
+Copyright 2006 The Inconsolata Project Authors

+

+This Font Software is licensed under the SIL Open Font License, Version 1.1.

+This license is copied below, and is also available with a FAQ at:

+http://scripts.sil.org/OFL

+

+

+-----------------------------------------------------------

+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007

+-----------------------------------------------------------

+

+PREAMBLE

+The goals of the Open Font License (OFL) are to stimulate worldwide

+development of collaborative font projects, to support the font creation

+efforts of academic and linguistic communities, and to provide a free and

+open framework in which fonts may be shared and improved in partnership

+with others.

+

+The OFL allows the licensed fonts to be used, studied, modified and

+redistributed freely as long as they are not sold by themselves. The

+fonts, including any derivative works, can be bundled, embedded, 

+redistributed and/or sold with any software provided that any reserved

+names are not used by derivative works. The fonts and derivatives,

+however, cannot be released under any other type of license. The

+requirement for fonts to remain under this license does not apply

+to any document created using the fonts or their derivatives.

+

+DEFINITIONS

+"Font Software" refers to the set of files released by the Copyright

+Holder(s) under this license and clearly marked as such. This may

+include source files, build scripts and documentation.

+

+"Reserved Font Name" refers to any names specified as such after the

+copyright statement(s).

+

+"Original Version" refers to the collection of Font Software components as

+distributed by the Copyright Holder(s).

+

+"Modified Version" refers to any derivative made by adding to, deleting,

+or substituting -- in part or in whole -- any of the components of the

+Original Version, by changing formats or by porting the Font Software to a

+new environment.

+

+"Author" refers to any designer, engineer, programmer, technical

+writer or other person who contributed to the Font Software.

+

+PERMISSION & CONDITIONS

+Permission is hereby granted, free of charge, to any person obtaining

+a copy of the Font Software, to use, study, copy, merge, embed, modify,

+redistribute, and sell modified and unmodified copies of the Font

+Software, subject to the following conditions:

+

+1) Neither the Font Software nor any of its individual components,

+in Original or Modified Versions, may be sold by itself.

+

+2) Original or Modified Versions of the Font Software may be bundled,

+redistributed and/or sold with any software, provided that each copy

+contains the above copyright notice and this license. These can be

+included either as stand-alone text files, human-readable headers or

+in the appropriate machine-readable metadata fields within text or

+binary files as long as those fields can be easily viewed by the user.

+

+3) No Modified Version of the Font Software may use the Reserved Font

+Name(s) unless explicit written permission is granted by the corresponding

+Copyright Holder. This restriction only applies to the primary font name as

+presented to the users.

+

+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font

+Software shall not be used to promote, endorse or advertise any

+Modified Version, except to acknowledge the contribution(s) of the

+Copyright Holder(s) and the Author(s) or with their explicit written

+permission.

+

+5) The Font Software, modified or unmodified, in part or in whole,

+must be distributed entirely under this license, and must not be

+distributed under any other license. The requirement for fonts to

+remain under this license does not apply to any document created

+using the Font Software.

+

+TERMINATION

+This license becomes null and void if any of the above conditions are

+not met.

+

+DISCLAIMER

+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,

+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF

+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT

+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE

+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,

+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL

+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING

+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM

+OTHER DEALINGS IN THE FONT SOFTWARE.

diff --git a/core/ktx/src/androidTest/java/androidx/core/TestActivity.kt b/core/ktx/src/androidTest/java/androidx/core/TestActivity.kt
new file mode 100644
index 0000000..b0132ab
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/TestActivity.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core
+
+import android.app.Activity
+import android.os.Bundle
+import androidx.core.ktx.test.R
+
+class TestActivity : Activity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.test_activity)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/TestPreferenceActivity.kt b/core/ktx/src/androidTest/java/androidx/core/TestPreferenceActivity.kt
new file mode 100644
index 0000000..43e3dee
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/TestPreferenceActivity.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+package androidx.core
+
+import android.app.Activity
+import android.os.Bundle
+import android.preference.PreferenceFragment
+import androidx.core.ktx.test.R
+
+class TestPreferenceActivity : Activity() {
+
+    companion object {
+        const val TAG = "TestPreferenceActivity"
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        fragmentManager.beginTransaction()
+            .add(android.R.id.content, TestPreferenceFragment(), TAG)
+            .commitNow()
+    }
+
+    class TestPreferenceFragment : PreferenceFragment() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            super.onCreate(savedInstanceState)
+            addPreferencesFromResource(R.xml.preferences)
+        }
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/animation/AnimatorTest.kt b/core/ktx/src/androidTest/java/androidx/core/animation/AnimatorTest.kt
new file mode 100644
index 0000000..de298a5
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/animation/AnimatorTest.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.animation
+
+import android.animation.Animator
+import android.animation.ObjectAnimator
+import android.support.test.InstrumentationRegistry
+import android.support.test.annotation.UiThreadTest
+import android.support.test.filters.SdkSuppress
+import android.support.test.runner.AndroidJUnit4
+import android.view.View
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AnimatorTest {
+    private val context = InstrumentationRegistry.getContext()
+    private val view = View(context)
+
+    private lateinit var animator: Animator
+
+    @Before fun before() {
+        animator = ObjectAnimator.ofFloat(view, View.ALPHA, 0f, 1f)
+    }
+
+    @Test fun testDoOnStart() {
+        var called = false
+        animator.doOnStart {
+            called = true
+        }
+
+        animator.listeners.forEach { it.onAnimationStart(animator) }
+        assertTrue(called)
+    }
+
+    @Test fun testDoOnEnd() {
+        var called = false
+        animator.doOnEnd {
+            called = true
+        }
+
+        animator.listeners.forEach { it.onAnimationEnd(animator) }
+        assertTrue(called)
+    }
+
+    @Test fun testDoOnCancel() {
+        var cancelCalled = false
+        animator.doOnCancel {
+            cancelCalled = true
+        }
+
+        animator.listeners.forEach { it.onAnimationCancel(animator) }
+        assertTrue(cancelCalled)
+    }
+
+    @Test fun testDoOnRepeat() {
+        var called = false
+        animator.doOnRepeat {
+            called = true
+        }
+
+        animator.listeners.forEach { it.onAnimationRepeat(animator) }
+        assertTrue(called)
+    }
+
+    @UiThreadTest
+    @SdkSuppress(minSdkVersion = 19)
+    @Test fun testDoOnPause() {
+        var called = false
+        animator.doOnPause {
+            called = true
+        }
+
+        // Start and pause and assert doOnPause was called
+        animator.start()
+        animator.pause()
+        assertTrue(called)
+
+        animator.cancel()
+    }
+
+    @UiThreadTest
+    @SdkSuppress(minSdkVersion = 19)
+    @Test fun testDoOnResume() {
+        var called = false
+        animator.doOnResume {
+            called = true
+        }
+
+        animator.start()
+        animator.pause()
+
+        // Now resume and assert doOnResume was called
+        animator.resume()
+        assertTrue(called)
+
+        animator.cancel()
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/content/ContentValuesTest.kt b/core/ktx/src/androidTest/java/androidx/core/content/ContentValuesTest.kt
new file mode 100644
index 0000000..746a634
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/content/ContentValuesTest.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+package androidx.core.content
+
+import androidx.testutils.assertThrows
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import java.util.concurrent.atomic.AtomicInteger
+
+class ContentValuesTest {
+    @Test fun valuesOfValid() {
+        val values = contentValuesOf(
+            "null" to null,
+            "string" to "string",
+            "byte" to 1.toByte(),
+            "short" to 1.toShort(),
+            "int" to 1,
+            "long" to 1L,
+            "float" to 1f,
+            "double" to 1.0,
+            "boolean" to true,
+            "byteArray" to byteArrayOf()
+        )
+        assertEquals(10, values.size())
+        assertNull(values.get("null"))
+        assertEquals("string", values.get("string"))
+        assertEquals(1.toByte(), values.get("byte"))
+        assertEquals(1.toShort(), values.get("short"))
+        assertEquals(1, values.get("int"))
+        assertEquals(1L, values.get("long"))
+        assertEquals(1f, values.get("float"))
+        assertEquals(1.0, values.get("double"))
+        assertEquals(true, values.get("boolean"))
+        assertArrayEquals(byteArrayOf(), values.get("byteArray") as ByteArray)
+    }
+
+    @Test fun valuesOfInvalid() {
+        assertThrows<IllegalArgumentException> {
+            contentValuesOf("nope" to AtomicInteger(1))
+        }.hasMessageThat().isEqualTo(
+            "Illegal value type java.util.concurrent.atomic.AtomicInteger for key \"nope\"")
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/content/ContextTest.kt b/core/ktx/src/androidTest/java/androidx/core/content/ContextTest.kt
new file mode 100644
index 0000000..09d2355
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/content/ContextTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.content
+
+import android.content.ContextWrapper
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SdkSuppress
+import androidx.core.ktx.test.R
+import androidx.core.getAttributeSet
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ContextTest {
+    private val context = InstrumentationRegistry.getContext()
+
+    @SdkSuppress(minSdkVersion = 23)
+    @Test fun systemService() {
+        var lookup: Class<*>? = null
+        val context = object : ContextWrapper(context) {
+            override fun getSystemServiceName(serviceClass: Class<*>): String? {
+                lookup = serviceClass
+                return if (serviceClass == Unit::class.java) "unit" else null
+            }
+
+            override fun getSystemService(name: String): Any? {
+                return if (name == "unit") Unit else null
+            }
+        }
+        val actual = context.systemService<Unit>()
+        assertEquals(Unit::class.java, lookup)
+        assertSame(Unit, actual)
+    }
+
+    @Test fun withStyledAttributes() {
+        context.withStyledAttributes(attrs = intArrayOf(android.R.attr.textColorPrimary)) {
+            val resourceId = getResourceId(0, -1)
+            assertTrue(resourceId != 1)
+        }
+
+        context.withStyledAttributes(
+            android.R.style.Theme_Light,
+            intArrayOf(android.R.attr.textColorPrimary)
+        ) {
+            val resourceId = getResourceId(0, -1)
+            assertTrue(resourceId != 1)
+        }
+
+        val attrs = context.getAttributeSet(R.layout.test_attrs)
+        context.withStyledAttributes(attrs, R.styleable.SampleAttrs) {
+            assertTrue(getInt(R.styleable.SampleAttrs_sample, -1) != -1)
+        }
+
+        context.withStyledAttributes(attrs, R.styleable.SampleAttrs, 0, 0) {
+            assertTrue(getInt(R.styleable.SampleAttrs_sample, -1) != -1)
+        }
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/content/SharedPreferencesTest.kt b/core/ktx/src/androidTest/java/androidx/core/content/SharedPreferencesTest.kt
new file mode 100644
index 0000000..bf1e67d
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/content/SharedPreferencesTest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.content
+
+import android.support.test.InstrumentationRegistry
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class SharedPreferencesTest {
+    private val context = InstrumentationRegistry.getContext()
+
+    @Test fun editApply() {
+        val preferences = context.getSharedPreferences("prefs", 0)
+
+        preferences.edit {
+            putString("test_key1", "test_value")
+            putInt("test_key2", 100)
+        }
+
+        assertEquals("test_value", preferences.getString("test_key1", null))
+        assertEquals(100, preferences.getInt("test_key2", 0))
+    }
+
+    @Test fun editCommit() {
+        val preferences = context.getSharedPreferences("prefs", 0)
+        preferences.edit(commit = true) {
+            putString("test_key1", "test_value")
+            putInt("test_key2", 100)
+        }
+
+        assertEquals("test_value", preferences.getString("test_key1", null))
+        assertEquals(100, preferences.getInt("test_key2", 0))
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/content/res/TypedArrayTest.kt b/core/ktx/src/androidTest/java/androidx/core/content/res/TypedArrayTest.kt
new file mode 100644
index 0000000..fe576df
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/content/res/TypedArrayTest.kt
@@ -0,0 +1,221 @@
+/*
+ * 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.
+ */
+
+package androidx.core.content.res
+
+import android.graphics.Color
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SdkSuppress
+import androidx.core.ktx.test.R
+import androidx.testutils.assertThrows
+import androidx.core.getAttributeSet
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class TypedArrayTest {
+    private val context = InstrumentationRegistry.getContext()
+
+    @Test fun boolean() {
+        val attrs = context.getAttributeSet(R.layout.typed_array)
+        val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+        assertTrue(array.getBooleanOrThrow(R.styleable.TypedArrayTypes_boolean_present))
+
+        assertThrows<IllegalArgumentException> {
+            array.getBooleanOrThrow(R.styleable.TypedArrayTypes_boolean_absent)
+        }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+    }
+
+    @Test fun color() {
+        val attrs = context.getAttributeSet(R.layout.typed_array)
+        val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+        assertEquals(Color.WHITE, array.getColorOrThrow(R.styleable.TypedArrayTypes_color_present))
+
+        assertThrows<IllegalArgumentException> {
+            array.getColorOrThrow(R.styleable.TypedArrayTypes_color_absent)
+        }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+    }
+
+    @Test fun colorStateList() {
+        val attrs = context.getAttributeSet(R.layout.typed_array)
+        val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+        val stateList = array.getColorStateListOrThrow(R.styleable.TypedArrayTypes_color_present)
+
+        assertEquals(Color.WHITE, stateList.defaultColor)
+
+        assertThrows<IllegalArgumentException> {
+            array.getColorStateListOrThrow(R.styleable.TypedArrayTypes_color_absent)
+        }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+    }
+
+    @Test fun dimension() {
+        val attrs = context.getAttributeSet(R.layout.typed_array)
+        val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+        assertEquals(1f, array.getDimensionOrThrow(R.styleable.TypedArrayTypes_dimension_present))
+
+        assertThrows<IllegalArgumentException> {
+            array.getDimensionOrThrow(R.styleable.TypedArrayTypes_dimension_absent)
+        }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+    }
+
+    @Test fun dimensionPixelSize() {
+        val attrs = context.getAttributeSet(R.layout.typed_array)
+        val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+        assertEquals(1,
+            array.getDimensionPixelSizeOrThrow(R.styleable.TypedArrayTypes_dimension_present))
+
+        assertThrows<IllegalArgumentException> {
+            array.getDimensionPixelSizeOrThrow(R.styleable.TypedArrayTypes_dimension_absent)
+        }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+    }
+
+    @Test fun dimensionPixelOffset() {
+        val attrs = context.getAttributeSet(R.layout.typed_array)
+        val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+        assertEquals(1,
+            array.getDimensionPixelOffsetOrThrow(R.styleable.TypedArrayTypes_dimension_present))
+
+        assertThrows<IllegalArgumentException> {
+            array.getDimensionPixelOffsetOrThrow(R.styleable.TypedArrayTypes_dimension_absent)
+        }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+    }
+
+    @Test fun drawable() {
+        val attrs = context.getAttributeSet(R.layout.typed_array)
+        val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+        assertNotNull(array.getDrawableOrThrow(R.styleable.TypedArrayTypes_drawable_present))
+
+        assertThrows<IllegalArgumentException> {
+            array.getDrawableOrThrow(R.styleable.TypedArrayTypes_drawable_absent)
+        }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+    }
+
+    @Test fun float() {
+        val attrs = context.getAttributeSet(R.layout.typed_array)
+        val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+        assertEquals(0.1f, array.getFloatOrThrow(R.styleable.TypedArrayTypes_float_present))
+
+        assertThrows<IllegalArgumentException> {
+            array.getFloatOrThrow(R.styleable.TypedArrayTypes_float_absent)
+        }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun font() {
+        val attrs = context.getAttributeSet(R.layout.typed_array)
+        val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+        assertNotNull(array.getFontOrThrow(R.styleable.TypedArrayTypes_font_present))
+
+        assertThrows<IllegalArgumentException> {
+            array.getFontOrThrow(R.styleable.TypedArrayTypes_font_absent)
+        }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+    }
+
+    @Test fun int() {
+        val attrs = context.getAttributeSet(R.layout.typed_array)
+        val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+        assertEquals(1, array.getIntOrThrow(R.styleable.TypedArrayTypes_integer_present))
+
+        assertThrows<IllegalArgumentException> {
+            array.getIntOrThrow(R.styleable.TypedArrayTypes_integer_absent)
+        }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+    }
+
+    @Test fun integer() {
+        val attrs = context.getAttributeSet(R.layout.typed_array)
+        val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+        assertEquals(1, array.getIntegerOrThrow(R.styleable.TypedArrayTypes_integer_present))
+
+        assertThrows<IllegalArgumentException> {
+            array.getIntegerOrThrow(R.styleable.TypedArrayTypes_integer_absent)
+        }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+    }
+
+    @Test
+    fun resourceId() {
+        val attrs = context.getAttributeSet(R.layout.typed_array)
+        val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+        assertEquals(
+            R.font.inconsolata_regular,
+            array.getResourceIdOrThrow(R.styleable.TypedArrayTypes_resource_present)
+        )
+
+        assertThrows<IllegalArgumentException> {
+            array.getResourceIdOrThrow(R.styleable.TypedArrayTypes_resource_absent)
+        }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+    }
+
+    @Test fun string() {
+        val attrs = context.getAttributeSet(R.layout.typed_array)
+        val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+        assertEquals("Hello", array.getStringOrThrow(R.styleable.TypedArrayTypes_string_present))
+
+        assertThrows<IllegalArgumentException> {
+            array.getStringOrThrow(R.styleable.TypedArrayTypes_string_absent)
+        }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+    }
+
+    @Test fun text() {
+        val attrs = context.getAttributeSet(R.layout.typed_array)
+        val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+        assertEquals("Hello", array.getTextOrThrow(R.styleable.TypedArrayTypes_string_present))
+
+        assertThrows<IllegalArgumentException> {
+            array.getTextOrThrow(R.styleable.TypedArrayTypes_string_absent)
+        }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+    }
+
+    @Test fun textArray() {
+        val attrs = context.getAttributeSet(R.layout.typed_array)
+        val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+        val text = array.getTextArrayOrThrow(R.styleable.TypedArrayTypes_text_array_present)
+        assertEquals("Hello", text[0].toString())
+        assertEquals("World", text[1].toString())
+
+        assertThrows<IllegalArgumentException> {
+            array.getTextOrThrow(R.styleable.TypedArrayTypes_text_array_absent)
+        }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+    }
+
+    @Test fun useRecyclesArray() {
+        val attrs = context.getAttributeSet(R.layout.typed_array)
+        val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+        val result = array.use {
+            it.getBoolean(R.styleable.TypedArrayTypes_boolean_present, false)
+        }
+        assertTrue(result)
+
+        assertThrows<RuntimeException> {
+            array.recycle()
+        }
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/database/CursorTest.kt b/core/ktx/src/androidTest/java/androidx/core/database/CursorTest.kt
new file mode 100644
index 0000000..86d0731
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/database/CursorTest.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.
+ */
+
+package androidx.core.database
+
+import android.database.Cursor
+import android.database.MatrixCursor
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class CursorTest {
+    @Test fun blobByName() {
+        val cursor = scalarCursor(byteArrayOf(0x01))
+        val blob = cursor.getBlob("data")
+        assertArrayEquals(byteArrayOf(0x01), blob)
+    }
+
+    @Test fun doubleByName() {
+        val cursor = scalarCursor(1.5)
+        val double = cursor.getDouble("data")
+        assertEquals(1.5, double, 0.0)
+    }
+
+    @Test fun floatByName() {
+        val cursor = scalarCursor(1.5f)
+        val float = cursor.getFloat("data")
+        assertEquals(1.5f, float, 0f)
+    }
+
+    @Test fun intByName() {
+        val cursor = scalarCursor(1)
+        val int = cursor.getInt("data")
+        assertEquals(1, int)
+    }
+
+    @Test fun longByName() {
+        val cursor = scalarCursor(1L)
+        val long = cursor.getLong("data")
+        assertEquals(1L, long)
+    }
+
+    @Test fun shortByName() {
+        val cursor = scalarCursor(1.toShort())
+        val short = cursor.getShort("data")
+        assertEquals(1.toShort(), short)
+    }
+
+    @Test fun stringByName() {
+        val cursor = scalarCursor("hey")
+        val string = cursor.getString("data")
+        assertEquals("hey", string)
+    }
+
+    @Test fun blobOrNullByIndex() {
+        val cursor = scalarCursor(null)
+        val blob = cursor.getBlobOrNull(0)
+        assertNull(blob)
+    }
+
+    @Test fun doubleOrNullByIndex() {
+        val cursor = scalarCursor(null)
+        val double = cursor.getDoubleOrNull(0)
+        assertNull(double)
+    }
+
+    @Test fun floatOrNullByIndex() {
+        val cursor = scalarCursor(null)
+        val float = cursor.getFloatOrNull(0)
+        assertNull(float)
+    }
+
+    @Test fun intOrNullByIndex() {
+        val cursor = scalarCursor(null)
+        val int = cursor.getIntOrNull(0)
+        assertNull(int)
+    }
+
+    @Test fun longOrNullByIndex() {
+        val cursor = scalarCursor(null)
+        val long = cursor.getLongOrNull(0)
+        assertNull(long)
+    }
+
+    @Test fun shortOrNullByIndex() {
+        val cursor = scalarCursor(null)
+        val short = cursor.getShortOrNull(0)
+        assertNull(short)
+    }
+
+    @Test fun stringOrNullByIndex() {
+        val cursor = scalarCursor(null)
+        val string = cursor.getStringOrNull(0)
+        assertNull(string)
+    }
+
+    @Test fun blobOrNullByName() {
+        val cursor = scalarCursor(null)
+        val blob = cursor.getBlobOrNull("data")
+        assertNull(blob)
+    }
+
+    @Test fun doubleOrNullByName() {
+        val cursor = scalarCursor(null)
+        val double = cursor.getDoubleOrNull("data")
+        assertNull(double)
+    }
+
+    @Test fun floatOrNullByName() {
+        val cursor = scalarCursor(null)
+        val float = cursor.getFloatOrNull("data")
+        assertNull(float)
+    }
+
+    @Test fun intOrNullByName() {
+        val cursor = scalarCursor(null)
+        val int = cursor.getIntOrNull("data")
+        assertNull(int)
+    }
+
+    @Test fun longOrNullByName() {
+        val cursor = scalarCursor(null)
+        val long = cursor.getLongOrNull("data")
+        assertNull(long)
+    }
+
+    @Test fun shortOrNullByName() {
+        val cursor = scalarCursor(null)
+        val short = cursor.getShortOrNull("data")
+        assertNull(short)
+    }
+
+    @Test fun stringOrNullByName() {
+        val cursor = scalarCursor(null)
+        val string = cursor.getStringOrNull("data")
+        assertNull(string)
+    }
+
+    private fun scalarCursor(item: Any?): Cursor = MatrixCursor(arrayOf("data")).apply {
+        addRow(arrayOf(item))
+        moveToFirst() // Prepare for consumers to read.
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/database/sqlite/SQLiteDatabaseTest.kt b/core/ktx/src/androidTest/java/androidx/core/database/sqlite/SQLiteDatabaseTest.kt
new file mode 100644
index 0000000..c8ed67d
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/database/sqlite/SQLiteDatabaseTest.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+package androidx.core.database.sqlite
+
+import android.content.ContentValues
+import android.database.sqlite.SQLiteDatabase
+import android.database.sqlite.SQLiteOpenHelper
+import android.support.test.InstrumentationRegistry
+import androidx.testutils.assertThrows
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class SQLiteDatabaseTest {
+    private val context = InstrumentationRegistry.getContext()
+    private val openHelper = object : SQLiteOpenHelper(context, null, null, 1) {
+        override fun onCreate(db: SQLiteDatabase) {
+            db.execSQL("CREATE TABLE test(name TEXT)")
+        }
+
+        override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
+        }
+    }
+    private val db = openHelper.writableDatabase
+
+    @Test fun throwingBodyNotSuccessful() {
+        val exception = RuntimeException()
+        assertThrows<RuntimeException> {
+            db.transaction {
+                insert("test", null, ContentValues().apply { put("name", "Alice") })
+                throw exception
+            }
+        }.isSameAs(exception)
+
+        val query = db.rawQuery("SELECT COUNT(*) FROM test", emptyArray())
+        query.moveToFirst()
+        assertEquals(0L, query.getLong(0))
+        query.close()
+    }
+
+    @Test fun bodyReturnValue() {
+        val result = db.transaction {
+            "Hey"
+        }
+        assertEquals("Hey", result)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/extensions.kt b/core/ktx/src/androidTest/java/androidx/core/extensions.kt
new file mode 100644
index 0000000..dc07142
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/extensions.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.util.Xml
+import androidx.annotation.LayoutRes
+import org.xmlpull.v1.XmlPullParser
+
+@SuppressLint("ResourceType")
+fun Context.getAttributeSet(@LayoutRes layoutId: Int): AttributeSet {
+    val parser = resources.getXml(layoutId)
+    var type = parser.next()
+    while (type != XmlPullParser.START_TAG) {
+        type = parser.next()
+    }
+    return Xml.asAttributeSet(parser)
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/BitmapTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/BitmapTest.kt
new file mode 100644
index 0000000..25b318b
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/BitmapTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Bitmap
+import android.graphics.ColorSpace
+import android.support.test.filters.SdkSuppress
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class BitmapTest {
+    @Test fun create() {
+        val bitmap = createBitmap(7, 9)
+        assertEquals(7, bitmap.width)
+        assertEquals(9, bitmap.height)
+        assertEquals(Bitmap.Config.ARGB_8888, bitmap.config)
+    }
+
+    @Test fun createWithConfig() {
+        val bitmap = createBitmap(7, 9, config = Bitmap.Config.RGB_565)
+        assertEquals(Bitmap.Config.RGB_565, bitmap.config)
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun createWithColorSpace() {
+        val colorSpace = ColorSpace.get(ColorSpace.Named.ADOBE_RGB)
+        val bitmap = createBitmap(7, 9, colorSpace = colorSpace)
+        assertEquals(colorSpace, bitmap.colorSpace)
+    }
+
+    @Test fun scale() {
+        val b = createBitmap(7, 9).scale(3, 5)
+        assertEquals(3, b.width)
+        assertEquals(5, b.height)
+    }
+
+    @Test fun applyCanvas() {
+        val p = createBitmap(2, 2).applyCanvas {
+            drawColor(0x40302010)
+        }.getPixel(1, 1)
+
+        assertEquals(0x40302010, p)
+    }
+
+    @Test fun getPixel() {
+        val b = createBitmap(2, 2).applyCanvas {
+            drawColor(0x40302010)
+        }
+        assertEquals(0x40302010, b[1, 1])
+    }
+
+    @Test fun setPixel() {
+        val b = createBitmap(2, 2)
+        b[1, 1] = 0x40302010
+        assertEquals(0x40302010, b[1, 1])
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/CanvasTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/CanvasTest.kt
new file mode 100644
index 0000000..e940aa9
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/CanvasTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Canvas
+import android.graphics.Matrix
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class CanvasTest {
+    private val values = FloatArray(9)
+    private val canvas = Canvas(createBitmap(1, 1))
+
+    @Suppress("DEPRECATION")
+    @Test fun withSave() {
+        val beforeCount = canvas.saveCount
+
+        canvas.matrix.getValues(values)
+        val x = values[Matrix.MTRANS_X]
+        val y = values[Matrix.MTRANS_Y]
+
+        canvas.withSave {
+            assertThat(beforeCount).isLessThan(saveCount)
+            translate(10.0f, 10.0f)
+        }
+
+        canvas.matrix.getValues(values)
+        assertEquals(x, values[Matrix.MTRANS_X])
+        assertEquals(y, values[Matrix.MTRANS_Y])
+
+        assertEquals(beforeCount, canvas.saveCount)
+    }
+
+    @Test fun withTranslation() {
+        val beforeCount = canvas.saveCount
+        canvas.withTranslation(x = 16.0f, y = 32.0f) {
+            assertThat(beforeCount).isLessThan(saveCount)
+
+            @Suppress("DEPRECATION")
+            matrix.getValues(values) // will work for a software canvas
+
+            assertEquals(16.0f, values[Matrix.MTRANS_X])
+            assertEquals(32.0f, values[Matrix.MTRANS_Y])
+        }
+        assertEquals(beforeCount, canvas.saveCount)
+    }
+
+    @Test fun withRotation() {
+        val beforeCount = canvas.saveCount
+        canvas.withRotation(degrees = 90.0f, pivotX = 16.0f, pivotY = 32.0f) {
+            assertThat(beforeCount).isLessThan(saveCount)
+
+            @Suppress("DEPRECATION")
+            matrix.getValues(values) // will work for a software canvas
+
+            assertEquals(48.0f, values[Matrix.MTRANS_X])
+            assertEquals(16.0f, values[Matrix.MTRANS_Y])
+            assertEquals(-1.0f, values[Matrix.MSKEW_X])
+            assertEquals(1.0f, values[Matrix.MSKEW_Y])
+        }
+        assertEquals(beforeCount, canvas.saveCount)
+    }
+
+    @Test fun withScale() {
+        val beforeCount = canvas.saveCount
+        canvas.withScale(x = 2.0f, y = 4.0f, pivotX = 16.0f, pivotY = 32.0f) {
+            assertThat(beforeCount).isLessThan(saveCount)
+
+            @Suppress("DEPRECATION")
+            matrix.getValues(values) // will work for a software canvas
+
+            assertEquals(-16.0f, values[Matrix.MTRANS_X])
+            assertEquals(-96.0f, values[Matrix.MTRANS_Y])
+            assertEquals(2.0f, values[Matrix.MSCALE_X])
+            assertEquals(4.0f, values[Matrix.MSCALE_Y])
+        }
+        assertEquals(beforeCount, canvas.saveCount)
+    }
+
+    @Test fun withSkew() {
+        val beforeCount = canvas.saveCount
+        canvas.withSkew(x = 2.0f, y = 4.0f) {
+            assertThat(beforeCount).isLessThan(saveCount)
+
+            @Suppress("DEPRECATION")
+            matrix.getValues(values) // will work for a software canvas
+
+            assertEquals(2.0f, values[Matrix.MSKEW_X])
+            assertEquals(4.0f, values[Matrix.MSKEW_Y])
+        }
+        assertEquals(beforeCount, canvas.saveCount)
+    }
+
+    @Suppress("DEPRECATION")
+    @Test fun withMatrix() {
+        val originMatrix = canvas.matrix
+
+        val inputMatrix = Matrix()
+        inputMatrix.postTranslate(16.0f, 32.0f)
+        inputMatrix.postRotate(90.0f, 16.0f, 32.0f)
+        inputMatrix.postScale(2.0f, 4.0f, 16.0f, 32.0f)
+
+        val beforeCount = canvas.saveCount
+        canvas.withMatrix(inputMatrix) {
+            assertThat(beforeCount).isLessThan(saveCount)
+            assertEquals(inputMatrix, matrix)
+        }
+
+        assertEquals(originMatrix, canvas.matrix)
+        assertEquals(beforeCount, canvas.saveCount)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/ColorTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/ColorTest.kt
new file mode 100644
index 0000000..5c647d1
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/ColorTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Color
+import android.graphics.ColorSpace
+import android.support.test.filters.SdkSuppress
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ColorTest {
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun destructuringColor() {
+        val (r, g, b, a) = 0x337f3010.toColor()
+        assertEquals(0.5f, r, 1e-2f)
+        assertEquals(0.19f, g, 1e-2f)
+        assertEquals(0.06f, b, 1e-2f)
+        assertEquals(0.2f, a, 1e-2f)
+    }
+
+    @Test fun destructuringInt() {
+        val (a, r, g, b) = 0x337f3010
+        assertEquals(0x33, a)
+        assertEquals(0x7f, r)
+        assertEquals(0x30, g)
+        assertEquals(0x10, b)
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun intToColor() = assertEquals(Color.valueOf(0x337f3010), 0x337f3010.toColor())
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun intToColorLong() = assertEquals(Color.pack(0x337f3010), 0x337f3010.toColorLong())
+
+    @Test fun alpha() = assertEquals(0x33, 0x337f3010.alpha)
+    @Test fun red() = assertEquals(0x7f, 0x337f3010.red)
+    @Test fun green() = assertEquals(0x30, 0x337f3010.green)
+    @Test fun blue() = assertEquals(0x10, 0x337f3010.blue)
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun luminance() = assertEquals(0.212f, 0xff7f7f7f.toInt().luminance, 1e-3f)
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun longToColor() {
+        assertEquals(Color.valueOf(0x337f3010), Color.pack(0x337f3010).toColor())
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun longToColorInt() = assertEquals(0x337f3010, Color.pack(0x337f3010).toColorInt())
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun destructuringLong() {
+        val (r, g, b, a) = Color.pack(0x337f3010)
+        assertEquals(0.20f, a, 1e-2f)
+        assertEquals(0.50f, r, 1e-2f)
+        assertEquals(0.19f, g, 1e-2f)
+        assertEquals(0.06f, b, 1e-2f)
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun alphaLong() = assertEquals(0.20f, Color.pack(0x337f3010).alpha, 1e-2f)
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun redLong() = assertEquals(0.50f, Color.pack(0x337f3010).red, 1e-2f)
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun greenLong() = assertEquals(0.19f, Color.pack(0x337f3010).green, 1e-2f)
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun blueLong() = assertEquals(0.06f, Color.pack(0x337f3010).blue, 1e-2f)
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun luminanceLong() {
+        assertEquals(0.212f, Color.pack(0xff7f7f7f.toInt()).luminance, 1e-3f)
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun isSrgb() {
+        assertTrue(0x337f3010.toColorLong().isSrgb)
+        val c = Color.pack(1.0f, 0.0f, 0.0f, 1.0f, ColorSpace.get(ColorSpace.Named.BT2020))
+        assertFalse(c.isSrgb)
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun isWideGamut() {
+        assertFalse(0x337f3010.toColorLong().isWideGamut)
+        val c = Color.pack(1.0f, 0.0f, 0.0f, 1.0f, ColorSpace.get(ColorSpace.Named.BT2020))
+        assertTrue(c.isWideGamut)
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun getColorSpace() {
+        val sRGB = ColorSpace.get(ColorSpace.Named.SRGB)
+        assertEquals(sRGB, 0x337f3010.toColorLong().colorSpace)
+
+        val bt2020 = ColorSpace.get(ColorSpace.Named.BT2020)
+        val c = Color.pack(1.0f, 0.0f, 0.0f, 1.0f, bt2020)
+        assertEquals(bt2020, c.colorSpace)
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun addColorsSameColorSpace() {
+        val (r, g, b, a) = 0x7f7f0000.toColor() + 0x7f007f00.toColor()
+        assertEquals(0.16f, r, 1e-2f)
+        assertEquals(0.33f, g, 1e-2f)
+        assertEquals(0.00f, b, 1e-2f)
+        assertEquals(0.75f, a, 1e-2f)
+    }
+
+    @Test fun stringToColorInt() = assertEquals(Color.GREEN, "#00ff00".toColorInt())
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/MatrixTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/MatrixTest.kt
new file mode 100644
index 0000000..61fce4f
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/MatrixTest.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Matrix
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class MatrixTest {
+    @Test fun translationMatrix() {
+        val r = translationMatrix(2.0f, 3.0f).values()
+        assertEquals(1.0f, r[Matrix.MSCALE_X])
+        assertEquals(0.0f, r[Matrix.MSKEW_X])
+        assertEquals(2.0f, r[Matrix.MTRANS_X])
+        assertEquals(0.0f, r[Matrix.MSKEW_Y])
+        assertEquals(1.0f, r[Matrix.MSCALE_Y])
+        assertEquals(3.0f, r[Matrix.MTRANS_Y])
+    }
+
+    @Test fun scaleMatrix() {
+        val r = scaleMatrix(2.0f, 3.0f).values()
+        assertEquals(2.0f, r[Matrix.MSCALE_X])
+        assertEquals(0.0f, r[Matrix.MSKEW_X])
+        assertEquals(0.0f, r[Matrix.MTRANS_X])
+        assertEquals(0.0f, r[Matrix.MSKEW_Y])
+        assertEquals(3.0f, r[Matrix.MSCALE_Y])
+        assertEquals(0.0f, r[Matrix.MTRANS_Y])
+    }
+
+    @Test fun rotationMatrix() {
+        val r = rotationMatrix(90.0f, 2.0f, 3.0f).values()
+        assertEquals(0.0f, r[Matrix.MSCALE_X])
+        assertEquals(-1.0f, r[Matrix.MSKEW_X])
+        assertEquals(5.0f, r[Matrix.MTRANS_X])
+        assertEquals(1.0f, r[Matrix.MSKEW_Y])
+        assertEquals(0.0f, r[Matrix.MSCALE_Y])
+        assertEquals(1.0f, r[Matrix.MTRANS_Y])
+    }
+
+    @Test fun multiply() {
+        val t = translationMatrix(2.0f, 3.0f)
+        val s = scaleMatrix(2.0f, 3.0f)
+        val r = (s * t).values()
+
+        assertEquals(4.0f, r[Matrix.MTRANS_X], 1e-4f)
+        assertEquals(9.0f, r[Matrix.MTRANS_Y], 1e-4f)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/PathTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/PathTest.kt
new file mode 100644
index 0000000..36f2cba
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/PathTest.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Path
+import android.graphics.RectF
+import android.support.test.filters.SdkSuppress
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class PathTest {
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun testFlatten() {
+        val p = Path()
+
+        // Start with several moves
+        p.moveTo(5.0f, 5.0f)
+        p.moveTo(10.0f, 10.0f)
+        p.lineTo(20.0f, 20.0f)
+        p.lineTo(30.0f, 10.0f)
+        // Several moves in the middle
+        p.moveTo(40.0f, 10.0f)
+        p.moveTo(50.0f, 10.0f)
+        p.lineTo(60.0f, 20.0f)
+        // End with several moves
+        p.moveTo(10.0f, 10.0f)
+        p.moveTo(30.0f, 30.0f)
+
+        var count = 0
+        p.flatten().forEach {
+            count++
+            assertNotEquals(it.startFraction, it.endFraction)
+        }
+        assertEquals(3, count)
+    }
+
+    @SdkSuppress(minSdkVersion = 19)
+    @Test fun testUnion() {
+        val r1 = Path().apply { addRect(0.0f, 0.0f, 10.0f, 10.0f, Path.Direction.CW) }
+        val r2 = Path().apply { addRect(5.0f, 0.0f, 15.0f, 15.0f, Path.Direction.CW) }
+
+        val p = r1 + r2
+        val r = RectF()
+        p.computeBounds(r, true)
+
+        assertEquals(RectF(0.0f, 0.0f, 15.0f, 15.0f), r)
+    }
+
+    @SdkSuppress(minSdkVersion = 19)
+    @Test fun testAnd() {
+        val r1 = Path().apply { addRect(0.0f, 0.0f, 10.0f, 10.0f, Path.Direction.CW) }
+        val r2 = Path().apply { addRect(5.0f, 0.0f, 15.0f, 15.0f, Path.Direction.CW) }
+
+        val p = r1 and r2
+        val r = RectF()
+        p.computeBounds(r, true)
+
+        assertEquals(RectF(0.0f, 0.0f, 15.0f, 15.0f), r)
+    }
+
+    @SdkSuppress(minSdkVersion = 19)
+    @Test fun testDifference() {
+        val r1 = Path().apply { addRect(0.0f, 0.0f, 10.0f, 10.0f, Path.Direction.CW) }
+        val r2 = Path().apply { addRect(5.0f, 0.0f, 15.0f, 15.0f, Path.Direction.CW) }
+
+        val p = r1 - r2
+        val r = RectF()
+        p.computeBounds(r, true)
+
+        assertEquals(RectF(0.0f, 0.0f, 5.0f, 10.0f), r)
+    }
+
+    @SdkSuppress(minSdkVersion = 19)
+    @Test fun testIntersection() {
+        val r1 = Path().apply { addRect(0.0f, 0.0f, 10.0f, 10.0f, Path.Direction.CW) }
+        val r2 = Path().apply { addRect(5.0f, 0.0f, 15.0f, 15.0f, Path.Direction.CW) }
+
+        val p = r1 or r2
+        val r = RectF()
+        p.computeBounds(r, true)
+
+        assertEquals(RectF(5.0f, 0.0f, 10.0f, 10.0f), r)
+    }
+
+    @SdkSuppress(minSdkVersion = 19)
+    @Test fun testEmptyIntersection() {
+        val r1 = Path().apply { addRect(0.0f, 0.0f, 2.0f, 2.0f, Path.Direction.CW) }
+        val r2 = Path().apply { addRect(5.0f, 5.0f, 7.0f, 7.0f, Path.Direction.CW) }
+
+        val p = r1 or r2
+        assertTrue(p.isEmpty)
+    }
+
+    @SdkSuppress(minSdkVersion = 19)
+    @Test fun testXor() {
+        val r1 = Path().apply { addRect(0.0f, 0.0f, 10.0f, 10.0f, Path.Direction.CW) }
+        val r2 = Path().apply { addRect(5.0f, 5.0f, 15.0f, 15.0f, Path.Direction.CW) }
+
+        val p = r1 xor r2
+        val r = RectF()
+        p.computeBounds(r, true)
+
+        assertEquals(RectF(0.0f, 0.0f, 15.0f, 15.0f), r)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/PictureTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/PictureTest.kt
new file mode 100644
index 0000000..6d0029f
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/PictureTest.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Color
+import android.graphics.Picture
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class PictureTest {
+    @Test fun record() {
+        val p = Picture().record(1, 1) {
+            drawColor(Color.RED)
+        }
+        val v = createBitmap(1, 1).applyCanvas {
+            drawPicture(p)
+        }.getPixel(0, 0)
+        assertEquals(0xffff0000.toInt(), v)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/PointTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/PointTest.kt
new file mode 100644
index 0000000..791d35f
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/PointTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Point
+import android.graphics.PointF
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class PointTest {
+    @Test fun destructuringInt() {
+        val (x, y) = Point(2, 3)
+        assertEquals(2, x)
+        assertEquals(3, y)
+    }
+
+    @Test fun destructuringFloat() {
+        val (x, y) = PointF(2.0f, 3.0f)
+        assertEquals(2.0f, x)
+        assertEquals(3.0f, y)
+    }
+
+    @Test fun offsetInt() {
+        val (x, y) = Point(2, 3) + 2
+        assertEquals(4, x)
+        assertEquals(5, y)
+    }
+
+    @Test fun offsetFloat() {
+        val (x, y) = PointF(2.0f, 3.0f) + 2.0f
+        assertEquals(4.0f, x)
+        assertEquals(5.0f, y)
+    }
+
+    @Test fun offsetPoint() {
+        val (x, y) = Point(2, 3) + Point(1, 2)
+        assertEquals(3, x)
+        assertEquals(5, y)
+    }
+
+    @Test fun offsetPointF() {
+        val (x, y) = PointF(2.0f, 3.0f) + PointF(1.0f, 2.0f)
+        assertEquals(3.0f, x)
+        assertEquals(5.0f, y)
+    }
+
+    @Test fun negativeOffsetInt() {
+        val (x, y) = Point(2, 3) - 2
+        assertEquals(0, x)
+        assertEquals(1, y)
+    }
+
+    @Test fun negativeOffsetFloat() {
+        val (x, y) = PointF(2.0f, 3.0f) - 2.0f
+        assertEquals(0.0f, x)
+        assertEquals(1.0f, y)
+    }
+
+    @Test fun negativeOffsetPoint() {
+        val (x, y) = Point(2, 3) - Point(1, 2)
+        assertEquals(1, x)
+        assertEquals(1, y)
+    }
+
+    @Test fun negativeOffsetPointF() {
+        val (x, y) = PointF(2.0f, 3.0f) - PointF(1.0f, 2.0f)
+        assertEquals(1.0f, x)
+        assertEquals(1.0f, y)
+    }
+
+    @Test fun negateInt() {
+        val (x, y) = -Point(2, 3)
+        assertEquals(-2, x)
+        assertEquals(-3, y)
+    }
+
+    @Test fun negateFloat() {
+        val (x, y) = -PointF(2.0f, 3.0f)
+        assertEquals(-2.0f, x)
+        assertEquals(-3.0f, y)
+    }
+
+    @Test fun toPointF() {
+        val pointF = Point(1, 2).toPointF()
+        assertEquals(1f, pointF.x, 0f)
+        assertEquals(2f, pointF.y, 0f)
+    }
+
+    @Test fun toPoint() {
+        assertEquals(Point(1, 2), PointF(1.1f, 2.8f).toPoint())
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/PorterDuffTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/PorterDuffTest.kt
new file mode 100644
index 0000000..9b44a50
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/PorterDuffTest.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class PorterDuffTest {
+    @Test fun xfermode() {
+        val p = createBitmap(1, 1).applyCanvas {
+            val p = Paint().apply { color = 0xffffffff.toInt() }
+            drawRect(0f, 0f, 1f, 1f, p)
+
+            p.color = 0x7f00ff00
+            p.xfermode = PorterDuff.Mode.SRC.toXfermode()
+            drawRect(0f, 0f, 1f, 1f, p)
+        }.getPixel(0, 0)
+
+        assertEquals(0x7f00ff00, p)
+    }
+
+    @Test fun colorFilter() {
+        val p = createBitmap(1, 1).applyCanvas {
+            val p = Paint().apply { color = 0xffffffff.toInt() }
+            drawRect(0f, 0f, 1f, 1f, p)
+
+            p.color = 0xff000000.toInt()
+            p.colorFilter = PorterDuff.Mode.SRC.toColorFilter(0xff00ff00.toInt())
+            drawRect(0f, 0f, 1f, 1f, p)
+        }.getPixel(0, 0)
+
+        assertEquals(0xff00ff00.toInt(), p)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/RectTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/RectTest.kt
new file mode 100644
index 0000000..8b2613f
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/RectTest.kt
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Matrix
+import android.graphics.Point
+import android.graphics.PointF
+import android.graphics.Rect
+import android.graphics.RectF
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class RectTest {
+    @Test fun destructuringInt() {
+        val (l, t, r, b) = Rect(4, 8, 16, 24)
+        assertEquals(4, l)
+        assertEquals(8, t)
+        assertEquals(16, r)
+        assertEquals(24, b)
+    }
+
+    @Test fun destructuringFloat() {
+        val (l, t, r, b) = RectF(4.0f, 8.0f, 16.0f, 24.0f)
+        assertEquals(4.0f, l)
+        assertEquals(8.0f, t)
+        assertEquals(16.0f, r)
+        assertEquals(24.0f, b)
+    }
+
+    @Test fun unionInt() {
+        val (l, t, r, b) = Rect(0, 0, 4, 4) + Rect(-1, -1, 6, 6)
+        assertEquals(-1, l)
+        assertEquals(-1, t)
+        assertEquals(6, r)
+        assertEquals(6, b)
+    }
+
+    @Test fun unionAsAndInt() {
+        val (l, t, r, b) = Rect(0, 0, 4, 4) and Rect(-1, -1, 6, 6)
+        assertEquals(-1, l)
+        assertEquals(-1, t)
+        assertEquals(6, r)
+        assertEquals(6, b)
+    }
+
+    @Test fun unionFloat() {
+        val (l, t, r, b) = RectF(0.0f, 0.0f, 4.0f, 4.0f) + RectF(-1.0f, -1.0f, 6.0f, 6.0f)
+        assertEquals(-1.0f, l)
+        assertEquals(-1.0f, t)
+        assertEquals(6.0f, r)
+        assertEquals(6.0f, b)
+    }
+
+    @Test fun unionAsAndFloat() {
+        val (l, t, r, b) = RectF(0.0f, 0.0f, 4.0f, 4.0f) and RectF(-1.0f, -1.0f, 6.0f, 6.0f)
+        assertEquals(-1.0f, l)
+        assertEquals(-1.0f, t)
+        assertEquals(6.0f, r)
+        assertEquals(6.0f, b)
+    }
+
+    @Test fun differenceInt() {
+        val r = Rect(0, 0, 4, 4) - Rect(-1, 2, 6, 6)
+        assertEquals(Rect(0, 0, 4, 2), r.bounds)
+    }
+
+    @Test fun differenceFloat() {
+        val r = RectF(0.0f, 0.0f, 4.0f, 4.0f) - RectF(-1.0f, 2.0f, 6.0f, 6.0f)
+        assertEquals(Rect(0, 0, 4, 2), r.bounds)
+    }
+
+    @Test fun intersectionAsOrInt() {
+        val (l, t, r, b) = Rect(0, 0, 4, 4) or Rect(2, 2, 6, 6)
+        assertEquals(2, l)
+        assertEquals(2, t)
+        assertEquals(4, r)
+        assertEquals(4, b)
+    }
+
+    @Test fun intersectionAsOrFloat() {
+        val (l, t, r, b) = RectF(0.0f, 0.0f, 4.0f, 4.0f) or RectF(2.0f, 2.0f, 6.0f, 6.0f)
+        assertEquals(2.0f, l)
+        assertEquals(2.0f, t)
+        assertEquals(4.0f, r)
+        assertEquals(4.0f, b)
+    }
+
+    @Test fun xorInt() {
+        val r = Rect(0, 0, 4, 4) xor Rect(2, 2, 6, 6)
+        assertEquals(Rect(0, 0, 4, 4) and Rect(2, 2, 6, 6), r.bounds)
+        assertFalse(r.contains(3, 3))
+    }
+
+    @Test fun xorFloat() {
+        val r = RectF(0.0f, 0.0f, 4.0f, 4.0f) xor RectF(2.0f, 2.0f, 6.0f, 6.0f)
+        assertEquals(Rect(0, 0, 4, 4) and Rect(2, 2, 6, 6), r.bounds)
+        assertFalse(r.contains(3, 3))
+    }
+
+    @Test fun offsetInt() {
+        val (l, t, r, b) = Rect(0, 0, 2, 2) + 2
+        assertEquals(l, 2)
+        assertEquals(t, 2)
+        assertEquals(r, 4)
+        assertEquals(b, 4)
+    }
+
+    @Test fun offsetPoint() {
+        val (l, t, r, b) = Rect(0, 0, 2, 2) + Point(1, 2)
+        assertEquals(l, 1)
+        assertEquals(t, 2)
+        assertEquals(r, 3)
+        assertEquals(b, 4)
+    }
+
+    @Test fun offsetFloat() {
+        val (l, t, r, b) = RectF(0.0f, 0.0f, 2.0f, 2.0f) + 2.0f
+        assertEquals(l, 2.0f)
+        assertEquals(t, 2.0f)
+        assertEquals(r, 4.0f)
+        assertEquals(b, 4.0f)
+    }
+
+    @Test fun offsetPointF() {
+        val (l, t, r, b) = RectF(0.0f, 0.0f, 2.0f, 2.0f) + PointF(1.0f, 2.0f)
+        assertEquals(l, 1.0f)
+        assertEquals(t, 2.0f)
+        assertEquals(r, 3.0f)
+        assertEquals(b, 4.0f)
+    }
+
+    @Test fun negativeOffsetInt() {
+        val (l, t, r, b) = Rect(0, 0, 2, 2) - 2
+        assertEquals(l, -2)
+        assertEquals(t, -2)
+        assertEquals(r, 0)
+        assertEquals(b, 0)
+    }
+
+    @Test fun negativeOffsetPoint() {
+        val (l, t, r, b) = Rect(0, 0, 2, 2) - Point(1, 2)
+        assertEquals(l, -1)
+        assertEquals(t, -2)
+        assertEquals(r, 1)
+        assertEquals(b, 0)
+    }
+
+    @Test fun negativeOffsetFloat() {
+        val (l, t, r, b) = RectF(0.0f, 0.0f, 2.0f, 2.0f) - 2.0f
+        assertEquals(l, -2.0f)
+        assertEquals(t, -2.0f)
+        assertEquals(r, 0.0f)
+        assertEquals(b, 0.0f)
+    }
+
+    @Test fun negativeOffsetPointF() {
+        val (l, t, r, b) = RectF(0.0f, 0.0f, 2.0f, 2.0f) - PointF(1.0f, 2.0f)
+        assertEquals(l, -1.0f)
+        assertEquals(t, -2.0f)
+        assertEquals(r, 1.0f)
+        assertEquals(b, 0.0f)
+    }
+
+    @Test fun pointInside() {
+        assertTrue(Point(1, 1) in Rect(0, 0, 2, 2))
+        assertTrue(PointF(1.0f, 1.0f) in RectF(0.0f, 0.0f, 2.0f, 2.0f))
+    }
+
+    @Test fun toRect() {
+        assertEquals(
+            Rect(0, 1, 2, 3),
+            RectF(0f, 1f, 2f, 3f).toRect())
+
+        assertEquals(
+            Rect(0, 1, 2, 3),
+            RectF(0.1f, 1.1f, 1.9f, 2.9f).toRect())
+    }
+
+    @Test fun toRectF() {
+        val rectF = Rect(0, 1, 2, 3).toRectF()
+        assertEquals(0f, rectF.left, 0f)
+        assertEquals(1f, rectF.top, 0f)
+        assertEquals(2f, rectF.right, 0f)
+        assertEquals(3f, rectF.bottom, 0f)
+    }
+
+    @Test fun toRegionInt() {
+        assertEquals(Rect(1, 1, 5, 5), Rect(1, 1, 5, 5).toRegion().bounds)
+    }
+
+    @Test fun toRegionFloat() {
+        assertEquals(Rect(1, 1, 5, 5), RectF(1.1f, 1.1f, 4.8f, 4.8f).toRegion().bounds)
+    }
+
+    @Test fun transformRectToRect() {
+        val m = Matrix()
+        m.setScale(2.0f, 2.0f)
+
+        val r = RectF(2.0f, 2.0f, 5.0f, 7.0f).transform(m)
+        assertEquals(4f, r.left, 0f)
+        assertEquals(4f, r.top, 0f)
+        assertEquals(10f, r.right, 0f)
+        assertEquals(14f, r.bottom, 0f)
+    }
+
+    @Test fun transformRectNotPreserved() {
+        val m = Matrix()
+        m.setRotate(45.0f)
+
+        val (l, t, r, b) = RectF(-1.0f, -1.0f, 1.0f, 1.0f).transform(m)
+        assertEquals(-1.414f, l, 1e-3f)
+        assertEquals(-1.414f, t, 1e-3f)
+        assertEquals( 1.414f, r, 1e-3f)
+        assertEquals( 1.414f, b, 1e-3f)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/RegionTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/RegionTest.kt
new file mode 100644
index 0000000..21e6443
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/RegionTest.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Point
+import android.graphics.Rect
+import android.graphics.Region
+import androidx.testutils.assertThrows
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class RegionTest {
+    @Test fun containsPoint() {
+        assertFalse(Point(1, 1) in Region())
+        assertTrue(Point(1, 1) in Region(0, 0, 2, 2))
+    }
+
+    @Test fun unionRect() {
+        val r = Region(0, 0, 2, 2) + Rect(4, 4, 6, 6)
+        assertFalse(Point(3, 3) in r)
+        assertTrue(Point(1, 1) in r)
+        assertTrue(Point(5, 5) in r)
+    }
+
+    @Test fun unionRegion() {
+        val r = Region(0, 0, 2, 2) + Region(4, 4, 6, 6)
+        assertFalse(Point(3, 3) in r)
+        assertTrue(Point(1, 1) in r)
+        assertTrue(Point(5, 5) in r)
+    }
+
+    @Test fun unionAsAndRect() {
+        val r = Region(0, 0, 2, 2) and Rect(4, 4, 6, 6)
+        assertFalse(Point(3, 3) in r)
+        assertTrue(Point(1, 1) in r)
+        assertTrue(Point(5, 5) in r)
+    }
+
+    @Test fun unionAsAndRegion() {
+        val r = Region(0, 0, 2, 2) and Region(4, 4, 6, 6)
+        assertFalse(Point(3, 3) in r)
+        assertTrue(Point(1, 1) in r)
+        assertTrue(Point(5, 5) in r)
+    }
+
+    @Test fun differenceRect() {
+        val r = Region(0, 0, 4, 4) - Rect(2, 2, 6, 6)
+        assertFalse(Point(3, 3) in r)
+        assertTrue(Point(1, 1) in r)
+        assertFalse(Point(5, 5) in r)
+    }
+
+    @Test fun differenceRegion() {
+        val r = Region(0, 0, 4, 4) - Region(2, 2, 6, 6)
+        assertFalse(Point(3, 3) in r)
+        assertTrue(Point(1, 1) in r)
+        assertFalse(Point(5, 5) in r)
+    }
+
+    @Test fun unaryMinus() {
+        val r = Rect(0, 0, 10, 10) - Rect(4, 4, 6, 6)
+        assertTrue(Point(1, 1) in r)
+        assertFalse(Point(5, 5) in r)
+
+        val i = -r
+        assertFalse(Point(1, 1) in i)
+        assertTrue(Point(5, 5) in i)
+    }
+
+    @Test fun not() {
+        val r = Rect(0, 0, 10, 10) - Rect(4, 4, 6, 6)
+        assertTrue(Point(1, 1) in r)
+        assertFalse(Point(5, 5) in r)
+
+        val i = !r
+        assertFalse(Point(1, 1) in i)
+        assertTrue(Point(5, 5) in i)
+    }
+
+    @Test fun orRect() {
+        val r = Region(0, 0, 4, 4) or Rect(2, 2, 6, 6)
+        assertFalse(Point(1, 1) in r)
+        assertTrue(Point(3, 3) in r)
+    }
+
+    @Test fun orRegion() {
+        val r = Region(0, 0, 4, 4) or Region(2, 2, 6, 6)
+        assertFalse(Point(1, 1) in r)
+        assertTrue(Point(3, 3) in r)
+    }
+
+    @Test fun xorRect() {
+        val r = Region(0, 0, 4, 4) xor Rect(2, 2, 6, 6)
+        assertFalse(Point(3, 3) in r)
+        assertTrue(Point(1, 1) in r)
+    }
+
+    @Test fun xorRegion() {
+        val r = Region(0, 0, 4, 4) xor Region(2, 2, 6, 6)
+        assertFalse(Point(3, 3) in r)
+        assertTrue(Point(1, 1) in r)
+    }
+
+    @Test fun iteratorForLoop() {
+        val region = Region(0, 0, 4, 4) -
+                Rect(2, 2, 6, 6)
+        var count = 0
+        var r = Rect()
+        for (rect in region) {
+            count++
+            assertNotSame(r, rect)
+            r = rect
+        }
+        assertEquals(2, count)
+    }
+
+    @Test fun iteratorOutOfBounds() {
+        val region = Region(0, 0, 4, 4) -
+                Rect(2, 2, 6, 6)
+        val it = region.iterator()
+        it.next()
+        it.next()
+        assertThrows<IndexOutOfBoundsException> {
+            it.next()
+        }
+    }
+
+    @Test fun iteratorForEach() {
+        val region = Region(0, 0, 4, 4) -
+                Rect(2, 2, 6, 6)
+        var count = 0
+        var r = Rect()
+        region.forEach {
+            count++
+            assertNotSame(r, it)
+            r = it
+        }
+        assertEquals(2, count)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/ShaderTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/ShaderTest.kt
new file mode 100644
index 0000000..ad2b63c
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/ShaderTest.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Matrix
+import android.graphics.Shader
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ShaderTest {
+    @Test
+    fun testTransform() {
+        @Suppress("DEPRECATION")
+        val shader = Shader()
+        val values = FloatArray(9)
+        val matrix = Matrix()
+
+        shader.transform {
+            setTranslate(10f, 30f)
+        }
+
+        // Now read matrix from Shader
+        shader.getLocalMatrix(matrix)
+        matrix.getValues(values)
+
+        // Assert that the values are as expected
+        assertEquals(10f, values[Matrix.MTRANS_X])
+        assertEquals(30f, values[Matrix.MTRANS_Y])
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/BitmapDrawableTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/BitmapDrawableTest.kt
new file mode 100644
index 0000000..4c4ceb6
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/BitmapDrawableTest.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+package androidx.core.graphics.drawable
+
+import android.support.test.InstrumentationRegistry
+import androidx.core.graphics.createBitmap
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class BitmapDrawableTest {
+    private val context = InstrumentationRegistry.getContext()
+
+    @Test fun fromBitmap() {
+        val b = createBitmap(1, 1)
+        val drawable = b.toDrawable(context.resources)
+        assertEquals(b, drawable.bitmap)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/ColorDrawableTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/ColorDrawableTest.kt
new file mode 100644
index 0000000..ad7a39b
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/ColorDrawableTest.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+package androidx.core.graphics.drawable
+
+import android.graphics.Color
+import android.support.test.filters.SdkSuppress
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ColorDrawableTest {
+    @Test fun fromInt() {
+        val drawable = Color.CYAN.toDrawable()
+        assertEquals(Color.CYAN, drawable.color)
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test fun fromColor() {
+        val drawable = Color.valueOf(Color.CYAN).toDrawable()
+        assertEquals(Color.CYAN, drawable.color)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/DrawableTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/DrawableTest.kt
new file mode 100644
index 0000000..6455d0a
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/DrawableTest.kt
@@ -0,0 +1,167 @@
+/*
+ * 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.
+ */
+
+package androidx.core.graphics.drawable
+
+import android.graphics.Bitmap.Config
+import android.graphics.Color
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.ColorDrawable
+import android.support.test.InstrumentationRegistry
+import androidx.core.graphics.createBitmap
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class DrawableTest {
+    private val context = InstrumentationRegistry.getContext()
+    private val resources = context.resources
+
+    @Test fun bitmapDrawableNoSizeNoConfig() {
+        val original = createBitmap(10, 10).apply {
+            eraseColor(Color.RED)
+        }
+        val drawable = BitmapDrawable(resources, original)
+
+        val bitmap = drawable.toBitmap()
+        assertEquals(10, bitmap.width)
+        assertEquals(10, bitmap.height)
+        assertEquals(Config.ARGB_8888, bitmap.config)
+        assertEquals(Color.RED, bitmap.getPixel(5, 5))
+    }
+
+    @Test fun bitmapDrawableNoSizeRetainedConfig() {
+        val original = createBitmap(10, 10).apply {
+            eraseColor(Color.RED)
+        }
+        val drawable = BitmapDrawable(resources, original)
+
+        val bitmap = drawable.toBitmap(config = Config.ARGB_8888)
+        assertEquals(10, bitmap.width)
+        assertEquals(10, bitmap.height)
+        assertEquals(Config.ARGB_8888, bitmap.config)
+        assertEquals(Color.RED, bitmap.getPixel(5, 5))
+    }
+
+    @Test fun bitmapDrawableNoSizeDifferentConfig() {
+        val original = createBitmap(10, 10).apply {
+            eraseColor(Color.RED)
+        }
+        val drawable = BitmapDrawable(resources, original)
+
+        val bitmap = drawable.toBitmap(config = Config.ARGB_8888)
+        assertEquals(10, bitmap.width)
+        assertEquals(10, bitmap.height)
+        assertEquals(Config.ARGB_8888, bitmap.config)
+        assertEquals(Color.RED, bitmap.getPixel(5, 5))
+    }
+
+    @Test fun bitmapDrawableDifferentSizeNoConfig() {
+        val original = createBitmap(10, 10).apply {
+            eraseColor(Color.RED)
+        }
+        val drawable = BitmapDrawable(resources, original)
+
+        val bitmap = drawable.toBitmap(20, 20)
+        assertEquals(20, bitmap.width)
+        assertEquals(20, bitmap.height)
+        assertEquals(Config.ARGB_8888, bitmap.config)
+        assertEquals(Color.RED, bitmap.getPixel(10, 10))
+    }
+
+    @Test fun bitmapDrawableDifferentSizeDifferentConfig() {
+        val original = createBitmap(10, 10).apply {
+            eraseColor(Color.RED)
+        }
+        val drawable = BitmapDrawable(resources, original)
+
+        val bitmap = drawable.toBitmap(20, 20, Config.ARGB_8888)
+        assertEquals(20, bitmap.width)
+        assertEquals(20, bitmap.height)
+        assertEquals(Config.ARGB_8888, bitmap.config)
+        assertEquals(Color.RED, bitmap.getPixel(10, 10))
+    }
+
+    @Test fun drawableNoConfig() {
+        val drawable = object : ColorDrawable(Color.RED) {
+            override fun getIntrinsicWidth() = 10
+            override fun getIntrinsicHeight() = 10
+        }
+
+        val bitmap = drawable.toBitmap()
+        assertEquals(10, bitmap.width)
+        assertEquals(10, bitmap.height)
+        assertEquals(Config.ARGB_8888, bitmap.config)
+        assertEquals(Color.RED, bitmap.getPixel(5, 5))
+    }
+
+    @Test fun drawableConfig() {
+        val drawable = object : ColorDrawable(Color.RED) {
+            override fun getIntrinsicWidth() = 10
+            override fun getIntrinsicHeight() = 10
+        }
+
+        val bitmap = drawable.toBitmap(config = Config.RGB_565)
+        assertEquals(10, bitmap.width)
+        assertEquals(10, bitmap.height)
+        assertEquals(Config.RGB_565, bitmap.config)
+        assertEquals(Color.RED, bitmap.getPixel(5, 5))
+    }
+
+    @Test fun drawableSize() {
+        val drawable = object : ColorDrawable(Color.RED) {
+            override fun getIntrinsicWidth() = 10
+            override fun getIntrinsicHeight() = 10
+        }
+
+        val bitmap = drawable.toBitmap(20, 20)
+        assertEquals(20, bitmap.width)
+        assertEquals(20, bitmap.height)
+        assertEquals(Config.ARGB_8888, bitmap.config)
+        assertEquals(Color.RED, bitmap.getPixel(10, 10))
+    }
+
+    @Test fun oldBoundsRestored() {
+        val drawable = object : ColorDrawable(Color.RED) {
+            override fun getIntrinsicWidth() = 10
+            override fun getIntrinsicHeight() = 10
+        }
+        drawable.setBounds(2, 2, 8, 8)
+
+        val bitmap = drawable.toBitmap()
+        assertEquals(10, bitmap.width)
+        assertEquals(10, bitmap.height)
+
+        assertEquals(2, drawable.bounds.left)
+        assertEquals(2, drawable.bounds.top)
+        assertEquals(8, drawable.bounds.right)
+        assertEquals(8, drawable.bounds.bottom)
+    }
+
+    @Test fun updateBoundsTest() {
+        val drawable = object : ColorDrawable(Color.RED) {
+            override fun getIntrinsicWidth() = 10
+            override fun getIntrinsicHeight() = 10
+        }
+        drawable.setBounds(0, 0, 10, 10)
+
+        drawable.updateBounds(1, 2, 3, 4)
+
+        assertEquals(1, drawable.bounds.left)
+        assertEquals(2, drawable.bounds.top)
+        assertEquals(3, drawable.bounds.right)
+        assertEquals(4, drawable.bounds.bottom)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/IconTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/IconTest.kt
new file mode 100644
index 0000000..e2dd84c
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/IconTest.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+
+package androidx.core.graphics.drawable
+
+import android.graphics.Bitmap
+import android.graphics.Bitmap.Config.ARGB_8888
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.drawable.Icon
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SdkSuppress
+import androidx.core.graphics.createBitmap
+import androidx.core.net.toUri
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.io.File
+
+@SdkSuppress(minSdkVersion = 26)
+class IconTest {
+    private val context = InstrumentationRegistry.getContext()
+
+    @Test fun fromBitmapAdaptive() {
+        val density = context.resources.displayMetrics.density
+
+        val edge = (108.0f * density + 0.5f).toInt()
+        val bitmap = Bitmap.createBitmap(edge, edge, ARGB_8888).apply {
+            eraseColor(Color.RED)
+        }
+        val icon = bitmap.toAdaptiveIcon()
+
+        val rendered = icon.toIntrinsicBitmap()
+        val masked = (72.0f * density + 0.5f).toInt()
+        assertEquals(masked, rendered.width)
+        assertEquals(masked, rendered.height)
+        // Grab a pixel from the middle to ensure we are not being masked.
+        assertEquals(Color.RED, rendered.getPixel(masked / 2, masked / 2))
+    }
+
+    @Test fun fromBitmap() {
+        val bitmap = createBitmap(1, 1).apply {
+            eraseColor(Color.RED)
+        }
+        val icon = bitmap.toIcon()
+
+        val rendered = icon.toIntrinsicBitmap()
+        assertEquals(1, rendered.width)
+        assertEquals(1, rendered.height)
+        assertEquals(Color.RED, rendered.getPixel(0, 0))
+    }
+
+    @Test fun fromUri() {
+        // Icon can't read from file:///android_asset/red.png so copy to a real file.
+        val cacheFile = File(context.cacheDir, "red.png")
+        context.assets.open("red.png").use { cacheFile.writeBytes(it.readBytes()) }
+
+        val uri = cacheFile.toUri()
+        val icon = uri.toIcon()
+
+        val rendered = icon.toIntrinsicBitmap()
+        assertEquals(1, rendered.width)
+        assertEquals(1, rendered.height)
+        assertEquals(Color.RED, rendered.getPixel(0, 0))
+    }
+
+    @Test fun fromByteArray() {
+        val bytes = context.assets.open("red.png").use { it.readBytes() }
+        val icon = bytes.toIcon()
+
+        val rendered = icon.toIntrinsicBitmap()
+        assertEquals(1, rendered.width)
+        assertEquals(1, rendered.height)
+        assertEquals(Color.RED, rendered.getPixel(0, 0))
+    }
+
+    private fun Icon.toIntrinsicBitmap(): Bitmap {
+        val drawable = loadDrawable(context)
+        val bitmap = createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight)
+        drawable.setBounds(0, 0, drawable.intrinsicHeight, drawable.intrinsicHeight)
+        drawable.draw(Canvas(bitmap))
+        return bitmap
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/location/LocationTest.kt b/core/ktx/src/androidTest/java/androidx/core/location/LocationTest.kt
new file mode 100644
index 0000000..0f289ef
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/location/LocationTest.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+package androidx.core.location
+
+import android.location.Location
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class LocationTest {
+    @Test fun destructuring() {
+        val (lat, lon) = Location("").apply {
+            latitude = 1.0
+            longitude = 2.0
+        }
+        assertEquals(lat, 1.0, 0.0)
+        assertEquals(lon, 2.0, 0.0)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/net/UriTest.kt b/core/ktx/src/androidTest/java/androidx/core/net/UriTest.kt
new file mode 100644
index 0000000..d25de35
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/net/UriTest.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.net
+
+import android.net.Uri
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.io.File
+
+class UriTest {
+    @Test fun uriFromString() {
+        val string = "https://test.example.com/foo?bar#baz"
+        assertEquals(Uri.parse(string), string.toUri())
+    }
+
+    @Test fun uriFromFile() {
+        val file = File("/path/to/my/file")
+        assertEquals(Uri.fromFile(file), file.toUri())
+    }
+
+    @Test fun fileFromUri() {
+        val uri = Uri.parse("path/to/my/file")
+        assertEquals(File(uri.path), uri.toFile())
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/os/BundleTest.kt b/core/ktx/src/androidTest/java/androidx/core/os/BundleTest.kt
new file mode 100644
index 0000000..924e9ad
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/os/BundleTest.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.
+ */
+
+package androidx.core.os
+
+import android.graphics.Rect
+import android.os.Binder
+import android.os.Bundle
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SdkSuppress
+import android.util.Size
+import android.util.SizeF
+import android.view.View
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Test
+import java.util.concurrent.atomic.AtomicInteger
+
+class BundleTest {
+    @Test fun bundleOfValid() {
+        val bundleValue = Bundle()
+        val charSequenceValue = "hey"
+        val parcelableValue = Rect(1, 2, 3, 4)
+        val serializableValue = AtomicInteger(1)
+
+        val bundle = bundleOf(
+            "null" to null,
+
+            "boolean" to true,
+            "byte" to 1.toByte(),
+            "char" to 'a',
+            "double" to 1.0,
+            "float" to 1f,
+            "int" to 1,
+            "long" to 1L,
+            "short" to 1.toShort(),
+
+            "bundle" to bundleValue,
+            "charSequence" to charSequenceValue,
+            "parcelable" to parcelableValue,
+
+            "booleanArray" to booleanArrayOf(),
+            "byteArray" to byteArrayOf(),
+            "charArray" to charArrayOf(),
+            "doubleArray" to doubleArrayOf(),
+            "floatArray" to floatArrayOf(),
+            "intArray" to intArrayOf(),
+            "longArray" to longArrayOf(),
+            "shortArray" to shortArrayOf(),
+
+            "parcelableArray" to arrayOf(parcelableValue),
+            "stringArray" to arrayOf("hey"),
+            "charSequenceArray" to arrayOf<CharSequence>("hey"),
+            "serializableArray" to arrayOf(serializableValue),
+
+            "serializable" to serializableValue
+        )
+
+        assertEquals(25, bundle.size())
+
+        assertNull(bundle["null"])
+
+        assertEquals(true, bundle["boolean"])
+        assertEquals(1.toByte(), bundle["byte"])
+        assertEquals('a', bundle["char"])
+        assertEquals(1.0, bundle["double"])
+        assertEquals(1f, bundle["float"])
+        assertEquals(1, bundle["int"])
+        assertEquals(1L, bundle["long"])
+        assertEquals(1.toShort(), bundle["short"])
+
+        assertSame(bundleValue, bundle["bundle"])
+        assertSame(charSequenceValue, bundle["charSequence"])
+        assertSame(parcelableValue, bundle["parcelable"])
+
+        assertArrayEquals(booleanArrayOf(), bundle["booleanArray"] as BooleanArray)
+        assertArrayEquals(byteArrayOf(), bundle["byteArray"] as ByteArray)
+        assertArrayEquals(charArrayOf(), bundle["charArray"] as CharArray)
+        assertArrayEquals(doubleArrayOf(), bundle["doubleArray"] as DoubleArray, 0.0)
+        assertArrayEquals(floatArrayOf(), bundle["floatArray"] as FloatArray, 0f)
+        assertArrayEquals(intArrayOf(), bundle["intArray"] as IntArray)
+        assertArrayEquals(longArrayOf(), bundle["longArray"] as LongArray)
+        assertArrayEquals(shortArrayOf(), bundle["shortArray"] as ShortArray)
+
+        assertThat(bundle["parcelableArray"] as Array<*>).asList().containsExactly(parcelableValue)
+        assertThat(bundle["stringArray"] as Array<*>).asList().containsExactly("hey")
+        assertThat(bundle["charSequenceArray"] as Array<*>).asList().containsExactly("hey")
+        assertThat(bundle["serializableArray"] as Array<*>).asList()
+            .containsExactly(serializableValue)
+
+        assertSame(serializableValue, bundle["serializable"])
+    }
+
+    @SdkSuppress(minSdkVersion = 18)
+    @Test fun bundleOfValidApi18() {
+        val binderValue = Binder()
+        val bundle = bundleOf("binder" to binderValue)
+        assertSame(binderValue, bundle["binder"])
+    }
+
+    @SdkSuppress(minSdkVersion = 21)
+    @Test fun bundleOfValidApi21() {
+        val sizeValue = Size(1, 1)
+        val sizeFValue = SizeF(1f, 1f)
+
+        val bundle = bundleOf(
+            "size" to sizeValue,
+            "sizeF" to sizeFValue
+        )
+
+        assertSame(sizeValue, bundle["size"])
+        assertSame(sizeFValue, bundle["sizeF"])
+    }
+
+    @Test fun bundleOfInvalid() {
+        assertThrows<IllegalArgumentException> {
+            bundleOf("nope" to View(InstrumentationRegistry.getContext()))
+        }.hasMessageThat().isEqualTo("Illegal value type android.view.View for key \"nope\"")
+
+        assertThrows<IllegalArgumentException> {
+            bundleOf("nopes" to arrayOf(View(InstrumentationRegistry.getContext())))
+        }.hasMessageThat().isEqualTo("Illegal value array type android.view.View for key \"nopes\"")
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/os/HandlerTest.kt b/core/ktx/src/androidTest/java/androidx/core/os/HandlerTest.kt
new file mode 100644
index 0000000..e0fdf4e
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/os/HandlerTest.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.
+ */
+
+package androidx.core.os
+
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.SystemClock
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeUnit.MILLISECONDS
+import java.util.concurrent.TimeUnit.SECONDS
+
+class HandlerTest {
+    private val handlerThread = HandlerThread("handler-test")
+    private lateinit var handler: Handler
+
+    @Before fun before() {
+        handlerThread.start()
+        handler = Handler(handlerThread.looper)
+    }
+
+    @After fun after() {
+        handlerThread.quit()
+    }
+
+    @Test fun postDelayedLambdaMillis() {
+        var called = 0
+        handler.postDelayed(10) {
+            called++
+        }
+
+        handler.await(20, MILLISECONDS)
+        assertEquals(1, called)
+    }
+
+    @Test fun postDelayedLambdaMillisRemoved() {
+        var called = 0
+        val runnable = handler.postDelayed(10) {
+            called++
+        }
+        handler.removeCallbacks(runnable)
+
+        handler.await(20, MILLISECONDS)
+        assertEquals(0, called)
+    }
+
+    @Test fun postAtTimeLambda() {
+        var called = 0
+        handler.postAtTime(SystemClock.uptimeMillis() + 10) {
+            called++
+        }
+
+        handler.await(20, MILLISECONDS)
+        assertEquals(1, called)
+    }
+
+    @Test fun postAtTimeLambdaRemoved() {
+        var called = 0
+        val runnable = handler.postAtTime(SystemClock.uptimeMillis() + 10) {
+            called++
+        }
+        handler.removeCallbacks(runnable)
+
+        handler.await(20, MILLISECONDS)
+        assertEquals(0, called)
+    }
+
+    @Test fun postAtTimeLambdaWithTokenRuns() {
+        val token = Any()
+        var called = 0
+        handler.postAtTime(SystemClock.uptimeMillis() + 10, token) {
+            called++
+        }
+
+        handler.await(20, MILLISECONDS)
+        assertEquals(1, called)
+    }
+
+    @Test fun postAtTimeLambdaWithTokenCancelWithToken() {
+        // This test uses the token to cancel the runnable as it's the only way we have to verify
+        // that the Runnable was actually posted with the token.
+
+        val token = Any()
+        var called = 0
+        handler.postAtTime(SystemClock.uptimeMillis() + 10, token) {
+            called++
+        }
+        handler.removeCallbacksAndMessages(token)
+
+        handler.await(20, MILLISECONDS)
+        assertEquals(0, called)
+    }
+
+    private fun Handler.await(amount: Long, unit: TimeUnit) {
+        val latch = CountDownLatch(1)
+        postDelayed(latch::countDown, unit.toMillis(amount))
+
+        // Wait up to 1s longer than desired to account for time skew.
+        val wait = unit.toMillis(amount) + SECONDS.toMillis(1)
+        assertTrue(latch.await(wait, MILLISECONDS))
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/os/PersistableBundleTest.kt b/core/ktx/src/androidTest/java/androidx/core/os/PersistableBundleTest.kt
new file mode 100644
index 0000000..8683155
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/os/PersistableBundleTest.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+
+package androidx.core.os
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SdkSuppress
+import android.view.View
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 21)
+class PersistableBundleTest {
+    @Test fun persistableBundleOfValid() {
+        val bundle = persistableBundleOf(
+            "null" to null,
+
+            "double" to 1.0,
+            "int" to 1,
+            "long" to 1L,
+
+            "string" to "hey",
+
+            "doubleArray" to doubleArrayOf(),
+            "intArray" to intArrayOf(),
+            "longArray" to longArrayOf(),
+
+            "stringArray" to arrayOf("hey")
+        )
+
+        assertEquals(9, bundle.size())
+
+        assertNull(bundle["null"])
+
+        assertEquals(1.0, bundle["double"])
+        assertEquals(1, bundle["int"])
+        assertEquals(1L, bundle["long"])
+
+        assertEquals("hey", bundle["string"])
+
+        assertArrayEquals(doubleArrayOf(), bundle["doubleArray"] as DoubleArray, 0.0)
+        assertArrayEquals(intArrayOf(), bundle["intArray"] as IntArray)
+        assertArrayEquals(longArrayOf(), bundle["longArray"] as LongArray)
+
+        assertThat(bundle["stringArray"] as Array<*>).asList().containsExactly("hey")
+    }
+
+    @SdkSuppress(minSdkVersion = 22)
+    @Test fun persistableBundleOfValidApi22() {
+        val bundle = persistableBundleOf(
+            "boolean" to true,
+            "booleanArray" to booleanArrayOf()
+        )
+
+        assertEquals(true, bundle["boolean"])
+        assertArrayEquals(booleanArrayOf(), bundle["booleanArray"] as BooleanArray)
+    }
+
+    @Test fun persistableBundleOfInvalid() {
+        assertThrows<IllegalArgumentException> {
+            persistableBundleOf("nope" to View(InstrumentationRegistry.getContext()))
+        }.hasMessageThat().isEqualTo("Illegal value type android.view.View for key \"nope\"")
+
+        assertThrows<IllegalArgumentException> {
+            persistableBundleOf("nopes" to arrayOf(View(InstrumentationRegistry.getContext())))
+        }.hasMessageThat().isEqualTo("Illegal value array type android.view.View for key \"nopes\"")
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/preference/PreferenceGroupTest.kt b/core/ktx/src/androidTest/java/androidx/core/preference/PreferenceGroupTest.kt
new file mode 100644
index 0000000..120718e
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/preference/PreferenceGroupTest.kt
@@ -0,0 +1,185 @@
+/*
+ * 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.
+ */
+
+package androidx.core.preference
+
+import android.preference.Preference
+import android.preference.PreferenceFragment
+import android.preference.PreferenceGroup
+import android.support.test.InstrumentationRegistry
+import android.support.test.rule.ActivityTestRule
+import androidx.core.TestPreferenceActivity
+import androidx.testutils.assertThrows
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class PreferenceGroupTest {
+
+    @JvmField
+    @Rule
+    val rule = ActivityTestRule(TestPreferenceActivity::class.java)
+    private val context = InstrumentationRegistry.getContext()
+    private lateinit var preferenceGroup: PreferenceGroup
+
+    @Before fun setup() {
+        preferenceGroup = (rule
+            .activity
+            .fragmentManager
+            .findFragmentByTag(TestPreferenceActivity.TAG) as PreferenceFragment).preferenceScreen
+    }
+
+    @Test fun get() {
+        val key = "key"
+        val preference = Preference(context)
+        preference.key = key
+        preferenceGroup.addPreference(preference)
+        assertSame(preference, preferenceGroup[key])
+        assertSame(preference, preferenceGroup[0])
+    }
+
+    @Test fun contains() {
+        val preference = Preference(context)
+        assertFalse(preference in preferenceGroup)
+        assertTrue(preference !in preferenceGroup)
+        preferenceGroup.addPreference(preference)
+        assertFalse(preference !in preferenceGroup)
+        assertTrue(preference in preferenceGroup)
+        preferenceGroup.removePreference(preference)
+        assertFalse(preference in preferenceGroup)
+        assertTrue(preference !in preferenceGroup)
+    }
+
+    @Test fun plusAssign() {
+        assertEquals(0, preferenceGroup.preferenceCount)
+
+        val preference1 = Preference(context)
+        preferenceGroup += preference1
+        assertEquals(1, preferenceGroup.preferenceCount)
+        assertSame(preference1, preferenceGroup.getPreference(0))
+
+        val preference2 = Preference(context)
+        preferenceGroup += preference2
+        assertEquals(2, preferenceGroup.preferenceCount)
+        assertSame(preference2, preferenceGroup.getPreference(1))
+    }
+
+    @Test fun minusAssign() {
+        val preference1 = Preference(context)
+        preferenceGroup.addPreference(preference1)
+        val preference2 = Preference(context)
+        preferenceGroup.addPreference(preference2)
+
+        assertEquals(2, preferenceGroup.preferenceCount)
+
+        preferenceGroup -= preference2
+        assertEquals(1, preferenceGroup.preferenceCount)
+        assertSame(preference1, preferenceGroup.getPreference(0))
+
+        preferenceGroup -= preference1
+        assertEquals(0, preferenceGroup.preferenceCount)
+    }
+
+    @Test fun size() {
+        assertEquals(0, preferenceGroup.size)
+
+        val preference = Preference(context)
+        preferenceGroup.addPreference(preference)
+        assertEquals(1, preferenceGroup.size)
+
+        preferenceGroup.removePreference(preference)
+        assertEquals(0, preferenceGroup.size)
+    }
+
+    @Test fun isEmpty() {
+        assertTrue(preferenceGroup.isEmpty())
+        preferenceGroup.addPreference(Preference(context))
+        assertFalse(preferenceGroup.isEmpty())
+    }
+
+    @Test fun isNotEmpty() {
+        assertFalse(preferenceGroup.isNotEmpty())
+        preferenceGroup.addPreference(Preference(context))
+        assertTrue(preferenceGroup.isNotEmpty())
+    }
+
+    @Test fun forEach() {
+        preferenceGroup.forEach {
+            fail("Empty preference group should not invoke lambda")
+        }
+
+        val preference1 = Preference(context).apply { key = "ASD" }
+        preferenceGroup.addPreference(preference1)
+        val preference2 = Preference(context)
+        preferenceGroup.addPreference(preference2)
+
+        val preferences = mutableListOf<Preference>()
+        preferenceGroup.forEach {
+            preferences += it
+        }
+        assertThat(preferences).containsExactly(preference1, preference2)
+    }
+
+    @Test fun forEachIndexed() {
+        preferenceGroup.forEach {
+            fail("Empty preference group should not invoke lambda")
+        }
+
+        val preference1 = Preference(context)
+        preferenceGroup.addPreference(preference1)
+        val preference2 = Preference(context)
+        preferenceGroup.addPreference(preference2)
+
+        val preferences = mutableListOf<Preference>()
+        preferenceGroup.forEachIndexed { index, preference ->
+            assertEquals(index, preferences.size)
+            preferences += preference
+        }
+        assertThat(preferences).containsExactly(preference1, preference2)
+    }
+
+    @Test fun iterator() {
+        val preference1 = Preference(context)
+        preferenceGroup.addPreference(preference1)
+        val preference2 = Preference(context)
+        preferenceGroup.addPreference(preference2)
+
+        val iterator = preferenceGroup.iterator()
+        assertTrue(iterator.hasNext())
+        assertSame(preference1, iterator.next())
+        assertTrue(iterator.hasNext())
+        assertSame(preference2, iterator.next())
+        assertFalse(iterator.hasNext())
+        assertThrows<IndexOutOfBoundsException> {
+            iterator.next()
+        }
+    }
+
+    @Test fun children() {
+        val preferences = listOf(Preference(context), Preference(context), Preference(context))
+        preferences.forEach { preferenceGroup.addPreference(it) }
+
+        preferenceGroup.children.forEachIndexed { index, child ->
+            assertSame(preferences[index], child)
+        }
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/text/CharSequenceTest.kt b/core/ktx/src/androidTest/java/androidx/core/text/CharSequenceTest.kt
new file mode 100644
index 0000000..0ce0a3c
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/text/CharSequenceTest.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+package androidx.core.text
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class CharSequenceTest {
+    @Test fun isDigitsOnly() {
+        assertTrue("012345".isDigitsOnly())
+        assertFalse("0123 abc".isDigitsOnly())
+    }
+
+    @Test fun trimmedLength() {
+        assertEquals(6, "string    ".trimmedLength())
+        assertEquals(6, "    string".trimmedLength())
+        assertEquals(6, "string".trimmedLength())
+        assertEquals(0, "".trimmedLength())
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/text/HtmlTest.kt b/core/ktx/src/androidTest/java/androidx/core/text/HtmlTest.kt
new file mode 100644
index 0000000..0fef5ed
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/text/HtmlTest.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+package androidx.core.text
+
+import android.text.Html.FROM_HTML_MODE_COMPACT
+import android.text.Html.ImageGetter
+import android.text.Html.TagHandler
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class HtmlTest {
+    private val imageGetter = ImageGetter { null }
+    private val tagHandler = TagHandler { _, _, _, _ -> }
+
+    @Test fun parseAsHtml() {
+        val parsed = "<b>Hi</b> © > <".parseAsHtml().toString()
+        assertEquals("Hi \u00a9 > <", parsed)
+    }
+
+    @Test fun parseAsHtmlFlags() {
+        val parsed = "<b>Hi</b> © > <".parseAsHtml(FROM_HTML_MODE_COMPACT).toString()
+        assertEquals("Hi \u00a9 > <", parsed)
+    }
+
+    @Test fun parseAsHtmlImageGetterTagHandler() {
+        val parsed = "<b>Hi</b> © > <"
+            .parseAsHtml(FROM_HTML_MODE_COMPACT, imageGetter, tagHandler)
+            .toString()
+        assertEquals("Hi \u00a9 > <", parsed)
+    }
+
+    @Test fun parseAsHtmlFlagsImageGetterTagHandler() {
+        val parsed = "<b>Hi</b> © > <"
+            .parseAsHtml(imageGetter = imageGetter, tagHandler = tagHandler)
+            .toString()
+        assertEquals("Hi \u00a9 > <", parsed)
+    }
+
+    @Test fun convertToHtml() {
+        val html = buildSpannedString {
+            bold {
+                append("Hi")
+            }
+        }.toHtml()
+        assertTrue(html, html.contains("<b>Hi</b>"))
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/text/SpannableStringBuilderTest.kt b/core/ktx/src/androidTest/java/androidx/core/text/SpannableStringBuilderTest.kt
new file mode 100644
index 0000000..c66dd1f
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/text/SpannableStringBuilderTest.kt
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.text
+
+import android.graphics.Color.RED
+import android.graphics.Color.YELLOW
+import android.graphics.Typeface.BOLD
+import android.graphics.Typeface.ITALIC
+import android.text.SpannedString
+import android.text.style.BackgroundColorSpan
+import android.text.style.BulletSpan
+import android.text.style.ForegroundColorSpan
+import android.text.style.RelativeSizeSpan
+import android.text.style.StrikethroughSpan
+import android.text.style.StyleSpan
+import android.text.style.SubscriptSpan
+import android.text.style.SuperscriptSpan
+import android.text.style.UnderlineSpan
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Test
+
+class SpannableStringBuilderTest {
+    @Test fun builder() {
+        val result: SpannedString = buildSpannedString {
+            append("Hello,")
+            append(" World")
+        }
+        assertEquals("Hello, World", result.toString())
+    }
+
+    @Test fun builderInSpan() {
+        val bold = StyleSpan(BOLD)
+        val result: SpannedString = buildSpannedString {
+            append("Hello, ")
+            inSpans(bold) {
+                append("World")
+            }
+        }
+        assertEquals("Hello, World", result.toString())
+
+        val spans = result.getSpans<Any>()
+        assertEquals(1, spans.size)
+
+        val boldSpan = spans.filterIsInstance<StyleSpan>().single()
+        assertSame(bold, boldSpan)
+        assertEquals(7, result.getSpanStart(bold))
+        assertEquals(12, result.getSpanEnd(bold))
+    }
+
+    @Test fun builderBold() {
+        val result: SpannedString = buildSpannedString {
+            append("Hello, ")
+            bold {
+                append("World")
+            }
+        }
+        assertEquals("Hello, World", result.toString())
+
+        val spans = result.getSpans<Any>()
+        assertEquals(1, spans.size)
+
+        val bold = spans.filterIsInstance<StyleSpan>().single()
+        assertEquals(BOLD, bold.style)
+        assertEquals(7, result.getSpanStart(bold))
+        assertEquals(12, result.getSpanEnd(bold))
+    }
+
+    @Test fun builderItalic() {
+        val result: SpannedString = buildSpannedString {
+            append("Hello, ")
+            italic {
+                append("World")
+            }
+        }
+        assertEquals("Hello, World", result.toString())
+
+        val spans = result.getSpans<Any>()
+        assertEquals(1, spans.size)
+
+        val italic = spans.filterIsInstance<StyleSpan>().single()
+        assertEquals(ITALIC, italic.style)
+        assertEquals(7, result.getSpanStart(italic))
+        assertEquals(12, result.getSpanEnd(italic))
+    }
+
+    @Test fun builderUnderline() {
+        val result: SpannedString = buildSpannedString {
+            append("Hello, ")
+            underline {
+                append("World")
+            }
+        }
+        assertEquals("Hello, World", result.toString())
+
+        val spans = result.getSpans<Any>()
+        assertEquals(1, spans.size)
+
+        val underline = spans.filterIsInstance<UnderlineSpan>().single()
+        assertEquals(7, result.getSpanStart(underline))
+        assertEquals(12, result.getSpanEnd(underline))
+    }
+
+    @Test fun builderColor() {
+        val result: SpannedString = buildSpannedString {
+            append("Hello, ")
+            color(RED) {
+                append("World")
+            }
+        }
+        assertEquals("Hello, World", result.toString())
+
+        val spans = result.getSpans<Any>()
+        assertEquals(1, spans.size)
+
+        val color = spans.filterIsInstance<ForegroundColorSpan>().single()
+        assertEquals(RED, color.foregroundColor)
+        assertEquals(7, result.getSpanStart(color))
+        assertEquals(12, result.getSpanEnd(color))
+    }
+
+    @Test fun builderBackgroundColor() {
+        val result: SpannedString = buildSpannedString {
+            append("Hello, ")
+            backgroundColor(RED) {
+                append("World")
+            }
+        }
+        assertEquals("Hello, World", result.toString())
+
+        val spans = result.getSpans<Any>()
+        assertEquals(1, spans.size)
+
+        val color = spans.filterIsInstance<BackgroundColorSpan>().single()
+        assertEquals(RED, color.backgroundColor)
+        assertEquals(7, result.getSpanStart(color))
+        assertEquals(12, result.getSpanEnd(color))
+    }
+
+    @Test fun builderStrikeThrough() {
+        val result: SpannedString = buildSpannedString {
+            append("Hello, ")
+            strikeThrough {
+                append("World")
+            }
+        }
+        assertEquals("Hello, World", result.toString())
+
+        val spans = result.getSpans<Any>()
+        assertEquals(1, spans.size)
+
+        val strikeThrough = spans.filterIsInstance<StrikethroughSpan>().single()
+        assertEquals(7, result.getSpanStart(strikeThrough))
+        assertEquals(12, result.getSpanEnd(strikeThrough))
+    }
+
+    @Test fun builderScale() {
+        val result: SpannedString = buildSpannedString {
+            append("Hello, ")
+            scale(2f) {
+                append("World")
+            }
+        }
+        assertEquals("Hello, World", result.toString())
+
+        val spans = result.getSpans<Any>()
+        assertEquals(1, spans.size)
+
+        val scale = spans.filterIsInstance<RelativeSizeSpan>().single()
+        assertEquals(2f, scale.sizeChange)
+        assertEquals(7, result.getSpanStart(scale))
+        assertEquals(12, result.getSpanEnd(scale))
+    }
+
+    @Test fun builderSuperscript() {
+        val result: SpannedString = buildSpannedString {
+            append("Hello, ")
+            superscript {
+                append("World")
+            }
+        }
+        assertEquals("Hello, World", result.toString())
+
+        val spans = result.getSpans<Any>()
+        assertEquals(1, spans.size)
+
+        val superscript = spans.filterIsInstance<SuperscriptSpan>().single()
+        assertEquals(7, result.getSpanStart(superscript))
+        assertEquals(12, result.getSpanEnd(superscript))
+    }
+
+    @Test fun builderSubscript() {
+        val result: SpannedString = buildSpannedString {
+            append("Hello, ")
+            subscript {
+                append("World")
+            }
+        }
+        assertEquals("Hello, World", result.toString())
+
+        val spans = result.getSpans<Any>()
+        assertEquals(1, spans.size)
+
+        val subscript = spans.filterIsInstance<SubscriptSpan>().single()
+        assertEquals(7, result.getSpanStart(subscript))
+        assertEquals(12, result.getSpanEnd(subscript))
+    }
+
+    @Test fun nested() {
+        val result: SpannedString = buildSpannedString {
+            color(RED) {
+                append('H')
+                subscript {
+                    append('e')
+                }
+                append('l')
+                superscript {
+                    append('l')
+                }
+                append('o')
+            }
+            append(", ")
+            backgroundColor(YELLOW) {
+                append('W')
+                underline {
+                    append('o')
+                    bold {
+                        append('r')
+                    }
+                    append('l')
+                }
+                append('d')
+            }
+        }
+        assertEquals("Hello, World", result.toString())
+
+        val spans = result.getSpans<Any>()
+        assertEquals(6, spans.size)
+
+        val color = spans.filterIsInstance<ForegroundColorSpan>().single()
+        assertEquals(RED, color.foregroundColor)
+        assertEquals(0, result.getSpanStart(color))
+        assertEquals(5, result.getSpanEnd(color))
+
+        val subscript = spans.filterIsInstance<SubscriptSpan>().single()
+        assertEquals(1, result.getSpanStart(subscript))
+        assertEquals(2, result.getSpanEnd(subscript))
+
+        val superscript = spans.filterIsInstance<SuperscriptSpan>().single()
+        assertEquals(3, result.getSpanStart(superscript))
+        assertEquals(4, result.getSpanEnd(superscript))
+
+        val backgroundColor = spans.filterIsInstance<BackgroundColorSpan>().single()
+        assertEquals(YELLOW, backgroundColor.backgroundColor)
+        assertEquals(7, result.getSpanStart(backgroundColor))
+        assertEquals(12, result.getSpanEnd(backgroundColor))
+
+        val underline = spans.filterIsInstance<UnderlineSpan>().single()
+        assertEquals(8, result.getSpanStart(underline))
+        assertEquals(11, result.getSpanEnd(underline))
+
+        val bold = spans.filterIsInstance<StyleSpan>().single()
+        assertEquals(BOLD, bold.style)
+        assertEquals(9, result.getSpanStart(bold))
+        assertEquals(10, result.getSpanEnd(bold))
+    }
+
+    @Test fun multipleSpans() {
+        val result: SpannedString = buildSpannedString {
+            append("Hello, ")
+            inSpans(BulletSpan(), UnderlineSpan()) {
+                append("World")
+            }
+        }
+        assertEquals("Hello, World", result.toString())
+
+        val spans = result.getSpans<Any>()
+        assertEquals(2, spans.size)
+
+        val bullet = spans.filterIsInstance<BulletSpan>().single()
+        assertEquals(7, result.getSpanStart(bullet))
+        assertEquals(12, result.getSpanEnd(bullet))
+        val underline = spans.filterIsInstance<UnderlineSpan>().single()
+        assertEquals(7, result.getSpanStart(underline))
+        assertEquals(12, result.getSpanEnd(underline))
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/text/SpannableStringTest.kt b/core/ktx/src/androidTest/java/androidx/core/text/SpannableStringTest.kt
new file mode 100644
index 0000000..5fe3c5d
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/text/SpannableStringTest.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+
+package androidx.core.text
+
+import android.graphics.Typeface.BOLD
+import android.text.SpannableString
+import android.text.style.StyleSpan
+import android.text.style.UnderlineSpan
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class SpannableStringTest {
+
+    @Test fun toSpannableString() = assertTrue("Hello, World".toSpannable() is SpannableString)
+
+    @Test fun plusAssign() {
+        val s = "Hello, World".toSpannable()
+
+        val bold = StyleSpan(BOLD)
+        s += bold
+        assertEquals(0, s.getSpanStart(bold))
+        assertEquals(s.length, s.getSpanEnd(bold))
+    }
+
+    @Test fun minusAssign() {
+        val s = "Hello, World".toSpannable()
+        val bold = StyleSpan(BOLD)
+        s += bold
+        assertTrue(s.getSpans<Any>().isNotEmpty())
+        s -= bold
+        assertTrue(s.getSpans<Any>().isEmpty())
+    }
+
+    @Test fun clearSpans() {
+        val s = "Hello, World".toSpannable()
+        s += StyleSpan(BOLD)
+        s += UnderlineSpan()
+        assertTrue(s.getSpans<Any>().isNotEmpty())
+        s.clearSpans()
+        assertTrue(s.getSpans<Any>().isEmpty())
+    }
+
+    @Test fun setIndices() {
+        val s = "Hello, World".toSpannable()
+        s[0, 5] = StyleSpan(BOLD)
+        s[7, 12] = UnderlineSpan()
+
+        val spans = s.getSpans<Any>()
+
+        val bold = spans.filterIsInstance<StyleSpan>().single()
+        assertEquals(0, s.getSpanStart(bold))
+        assertEquals(5, s.getSpanEnd(bold))
+
+        val underline = spans.filterIsInstance<UnderlineSpan>().single()
+        assertEquals(7, s.getSpanStart(underline))
+        assertEquals(12, s.getSpanEnd(underline))
+    }
+
+    @Test fun setRange() {
+        val s = "Hello, World".toSpannable()
+        s[0..5] = StyleSpan(BOLD)
+        s[7..12] = UnderlineSpan()
+
+        val spans = s.getSpans<Any>()
+
+        val bold = spans.filterIsInstance<StyleSpan>().single()
+        assertEquals(0, s.getSpanStart(bold))
+        assertEquals(5, s.getSpanEnd(bold))
+
+        val underline = spans.filterIsInstance<UnderlineSpan>().single()
+        assertEquals(7, s.getSpanStart(underline))
+        assertEquals(12, s.getSpanEnd(underline))
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/text/SpannedStringTest.kt b/core/ktx/src/androidTest/java/androidx/core/text/SpannedStringTest.kt
new file mode 100644
index 0000000..cb8930a
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/text/SpannedStringTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+package androidx.core.text
+
+import android.graphics.Typeface.BOLD
+import android.text.Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+import android.text.SpannedString
+import android.text.style.StyleSpan
+import android.text.style.UnderlineSpan
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class SpannedStringTest {
+
+    @Test fun toSpanned() = assertTrue("Hello, World".toSpanned() is SpannedString)
+
+    @Test fun getSpans() {
+        val bold = StyleSpan(BOLD)
+        val underline = UnderlineSpan()
+
+        val s = "Hello, World".toSpannable()
+        s.setSpan(bold, 0, 5, SPAN_INCLUSIVE_EXCLUSIVE)
+        s.setSpan(underline, 7, 12, SPAN_INCLUSIVE_EXCLUSIVE)
+
+        assertSame(bold, s.getSpans<StyleSpan>().single())
+        assertSame(underline, s.getSpans<UnderlineSpan>().single())
+        assertEquals(s.getSpans<Any>().size, 2)
+
+        assertSame(bold, s.getSpans<Any>(0, 6).single())
+        assertSame(underline, s.getSpans<Any>(6, 12).single())
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/text/StringTest.kt b/core/ktx/src/androidTest/java/androidx/core/text/StringTest.kt
new file mode 100644
index 0000000..2812799
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/text/StringTest.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+package androidx.core.text
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class StringTest {
+    @Test fun htmlEncode() {
+        assertEquals("&lt;&gt; &amp; &quot; &#39;", """<> & " '""".htmlEncode())
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/transition/TransitionTest.kt b/core/ktx/src/androidTest/java/androidx/core/transition/TransitionTest.kt
new file mode 100644
index 0000000..c3a2d22
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/transition/TransitionTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.transition
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SdkSuppress
+import android.support.test.rule.ActivityTestRule
+import android.transition.Fade
+import android.transition.Transition
+import android.transition.TransitionManager
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.core.TestActivity
+import androidx.core.ktx.test.R
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+
+@SdkSuppress(minSdkVersion = 19)
+class TransitionTest {
+    @JvmField @Rule val rule = ActivityTestRule<TestActivity>(TestActivity::class.java)
+
+    private lateinit var transition: Transition
+
+    @Before fun setup() {
+        transition = Fade().setDuration(50)
+    }
+
+    @Test fun testDoOnStart() {
+        val called = AtomicBoolean()
+        transition.doOnStart {
+            called.set(true)
+        }
+
+        startTransition(transition)
+        assertTrue(called.get())
+    }
+
+    @Test fun testDoOnEnd() {
+        val called = AtomicBoolean()
+        transition.doOnEnd {
+            called.set(true)
+        }
+
+        val latch = CountDownLatch(1)
+        transition.addListener(object : Transition.TransitionListener {
+            override fun onTransitionEnd(transition: Transition?) {
+                latch.countDown()
+            }
+
+            override fun onTransitionResume(transition: Transition?) = Unit
+            override fun onTransitionPause(transition: Transition?) = Unit
+            override fun onTransitionCancel(transition: Transition?) = Unit
+            override fun onTransitionStart(transition: Transition?) = Unit
+        })
+
+        startTransition(transition)
+
+        assertTrue(latch.await(1, TimeUnit.SECONDS))
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+
+        assertTrue(called.get())
+    }
+
+    private fun startTransition(t: Transition) {
+        rule.runOnUiThread {
+            val sceneRoot = rule.activity.findViewById<ViewGroup>(R.id.root)
+            val view = rule.activity.findViewById<ImageView>(R.id.image_view)
+
+            TransitionManager.beginDelayedTransition(sceneRoot, t)
+
+            view.visibility = View.INVISIBLE
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/ArrayMapTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/ArrayMapTest.kt
new file mode 100644
index 0000000..5eddef2
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/ArrayMapTest.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.support.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 19)
+class ArrayMapTest {
+    @Test fun empty() {
+        val map = arrayMapOf<String, String>()
+        assertEquals(0, map.size)
+    }
+
+    @Test fun nonEmpty() {
+        val map = arrayMapOf("foo" to "bar", "bar" to "baz")
+        assertThat(map).containsExactly("foo", "bar", "bar", "baz")
+    }
+
+    @Test fun duplicateKeyKeepsLast() {
+        val map = arrayMapOf("foo" to "bar", "foo" to "baz")
+        assertThat(map).containsExactly("foo", "baz")
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/ArraySetTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/ArraySetTest.kt
new file mode 100644
index 0000000..934f18c
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/ArraySetTest.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.support.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 23)
+class ArraySetTest {
+    @Test fun empty() {
+        val set = arraySetOf<String>()
+        assertEquals(0, set.size)
+    }
+
+    @Test fun nonEmpty() {
+        val set = arraySetOf("foo", "bar", "baz")
+        assertThat(set).containsExactly("foo", "bar", "baz")
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/AtomicFileTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/AtomicFileTest.kt
new file mode 100644
index 0000000..59fc8d9
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/AtomicFileTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.support.test.filters.SdkSuppress
+import android.util.AtomicFile
+import androidx.testutils.assertThrows
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import java.io.IOException
+
+@SdkSuppress(minSdkVersion = 17)
+class AtomicFileTest {
+    @get:Rule val temporaryFolder = TemporaryFolder()
+
+    private lateinit var file: AtomicFile
+
+    @Before fun before() {
+        file = AtomicFile(temporaryFolder.newFile())
+    }
+
+    @Test fun tryWriteSuccess() {
+        file.tryWrite {
+            it.write(byteArrayOf(0, 1, 2))
+        }
+        val bytes = file.openRead().use { it.readBytes() }
+        assertArrayEquals(byteArrayOf(0, 1, 2), bytes)
+    }
+
+    @Test fun tryWriteFail() {
+        val os = file.startWrite()
+        os.write(byteArrayOf(0, 1, 2))
+        file.finishWrite(os)
+
+        val failure = IOException("Broken!")
+        assertThrows<IOException> {
+            file.tryWrite {
+                it.write(byteArrayOf(3, 4, 5))
+                throw failure
+            }
+        }.isSameAs(failure)
+
+        val bytes = file.openRead().use { it.readBytes() }
+        assertArrayEquals(byteArrayOf(0, 1, 2), bytes)
+    }
+
+    @Test fun writeBytes() {
+        file.writeBytes(byteArrayOf(0, 1, 2))
+
+        val bytes = file.openRead().use { it.readBytes() }
+        assertArrayEquals(byteArrayOf(0, 1, 2), bytes)
+    }
+
+    @Test fun writeText() {
+        file.writeText("Hey")
+
+        val bytes = file.openRead().use { it.readBytes() }
+        assertArrayEquals(byteArrayOf(72, 101, 121), bytes)
+    }
+
+    @Test fun writeTextCharset() {
+        file.writeText("Hey", charset = Charsets.UTF_16LE)
+
+        val bytes = file.openRead().use { it.readBytes() }
+        assertArrayEquals(byteArrayOf(72, 0, 101, 0, 121, 0), bytes)
+    }
+
+    @Test fun readBytes() {
+        val os = file.startWrite()
+        os.write(byteArrayOf(0, 1, 2))
+        file.finishWrite(os)
+
+        assertArrayEquals(byteArrayOf(0, 1, 2), file.readBytes())
+    }
+
+    @Test fun readText() {
+        val os = file.startWrite()
+        os.write(byteArrayOf(72, 101, 121))
+        file.finishWrite(os)
+
+        assertEquals("Hey", file.readText())
+    }
+
+    @Test fun readTextCharset() {
+        val os = file.startWrite()
+        os.write(byteArrayOf(72, 0, 101, 0, 121, 0))
+        file.finishWrite(os)
+
+        assertEquals("Hey", file.readText(charset = Charsets.UTF_16LE))
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/HalfTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/HalfTest.kt
new file mode 100644
index 0000000..b79e064
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/HalfTest.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.support.test.filters.SdkSuppress
+import android.util.Half
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 26)
+class HalfTest {
+    @Test fun shortToHalf() = assertEquals(Half(1.toShort()), 1.toShort().toHalf())
+
+    @Test fun floatToHalf() = assertEquals(Half(1f), 1f.toHalf())
+
+    @Test fun doubleToHalf() = assertEquals(Half(1.0), 1.0.toHalf())
+
+    @Test fun stringToHalf() = assertEquals(Half("1"), "1".toHalf())
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/LocaleTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/LocaleTest.kt
new file mode 100644
index 0000000..0a345f0
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/LocaleTest.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+package androidx.core.util
+
+import android.view.View
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.util.Locale
+
+class LocaleTest {
+    @Test fun layoutDirectionWithLTR() {
+        val ltrLocale = Locale.Builder().setLanguage("en").build()
+        assertEquals(View.LAYOUT_DIRECTION_LTR, ltrLocale.layoutDirection)
+    }
+
+    @Test fun layoutDirectionWithRTL() {
+        val rtlLocale = Locale.Builder().setLanguage("ar").build()
+        assertEquals(View.LAYOUT_DIRECTION_RTL, rtlLocale.layoutDirection)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/LongSparseArrayTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/LongSparseArrayTest.kt
new file mode 100644
index 0000000..c2289e6
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/LongSparseArrayTest.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.support.test.filters.SdkSuppress
+import android.util.LongSparseArray
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 16)
+class LongSparseArrayTest {
+    @Test fun sizeProperty() {
+        val array = LongSparseArray<String>()
+        assertEquals(0, array.size)
+        array.put(1L, "one")
+        assertEquals(1, array.size)
+    }
+
+    @Test fun containsOperator() {
+        val array = LongSparseArray<String>()
+        assertFalse(1L in array)
+        array.put(1L, "one")
+        assertTrue(1L in array)
+    }
+
+    @Test fun containsOperatorWithValue() {
+        val array = LongSparseArray<String>()
+
+        array.put(1L, "one")
+        assertFalse(2L in array)
+
+        array.put(2L, "two")
+        assertTrue(2L in array)
+    }
+
+    @Test fun setOperator() {
+        val array = LongSparseArray<String>()
+        array[1L] = "one"
+        assertEquals("one", array.get(1L))
+    }
+
+    @Test fun plusOperator() {
+        val first = LongSparseArray<String>().apply { put(1L, "one") }
+        val second = LongSparseArray<String>().apply { put(2L, "two") }
+        val combined = first + second
+        assertEquals(2, combined.size())
+        assertEquals(1L, combined.keyAt(0))
+        assertEquals("one", combined.valueAt(0))
+        assertEquals(2L, combined.keyAt(1))
+        assertEquals("two", combined.valueAt(1))
+    }
+
+    @Test fun containsKey() {
+        val array = LongSparseArray<String>()
+        assertFalse(array.containsKey(1L))
+        array.put(1L, "one")
+        assertTrue(array.containsKey(1L))
+    }
+
+    @Test fun containsKeyWithValue() {
+        val array = LongSparseArray<String>()
+
+        array.put(1L, "one")
+        assertFalse(array.containsKey(2L))
+
+        array.put(2L, "one")
+        assertTrue(array.containsKey(2L))
+    }
+
+    @Test fun containsValue() {
+        val array = LongSparseArray<String>()
+        assertFalse(array.containsValue("one"))
+        array.put(1L, "one")
+        assertTrue(array.containsValue("one"))
+    }
+
+    @Test fun getOrDefault() {
+        val array = LongSparseArray<Any>()
+        val default = Any()
+        assertSame(default, array.getOrDefault(1L, default))
+        array.put(1L, "one")
+        assertEquals("one", array.getOrDefault(1L, default))
+    }
+
+    @Test fun getOrElse() {
+        val array = LongSparseArray<Any>()
+        val default = Any()
+        assertSame(default, array.getOrElse(1L) { default })
+        array.put(1L, "one")
+        assertEquals("one", array.getOrElse(1L) { fail() })
+    }
+
+    @Test fun isEmpty() {
+        val array = LongSparseArray<String>()
+        assertTrue(array.isEmpty())
+        array.put(1L, "one")
+        assertFalse(array.isEmpty())
+    }
+
+    @Test fun isNotEmpty() {
+        val array = LongSparseArray<String>()
+        assertFalse(array.isNotEmpty())
+        array.put(1L, "one")
+        assertTrue(array.isNotEmpty())
+    }
+
+    @Test fun removeValue() {
+        val array = LongSparseArray<String>()
+        array.put(1L, "one")
+        assertFalse(array.remove(0L, "one"))
+        assertEquals(1, array.size())
+        assertFalse(array.remove(1L, "two"))
+        assertEquals(1, array.size())
+        assertTrue(array.remove(1L, "one"))
+        assertEquals(0, array.size())
+    }
+
+    @Test fun putAll() {
+        val dest = LongSparseArray<String>()
+        val source = LongSparseArray<String>()
+        source.put(1L, "one")
+
+        assertEquals(0, dest.size())
+        dest.putAll(source)
+        assertEquals(1, dest.size())
+    }
+
+    @Test fun forEach() {
+        val array = LongSparseArray<String>()
+        array.forEach { _, _ -> fail() }
+
+        array.put(1L, "one")
+        array.put(2L, "two")
+        array.put(6L, "six")
+
+        val keys = mutableListOf<Long>()
+        val values = mutableListOf<String>()
+        array.forEach { key, value ->
+            keys.add(key)
+            values.add(value)
+        }
+        assertThat(keys).containsExactly(1L, 2L, 6L)
+        assertThat(values).containsExactly("one", "two", "six")
+    }
+
+    @Test fun keyIterator() {
+        val array = LongSparseArray<String>()
+        assertFalse(array.keyIterator().hasNext())
+
+        array.put(1L, "one")
+        array.put(2L, "two")
+        array.put(6L, "six")
+
+        val iterator = array.keyIterator()
+        assertTrue(iterator.hasNext())
+        assertEquals(1L, iterator.nextLong())
+        assertTrue(iterator.hasNext())
+        assertEquals(2L, iterator.nextLong())
+        assertTrue(iterator.hasNext())
+        assertEquals(6L, iterator.nextLong())
+        assertFalse(iterator.hasNext())
+    }
+
+    @Test fun valueIterator() {
+        val array = LongSparseArray<String>()
+        assertFalse(array.valueIterator().hasNext())
+
+        array.put(1L, "one")
+        array.put(2L, "two")
+        array.put(6L, "six")
+
+        val iterator = array.valueIterator()
+        assertTrue(iterator.hasNext())
+        assertEquals("one", iterator.next())
+        assertTrue(iterator.hasNext())
+        assertEquals("two", iterator.next())
+        assertTrue(iterator.hasNext())
+        assertEquals("six", iterator.next())
+        assertFalse(iterator.hasNext())
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/LruCacheTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/LruCacheTest.kt
new file mode 100644
index 0000000..198e13d
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/LruCacheTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+package androidx.core.util
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class LruCacheTest {
+    private data class TestData(val x: String = "bla")
+
+    @Test fun size() {
+        val cache = lruCache<String, TestData>(200, { k, (x) -> k.length * x.length })
+        cache.put("long", TestData())
+        assertEquals(cache.size(), 12)
+    }
+
+    @Test fun create() {
+        val cache = lruCache<String, TestData>(200, create = { key -> TestData("$key foo") })
+        assertEquals(cache.get("kung"), TestData("kung foo"))
+    }
+
+    @Test fun onEntryRemoved() {
+        var wasCalled = false
+
+        val cache = lruCache<String, TestData>(200, onEntryRemoved = { _, _, _, _ ->
+            wasCalled = true
+        })
+        val initial = TestData()
+        cache.put("a", initial)
+        cache.remove("a")
+        assertTrue(wasCalled)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/PairTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/PairTest.kt
new file mode 100644
index 0000000..e3fac39
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/PairTest.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.util.Pair
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Test
+
+class PairTest {
+    @Test fun destructuringNonNull() {
+        val pair = Pair("one", "two")
+        val (first: String, second: String) = pair
+        assertSame(pair.first, first)
+        assertSame(pair.second, second)
+    }
+
+    @Test fun destructuringNullable() {
+        val pair = Pair("one", "two")
+        val (first: String?, second: String?) = pair
+        assertSame(pair.first, first)
+        assertSame(pair.second, second)
+    }
+
+    @Test fun toKotlin() {
+        val android = Pair("one", "two")
+        val kotlin = android.toKotlinPair()
+        assertEquals(android.first to android.second, kotlin)
+    }
+
+    @Test fun toAndroid() {
+        val kotlin = kotlin.Pair("one", "two")
+        val android = kotlin.toAndroidPair()
+        assertEquals(Pair(kotlin.first, kotlin.second), android)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/RangeTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/RangeTest.kt
new file mode 100644
index 0000000..ec5d238
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/RangeTest.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+
+package androidx.core.util
+
+import android.support.test.filters.SdkSuppress
+import android.util.Range
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 21)
+class RangeTest {
+    @Test fun infixFactory() {
+        val range: Range<String> = "a" rangeTo "c"
+        assertEquals("a", range.lower)
+        assertEquals("c", range.upper)
+    }
+
+    @Test fun extendValue() {
+        val range = ("a" rangeTo "c") + "e"
+        assertEquals("a", range.lower)
+        assertEquals("e", range.upper)
+    }
+
+    @Test fun extendRange() {
+        val range = ("a" rangeTo "c") + ("e" rangeTo "g")
+        assertEquals("a", range.lower)
+        assertEquals("g", range.upper)
+    }
+
+    @Test fun intersection() {
+        val range = ("a" rangeTo "e") and ("c" rangeTo "g")
+        assertEquals("c", range.lower)
+        assertEquals("e", range.upper)
+    }
+
+    @Test fun kotlinToAndroid() {
+        val range: Range<Int> = (1..3).toRange()
+        assertEquals(1, range.lower)
+        assertEquals(3, range.upper)
+    }
+
+    @Test fun androidToKotlin() {
+        val range: ClosedRange<String> = Range<String>("a", "c").toClosedRange()
+        assertEquals("a", range.start)
+        assertEquals("c", range.endInclusive)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/SizeTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/SizeTest.kt
new file mode 100644
index 0000000..b38c747
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/SizeTest.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+package androidx.core.util
+
+import android.support.test.filters.SdkSuppress
+import android.util.Size
+import android.util.SizeF
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 21)
+class SizeTest {
+    @Test fun destructuringSize() {
+        val (w, h) = Size(320, 240)
+        assertEquals(320, w)
+        assertEquals(240, h)
+    }
+
+    @Test fun destructuringSizeF() {
+        val (w, h) = SizeF(1920.0f, 1080.0f)
+        assertEquals(1920.0f, w)
+        assertEquals(1080.0f, h)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/SparseArrayTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/SparseArrayTest.kt
new file mode 100644
index 0000000..4a49214
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/SparseArrayTest.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.util.SparseArray
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class SparseArrayTest {
+    @Test fun sizeProperty() {
+        val array = SparseArray<String>()
+        assertEquals(0, array.size)
+        array.put(1, "one")
+        assertEquals(1, array.size)
+    }
+
+    @Test fun containsOperator() {
+        val array = SparseArray<String>()
+        assertFalse(1 in array)
+        array.put(1, "one")
+        assertTrue(1 in array)
+    }
+
+    @Test fun containsOperatorWithItem() {
+        val array = SparseArray<String>()
+
+        array.put(1, "one")
+        assertFalse(2 in array)
+
+        array.put(2, "two")
+        assertTrue(2 in array)
+    }
+
+    @Test fun setOperator() {
+        val array = SparseArray<String>()
+        array[1] = "one"
+        assertEquals("one", array.get(1))
+    }
+
+    @Test fun plusOperator() {
+        val first = SparseArray<String>().apply { put(1, "one") }
+        val second = SparseArray<String>().apply { put(2, "two") }
+        val combined = first + second
+        assertEquals(2, combined.size())
+        assertEquals(1, combined.keyAt(0))
+        assertEquals("one", combined.valueAt(0))
+        assertEquals(2, combined.keyAt(1))
+        assertEquals("two", combined.valueAt(1))
+    }
+
+    @Test fun containsKey() {
+        val array = SparseArray<String>()
+        assertFalse(array.containsKey(1))
+        array.put(1, "one")
+        assertTrue(array.containsKey(1))
+    }
+
+    @Test fun containsValue() {
+        val array = SparseArray<String>()
+        assertFalse(array.containsValue("one"))
+        array.put(1, "one")
+        assertTrue(array.containsValue("one"))
+    }
+
+    @Test fun getOrDefault() {
+        val array = SparseArray<Any>()
+        val default = Any()
+        assertSame(default, array.getOrDefault(1, default))
+        array.put(1, "one")
+        assertEquals("one", array.getOrDefault(1, default))
+    }
+
+    @Test fun getOrElse() {
+        val array = SparseArray<Any>()
+        val default = Any()
+        assertSame(default, array.getOrElse(1) { default })
+        array.put(1, "one")
+        assertEquals("one", array.getOrElse(1) { fail() })
+    }
+
+    @Test fun isEmpty() {
+        val array = SparseArray<String>()
+        assertTrue(array.isEmpty())
+        array.put(1, "one")
+        assertFalse(array.isEmpty())
+    }
+
+    @Test fun isNotEmpty() {
+        val array = SparseArray<String>()
+        assertFalse(array.isNotEmpty())
+        array.put(1, "one")
+        assertTrue(array.isNotEmpty())
+    }
+
+    @Test fun removeValue() {
+        val array = SparseArray<String>()
+        array.put(1, "one")
+        assertFalse(array.remove(0, "one"))
+        assertEquals(1, array.size())
+        assertFalse(array.remove(1, "two"))
+        assertEquals(1, array.size())
+        assertTrue(array.remove(1, "one"))
+        assertEquals(0, array.size())
+    }
+
+    @Test fun putAll() {
+        val dest = SparseArray<String>()
+        val source = SparseArray<String>()
+        source.put(1, "one")
+
+        assertEquals(0, dest.size())
+        dest.putAll(source)
+        assertEquals(1, dest.size())
+    }
+
+    @Test fun forEach() {
+        val array = SparseArray<String>()
+        array.forEach { _, _ -> fail() }
+
+        array.put(1, "one")
+        array.put(2, "two")
+        array.put(6, "six")
+
+        val keys = mutableListOf<Int>()
+        val values = mutableListOf<String>()
+        array.forEach { key, value ->
+            keys.add(key)
+            values.add(value)
+        }
+        assertThat(keys).containsExactly(1, 2, 6)
+        assertThat(values).containsExactly("one", "two", "six")
+    }
+
+    @Test fun keyIterator() {
+        val array = SparseArray<String>()
+        assertFalse(array.keyIterator().hasNext())
+
+        array.put(1, "one")
+        array.put(2, "two")
+        array.put(6, "six")
+
+        val iterator = array.keyIterator()
+        assertTrue(iterator.hasNext())
+        assertEquals(1, iterator.nextInt())
+        assertTrue(iterator.hasNext())
+        assertEquals(2, iterator.nextInt())
+        assertTrue(iterator.hasNext())
+        assertEquals(6, iterator.nextInt())
+        assertFalse(iterator.hasNext())
+    }
+
+    @Test fun valueIterator() {
+        val array = SparseArray<String>()
+        assertFalse(array.valueIterator().hasNext())
+
+        array.put(1, "one")
+        array.put(2, "two")
+        array.put(6, "six")
+
+        val iterator = array.valueIterator()
+        assertTrue(iterator.hasNext())
+        assertEquals("one", iterator.next())
+        assertTrue(iterator.hasNext())
+        assertEquals("two", iterator.next())
+        assertTrue(iterator.hasNext())
+        assertEquals("six", iterator.next())
+        assertFalse(iterator.hasNext())
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/SparseBooleanArrayTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/SparseBooleanArrayTest.kt
new file mode 100644
index 0000000..1d67493
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/SparseBooleanArrayTest.kt
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.util.SparseBooleanArray
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class SparseBooleanArrayTest {
+    @Test fun sizeProperty() {
+        val array = SparseBooleanArray()
+        assertEquals(0, array.size)
+        array.put(1, true)
+        assertEquals(1, array.size)
+    }
+
+    @Test fun containsOperator() {
+        val array = SparseBooleanArray()
+        assertFalse(1 in array)
+        array.put(1, true)
+        assertTrue(1 in array)
+    }
+
+    @Test fun containsOperatorWithValue() {
+        val array = SparseBooleanArray()
+
+        array.put(1, true)
+        assertFalse(2 in array)
+
+        array.put(2, true)
+        assertTrue(2 in array)
+    }
+
+    @Test fun setOperator() {
+        val array = SparseBooleanArray()
+        array[1] = true
+        assertTrue(array.get(1))
+    }
+
+    @Test fun plusOperator() {
+        val first = SparseBooleanArray().apply { put(1, true) }
+        val second = SparseBooleanArray().apply { put(2, false) }
+        val combined = first + second
+        assertEquals(2, combined.size())
+        assertEquals(1, combined.keyAt(0))
+        assertTrue(combined.valueAt(0))
+        assertEquals(2, combined.keyAt(1))
+        assertFalse(combined.valueAt(1))
+    }
+
+    @Test fun containsKey() {
+        val array = SparseBooleanArray()
+        assertFalse(array.containsKey(1))
+        array.put(1, true)
+        assertTrue(array.containsKey(1))
+    }
+
+    @Test fun containsKeyWithValue() {
+        val array = SparseBooleanArray()
+
+        array.put(1, true)
+        assertFalse(array.containsKey(2))
+
+        array.put(2, true)
+        assertTrue(array.containsKey(2))
+    }
+
+    @Test fun containsValue() {
+        val array = SparseBooleanArray()
+        assertFalse(array.containsValue(true))
+        array.put(1, true)
+        assertTrue(array.containsValue(true))
+    }
+
+    @Test fun getOrDefault() {
+        val array = SparseBooleanArray()
+        assertFalse(array.getOrDefault(1, false))
+        array.put(1, true)
+        assertTrue(array.getOrDefault(1, false))
+    }
+
+    @Test fun getOrElse() {
+        val array = SparseBooleanArray()
+        assertFalse(array.getOrElse(1) { false })
+        array.put(1, true)
+        assertTrue(array.getOrElse(1) { fail() })
+    }
+
+    @Test fun isEmpty() {
+        val array = SparseBooleanArray()
+        assertTrue(array.isEmpty())
+        array.put(1, true)
+        assertFalse(array.isEmpty())
+    }
+
+    @Test fun isNotEmpty() {
+        val array = SparseBooleanArray()
+        assertFalse(array.isNotEmpty())
+        array.put(1, true)
+        assertTrue(array.isNotEmpty())
+    }
+
+    @Test fun removeValue() {
+        val array = SparseBooleanArray()
+        array.put(1, true)
+        assertFalse(array.remove(0, true))
+        assertEquals(1, array.size())
+        assertFalse(array.remove(1, false))
+        assertEquals(1, array.size())
+        assertTrue(array.remove(1, true))
+        assertEquals(0, array.size())
+    }
+
+    @Test fun putAll() {
+        val dest = SparseBooleanArray()
+        val source = SparseBooleanArray()
+        source.put(1, true)
+
+        assertEquals(0, dest.size())
+        dest.putAll(source)
+        assertEquals(1, dest.size())
+    }
+
+    @Test fun forEach() {
+        val array = SparseBooleanArray()
+        array.forEach { _, _ -> fail() }
+
+        array.put(1, true)
+        array.put(2, false)
+        array.put(6, true)
+
+        val keys = mutableListOf<Int>()
+        val values = mutableListOf<Boolean>()
+        array.forEach { key, value ->
+            keys.add(key)
+            values.add(value)
+        }
+        assertThat(keys).containsExactly(1, 2, 6)
+        assertThat(values).containsExactly(true, false, true)
+    }
+
+    @Test fun keyIterator() {
+        val array = SparseBooleanArray()
+        assertFalse(array.keyIterator().hasNext())
+
+        array.put(1, true)
+        array.put(2, false)
+        array.put(6, true)
+
+        val iterator = array.keyIterator()
+        assertTrue(iterator.hasNext())
+        assertEquals(1, iterator.nextInt())
+        assertTrue(iterator.hasNext())
+        assertEquals(2, iterator.nextInt())
+        assertTrue(iterator.hasNext())
+        assertEquals(6, iterator.nextInt())
+        assertFalse(iterator.hasNext())
+    }
+
+    @Test fun valueIterator() {
+        val array = SparseBooleanArray()
+        assertFalse(array.valueIterator().hasNext())
+
+        array.put(1, true)
+        array.put(2, false)
+        array.put(6, true)
+
+        val iterator = array.valueIterator()
+        assertTrue(iterator.hasNext())
+        assertTrue(iterator.nextBoolean())
+        assertTrue(iterator.hasNext())
+        assertFalse(iterator.nextBoolean())
+        assertTrue(iterator.hasNext())
+        assertTrue(iterator.nextBoolean())
+        assertFalse(iterator.hasNext())
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/SparseIntArrayTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/SparseIntArrayTest.kt
new file mode 100644
index 0000000..40794c1
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/SparseIntArrayTest.kt
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.util.SparseIntArray
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class SparseIntArrayTest {
+    @Test fun sizeProperty() {
+        val array = SparseIntArray()
+        assertEquals(0, array.size)
+        array.put(1, 11)
+        assertEquals(1, array.size)
+    }
+
+    @Test fun containsOperator() {
+        val array = SparseIntArray()
+        assertFalse(1 in array)
+        array.put(1, 11)
+        assertTrue(1 in array)
+    }
+
+    @Test fun containsOperatorWithValue() {
+        val array = SparseIntArray()
+
+        array.put(1, 11)
+        assertFalse(2 in array)
+
+        array.put(2, 22)
+        assertTrue(2 in array)
+    }
+
+    @Test fun setOperator() {
+        val array = SparseIntArray()
+        array[1] = 11
+        assertEquals(11, array.get(1))
+    }
+
+    @Test fun plusOperator() {
+        val first = SparseIntArray().apply { put(1, 11) }
+        val second = SparseIntArray().apply { put(2, 22) }
+        val combined = first + second
+        assertEquals(2, combined.size())
+        assertEquals(1, combined.keyAt(0))
+        assertEquals(11, combined.valueAt(0))
+        assertEquals(2, combined.keyAt(1))
+        assertEquals(22, combined.valueAt(1))
+    }
+
+    @Test fun containsKey() {
+        val array = SparseIntArray()
+        assertFalse(array.containsKey(1))
+        array.put(1, 11)
+        assertTrue(array.containsKey(1))
+    }
+
+    @Test fun containsKeyWithValue() {
+        val array = SparseIntArray()
+
+        array.put(1, 11)
+        assertFalse(array.containsKey(2))
+
+        array.put(2, 22)
+        assertTrue(array.containsKey(2))
+    }
+
+    @Test fun containsValue() {
+        val array = SparseIntArray()
+        assertFalse(array.containsValue(11))
+        array.put(1, 11)
+        assertTrue(array.containsValue(11))
+    }
+
+    @Test fun getOrDefault() {
+        val array = SparseIntArray()
+        assertEquals(22, array.getOrDefault(1, 22))
+        array.put(1, 11)
+        assertEquals(11, array.getOrDefault(1, 22))
+    }
+
+    @Test fun getOrElse() {
+        val array = SparseIntArray()
+        assertEquals(22, array.getOrElse(1) { 22 })
+        array.put(1, 11)
+        assertEquals(11, array.getOrElse(1) { fail() })
+    }
+
+    @Test fun isEmpty() {
+        val array = SparseIntArray()
+        assertTrue(array.isEmpty())
+        array.put(1, 11)
+        assertFalse(array.isEmpty())
+    }
+
+    @Test fun isNotEmpty() {
+        val array = SparseIntArray()
+        assertFalse(array.isNotEmpty())
+        array.put(1, 11)
+        assertTrue(array.isNotEmpty())
+    }
+
+    @Test fun removeValue() {
+        val array = SparseIntArray()
+        array.put(1, 11)
+        assertFalse(array.remove(0, 11))
+        assertEquals(1, array.size())
+        assertFalse(array.remove(1, 22))
+        assertEquals(1, array.size())
+        assertTrue(array.remove(1, 11))
+        assertEquals(0, array.size())
+    }
+
+    @Test fun putAll() {
+        val dest = SparseIntArray()
+        val source = SparseIntArray()
+        source.put(1, 11)
+
+        assertEquals(0, dest.size())
+        dest.putAll(source)
+        assertEquals(1, dest.size())
+    }
+
+    @Test fun forEach() {
+        val array = SparseIntArray()
+        array.forEach { _, _ -> fail() }
+
+        array.put(1, 11)
+        array.put(2, 22)
+        array.put(6, 66)
+
+        val keys = mutableListOf<Int>()
+        val values = mutableListOf<Int>()
+        array.forEach { key, value ->
+            keys.add(key)
+            values.add(value)
+        }
+        assertThat(keys).containsExactly(1, 2, 6)
+        assertThat(values).containsExactly(11, 22, 66)
+    }
+
+    @Test fun keyIterator() {
+        val array = SparseIntArray()
+        assertFalse(array.keyIterator().hasNext())
+
+        array.put(1, 11)
+        array.put(2, 22)
+        array.put(6, 66)
+
+        val iterator = array.keyIterator()
+        assertTrue(iterator.hasNext())
+        assertEquals(1, iterator.nextInt())
+        assertTrue(iterator.hasNext())
+        assertEquals(2, iterator.nextInt())
+        assertTrue(iterator.hasNext())
+        assertEquals(6, iterator.nextInt())
+        assertFalse(iterator.hasNext())
+    }
+
+    @Test fun valueIterator() {
+        val array = SparseIntArray()
+        assertFalse(array.valueIterator().hasNext())
+
+        array.put(1, 11)
+        array.put(2, 22)
+        array.put(6, 66)
+
+        val iterator = array.valueIterator()
+        assertTrue(iterator.hasNext())
+        assertEquals(11, iterator.nextInt())
+        assertTrue(iterator.hasNext())
+        assertEquals(22, iterator.nextInt())
+        assertTrue(iterator.hasNext())
+        assertEquals(66, iterator.nextInt())
+        assertFalse(iterator.hasNext())
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/SparseLongArrayTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/SparseLongArrayTest.kt
new file mode 100644
index 0000000..c65c2ad
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/SparseLongArrayTest.kt
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.support.test.filters.SdkSuppress
+import android.util.SparseLongArray
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 18)
+class SparseLongArrayTest {
+    @Test fun sizeProperty() {
+        val array = SparseLongArray()
+        assertEquals(0, array.size)
+        array.put(1, 11L)
+        assertEquals(1, array.size)
+    }
+
+    @Test fun containsOperator() {
+        val array = SparseLongArray()
+        assertFalse(1 in array)
+        array.put(1, 11L)
+        assertTrue(1 in array)
+    }
+
+    @Test fun containsOperatorWithValue() {
+        val array = SparseLongArray()
+
+        array.put(1, 11L)
+        assertFalse(2 in array)
+
+        array.put(2, 22L)
+        assertTrue(2 in array)
+    }
+
+    @Test fun setOperator() {
+        val array = SparseLongArray()
+        array[1] = 11L
+        assertEquals(11L, array.get(1))
+    }
+
+    @Test fun plusOperator() {
+        val first = SparseLongArray().apply { put(1, 11L) }
+        val second = SparseLongArray().apply { put(2, 22L) }
+        val combined = first + second
+        assertEquals(2, combined.size())
+        assertEquals(1, combined.keyAt(0))
+        assertEquals(11L, combined.valueAt(0))
+        assertEquals(2, combined.keyAt(1))
+        assertEquals(22L, combined.valueAt(1))
+    }
+
+    @Test fun containsKey() {
+        val array = SparseLongArray()
+        assertFalse(array.containsKey(1))
+        array.put(1, 11L)
+        assertTrue(array.containsKey(1))
+    }
+
+    @Test fun containsKeyWithValue() {
+        val array = SparseLongArray()
+
+        array.put(1, 11L)
+        assertFalse(array.containsKey(2))
+
+        array.put(2, 22L)
+        assertTrue(array.containsKey(2))
+    }
+
+    @Test fun containsValue() {
+        val array = SparseLongArray()
+        assertFalse(array.containsValue(11L))
+        array.put(1, 11L)
+        assertTrue(array.containsValue(11L))
+    }
+
+    @Test fun getOrDefault() {
+        val array = SparseLongArray()
+        assertEquals(22L, array.getOrDefault(1, 22L))
+        array.put(1, 11L)
+        assertEquals(11L, array.getOrDefault(1, 22L))
+    }
+
+    @Test fun getOrElse() {
+        val array = SparseLongArray()
+        assertEquals(22L, array.getOrElse(1) { 22L })
+        array.put(1, 11L)
+        assertEquals(11L, array.getOrElse(1) { fail() })
+    }
+
+    @Test fun isEmpty() {
+        val array = SparseLongArray()
+        assertTrue(array.isEmpty())
+        array.put(1, 11L)
+        assertFalse(array.isEmpty())
+    }
+
+    @Test fun isNotEmpty() {
+        val array = SparseLongArray()
+        assertFalse(array.isNotEmpty())
+        array.put(1, 11L)
+        assertTrue(array.isNotEmpty())
+    }
+
+    @Test fun removeValue() {
+        val array = SparseLongArray()
+        array.put(1, 11L)
+        assertFalse(array.remove(0, 11L))
+        assertEquals(1, array.size())
+        assertFalse(array.remove(1, 22L))
+        assertEquals(1, array.size())
+        assertTrue(array.remove(1, 11L))
+        assertEquals(0, array.size())
+    }
+
+    @Test fun putAll() {
+        val dest = SparseLongArray()
+        val source = SparseLongArray()
+        source.put(1, 11L)
+
+        assertEquals(0, dest.size())
+        dest.putAll(source)
+        assertEquals(1, dest.size())
+    }
+
+    @Test fun forEach() {
+        val array = SparseLongArray()
+        array.forEach { _, _ -> fail() }
+
+        array.put(1, 11L)
+        array.put(2, 22L)
+        array.put(6, 66L)
+
+        val keys = mutableListOf<Int>()
+        val values = mutableListOf<Long>()
+        array.forEach { key, value ->
+            keys.add(key)
+            values.add(value)
+        }
+        assertThat(keys).containsExactly(1, 2, 6)
+        assertThat(values).containsExactly(11L, 22L, 66L)
+    }
+
+    @Test fun keyIterator() {
+        val array = SparseLongArray()
+        assertFalse(array.keyIterator().hasNext())
+
+        array.put(1, 11L)
+        array.put(2, 22L)
+        array.put(6, 66L)
+
+        val iterator = array.keyIterator()
+        assertTrue(iterator.hasNext())
+        assertEquals(1, iterator.nextInt())
+        assertTrue(iterator.hasNext())
+        assertEquals(2, iterator.nextInt())
+        assertTrue(iterator.hasNext())
+        assertEquals(6, iterator.nextInt())
+        assertFalse(iterator.hasNext())
+    }
+
+    @Test fun valueIterator() {
+        val array = SparseLongArray()
+        assertFalse(array.valueIterator().hasNext())
+
+        array.put(1, 11L)
+        array.put(2, 22L)
+        array.put(6, 66L)
+
+        val iterator = array.valueIterator()
+        assertTrue(iterator.hasNext())
+        assertEquals(11, iterator.nextLong())
+        assertTrue(iterator.hasNext())
+        assertEquals(22, iterator.nextLong())
+        assertTrue(iterator.hasNext())
+        assertEquals(66, iterator.nextLong())
+        assertFalse(iterator.hasNext())
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/view/AccessibilityAnnouncementCapturingView.kt b/core/ktx/src/androidTest/java/androidx/core/view/AccessibilityAnnouncementCapturingView.kt
new file mode 100644
index 0000000..81996b8
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/view/AccessibilityAnnouncementCapturingView.kt
@@ -0,0 +1,14 @@
+package androidx.core.view
+
+import android.content.Context
+import android.view.View
+
+class AccessibilityAnnouncementCapturingView(context: Context?) : View(context) {
+
+    var announcement: CharSequence? = null
+
+    override fun announceForAccessibility(text: CharSequence?) {
+        super.announceForAccessibility(text)
+        announcement = text
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/view/MenuTest.kt b/core/ktx/src/androidTest/java/androidx/core/view/MenuTest.kt
new file mode 100644
index 0000000..313ed1b
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/view/MenuTest.kt
@@ -0,0 +1,161 @@
+/*
+ * 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.
+ */
+
+package androidx.core.view
+
+import android.support.test.InstrumentationRegistry
+import android.view.Menu.NONE
+import android.view.MenuItem
+import android.widget.Toolbar
+import androidx.testutils.assertThrows
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class MenuTest {
+    private val menu = Toolbar(InstrumentationRegistry.getContext()).menu
+
+    @Test fun get() {
+        val item = menu.add("")
+        assertSame(item, menu[0])
+    }
+
+    @Test fun contains() {
+        val item1 = menu.add("")
+        assertTrue(item1 in menu)
+
+        val item2 = menu.add("")
+        assertTrue(item2 in menu)
+    }
+
+    @Test fun minusAssign() {
+        val item1 = menu.add(NONE, 1, NONE, "")
+        val item2 = menu.add(NONE, 2, NONE, "")
+
+        assertEquals(2, menu.size)
+
+        menu -= item2
+        assertEquals(1, menu.size)
+        assertSame(item1, menu.getItem(0))
+
+        menu -= item1
+        assertEquals(0, menu.size)
+    }
+
+    @Test fun size() {
+        assertEquals(0, menu.size)
+
+        menu.add("")
+        assertEquals(1, menu.size)
+
+        menu.add(NONE, 123, NONE, "")
+        assertEquals(2, menu.size)
+
+        menu.removeItem(123)
+        assertEquals(1, menu.size)
+    }
+
+    @Test fun isEmpty() {
+        assertTrue(menu.isEmpty())
+        menu.add("")
+        assertFalse(menu.isEmpty())
+    }
+
+    @Test fun isNotEmpty() {
+        assertFalse(menu.isNotEmpty())
+        menu.add("")
+        assertTrue(menu.isNotEmpty())
+    }
+
+    @Test fun forEach() {
+        menu.forEach {
+            fail("Empty menu should not invoke lambda")
+        }
+
+        val item1 = menu.add("")
+        val item2 = menu.add("")
+
+        val items = mutableListOf<MenuItem>()
+        menu.forEach {
+            items += it
+        }
+        assertThat(items).containsExactly(item1, item2)
+    }
+
+    @Test fun forEachIndexed() {
+        menu.forEachIndexed { _, _ ->
+            fail("Empty menu should not invoke lambda")
+        }
+
+        val item1 = menu.add("")
+        val item2 = menu.add("")
+
+        val items = mutableListOf<MenuItem>()
+        menu.forEachIndexed { index, item ->
+            assertEquals(index, items.size)
+            items += item
+        }
+        assertThat(items).containsExactly(item1, item2)
+    }
+
+    @Test fun iterator() {
+        val item1 = menu.add("")
+        val item2 = menu.add("")
+
+        val iterator = menu.iterator()
+        assertTrue(iterator.hasNext())
+        assertSame(item1, iterator.next())
+        assertTrue(iterator.hasNext())
+        assertSame(item2, iterator.next())
+        assertFalse(iterator.hasNext())
+        assertThrows<IndexOutOfBoundsException> {
+            iterator.next()
+        }
+    }
+
+    @Test fun iteratorRemoving() {
+        val item1 = menu.add("")
+        val item2 = menu.add("")
+
+        val iterator = menu.iterator()
+
+        assertSame(item1, iterator.next())
+        iterator.remove()
+        assertFalse(item1 in menu)
+        assertEquals(1, menu.size())
+
+        assertSame(item2, iterator.next())
+        iterator.remove()
+        assertFalse(item2 in menu)
+        assertEquals(0, menu.size())
+    }
+
+    @Test fun children() {
+        val items = listOf(
+            menu.add(NONE, 1, NONE, ""),
+            menu.add(NONE, 2, NONE, ""),
+            menu.add(NONE, 3, NONE, "")
+        )
+
+        menu.children.forEachIndexed { index, child ->
+            assertSame(items[index], child)
+        }
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/view/ViewGroupTest.kt b/core/ktx/src/androidTest/java/androidx/core/view/ViewGroupTest.kt
new file mode 100644
index 0000000..7ec791d
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/view/ViewGroupTest.kt
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.view
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SdkSuppress
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import androidx.testutils.assertThrows
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ViewGroupTest {
+    private val context = InstrumentationRegistry.getContext()
+    private val viewGroup = LinearLayout(context)
+
+    @Test fun get() {
+        val view1 = View(context)
+        viewGroup.addView(view1)
+        val view2 = View(context)
+        viewGroup.addView(view2)
+
+        assertSame(view1, viewGroup[0])
+        assertSame(view2, viewGroup[1])
+
+        assertThrows<IndexOutOfBoundsException> {
+            viewGroup[-1]
+        }.hasMessageThat().isEqualTo("Index: -1, Size: 2")
+
+        assertThrows<IndexOutOfBoundsException> {
+            viewGroup[2]
+        }.hasMessageThat().isEqualTo("Index: 2, Size: 2")
+    }
+
+    @Test fun contains() {
+        val view1 = View(context)
+        viewGroup.addView(view1)
+        assertTrue(view1 in viewGroup)
+        assertFalse(view1 !in viewGroup)
+
+        val view2 = View(context)
+        assertFalse(view2 in viewGroup)
+        assertTrue(view2 !in viewGroup)
+    }
+
+    @Test fun plusAssign() {
+        assertEquals(0, viewGroup.childCount)
+
+        val view1 = View(context)
+        viewGroup += view1
+        assertEquals(1, viewGroup.childCount)
+        assertSame(view1, viewGroup.getChildAt(0))
+
+        val view2 = View(context)
+        viewGroup += view2
+        assertEquals(2, viewGroup.childCount)
+        assertSame(view2, viewGroup.getChildAt(1))
+    }
+
+    @Test fun minusAssign() {
+        val view1 = View(context)
+        viewGroup.addView(view1)
+        val view2 = View(context)
+        viewGroup.addView(view2)
+
+        assertEquals(2, viewGroup.childCount)
+
+        viewGroup -= view2
+        assertEquals(1, viewGroup.childCount)
+        assertSame(view1, viewGroup.getChildAt(0))
+
+        viewGroup -= view1
+        assertEquals(0, viewGroup.childCount)
+    }
+
+    @Test fun size() {
+        assertEquals(0, viewGroup.size)
+
+        viewGroup.addView(View(context))
+        assertEquals(1, viewGroup.size)
+
+        viewGroup.addView(View(context))
+        assertEquals(2, viewGroup.size)
+
+        viewGroup.removeViewAt(0)
+        assertEquals(1, viewGroup.size)
+    }
+
+    @Test fun isEmpty() {
+        assertTrue(viewGroup.isEmpty())
+        viewGroup.addView(View(context))
+        assertFalse(viewGroup.isEmpty())
+    }
+
+    @Test fun isNotEmpty() {
+        assertFalse(viewGroup.isNotEmpty())
+        viewGroup.addView(View(context))
+        assertTrue(viewGroup.isNotEmpty())
+    }
+
+    @Test fun forEach() {
+        viewGroup.forEach {
+            fail("Empty view group should not invoke lambda")
+        }
+
+        val view1 = View(context)
+        viewGroup.addView(view1)
+        val view2 = View(context)
+        viewGroup.addView(view2)
+
+        val views = mutableListOf<View>()
+        viewGroup.forEach {
+            views += it
+        }
+        assertThat(views).containsExactly(view1, view2)
+    }
+
+    @Test fun forEachIndexed() {
+        viewGroup.forEachIndexed { _, _ ->
+            fail("Empty view group should not invoke lambda")
+        }
+
+        val view1 = View(context)
+        viewGroup.addView(view1)
+        val view2 = View(context)
+        viewGroup.addView(view2)
+
+        val views = mutableListOf<View>()
+        viewGroup.forEachIndexed { index, view ->
+            assertEquals(index, views.size)
+            views += view
+        }
+        assertThat(views).containsExactly(view1, view2)
+    }
+
+    @Test fun iterator() {
+        val view1 = View(context)
+        viewGroup.addView(view1)
+        val view2 = View(context)
+        viewGroup.addView(view2)
+
+        val iterator = viewGroup.iterator()
+        assertTrue(iterator.hasNext())
+        assertSame(view1, iterator.next())
+        assertTrue(iterator.hasNext())
+        assertSame(view2, iterator.next())
+        assertFalse(iterator.hasNext())
+        assertThrows<IndexOutOfBoundsException> {
+            iterator.next()
+        }
+    }
+
+    @Test fun iteratorRemoving() {
+        val view1 = View(context)
+        viewGroup.addView(view1)
+        val view2 = View(context)
+        viewGroup.addView(view2)
+
+        val iterator = viewGroup.iterator()
+
+        assertSame(view1, iterator.next())
+        iterator.remove()
+        assertFalse(view1 in viewGroup)
+        assertEquals(1, viewGroup.childCount)
+
+        assertSame(view2, iterator.next())
+        iterator.remove()
+        assertFalse(view2 in viewGroup)
+        assertEquals(0, viewGroup.childCount)
+    }
+
+    @Test fun iteratorForEach() {
+        val views = listOf(View(context), View(context))
+        views.forEach(viewGroup::addView)
+
+        var index = 0
+        for (view in viewGroup) {
+            assertSame(views[index++], view)
+        }
+    }
+
+    @Test fun children() {
+        val views = listOf(View(context), View(context), View(context))
+        views.forEach { viewGroup.addView(it) }
+
+        viewGroup.children.forEachIndexed { index, child ->
+            assertSame(views[index], child)
+        }
+    }
+
+    @Test fun setMargins() {
+        val layoutParams = ViewGroup.MarginLayoutParams(100, 200)
+        layoutParams.setMargins(42)
+        assertEquals(42, layoutParams.leftMargin)
+        assertEquals(42, layoutParams.topMargin)
+        assertEquals(42, layoutParams.rightMargin)
+        assertEquals(42, layoutParams.bottomMargin)
+    }
+
+    @Test fun updateMargins() {
+        val layoutParams = ViewGroup.MarginLayoutParams(100, 200)
+        layoutParams.updateMargins(top = 10, right = 20)
+        assertEquals(0, layoutParams.leftMargin)
+        assertEquals(10, layoutParams.topMargin)
+        assertEquals(20, layoutParams.rightMargin)
+        assertEquals(0, layoutParams.bottomMargin)
+    }
+
+    @Test fun updateMarginsNoOp() {
+        val layoutParams = ViewGroup.MarginLayoutParams(100, 200)
+        layoutParams.setMargins(10, 20, 30, 40)
+        layoutParams.updateMargins()
+        assertEquals(10, layoutParams.leftMargin)
+        assertEquals(20, layoutParams.topMargin)
+        assertEquals(30, layoutParams.rightMargin)
+        assertEquals(40, layoutParams.bottomMargin)
+    }
+
+    @SdkSuppress(minSdkVersion = 17)
+    @Test fun updateMarginsRelative() {
+        val layoutParams = ViewGroup.MarginLayoutParams(100, 200)
+        layoutParams.updateMarginsRelative(start = 10, end = 20)
+        assertEquals(0, layoutParams.leftMargin)
+        assertEquals(0, layoutParams.topMargin)
+        assertEquals(0, layoutParams.rightMargin)
+        assertEquals(0, layoutParams.bottomMargin)
+        assertEquals(10, layoutParams.marginStart)
+        assertEquals(20, layoutParams.marginEnd)
+        assertTrue(layoutParams.isMarginRelative)
+    }
+
+    @SdkSuppress(minSdkVersion = 17)
+    @Test fun updateMarginsRelativeNoOp() {
+        val layoutParams = ViewGroup.MarginLayoutParams(100, 200)
+        layoutParams.setMargins(10, 20, 30, 40)
+        layoutParams.updateMarginsRelative()
+        assertEquals(10, layoutParams.leftMargin)
+        assertEquals(20, layoutParams.topMargin)
+        assertEquals(30, layoutParams.rightMargin)
+        assertEquals(40, layoutParams.bottomMargin)
+        assertEquals(10, layoutParams.marginStart)
+        assertEquals(30, layoutParams.marginEnd)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/view/ViewTest.kt b/core/ktx/src/androidTest/java/androidx/core/view/ViewTest.kt
new file mode 100644
index 0000000..1ccd517
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/view/ViewTest.kt
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.view
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.support.test.InstrumentationRegistry
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import android.widget.RelativeLayout
+import androidx.core.ktx.test.R
+import androidx.testutils.assertThrows
+import androidx.testutils.fail
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ViewTest {
+    private val context = InstrumentationRegistry.getContext()
+    private val view = View(context)
+
+    @Test
+    fun doOnNextLayout() {
+        var calls = 0
+        view.doOnNextLayout {
+            calls++
+        }
+        view.layout(0, 0, 10, 10)
+        assertEquals(1, calls)
+
+        // Now layout again and make sure that the listener was removed
+        view.layout(0, 0, 10, 10)
+        assertEquals(1, calls)
+    }
+
+    @Test
+    fun doOnLayoutBeforeLayout() {
+        var called = false
+        view.doOnLayout {
+            called = true
+        }
+        view.layout(0, 0, 10, 10)
+        assertTrue(called)
+    }
+
+    @Test
+    fun doOnLayoutAfterLayout() {
+        view.layout(0, 0, 10, 10)
+
+        var called = false
+        view.doOnLayout {
+            called = true
+        }
+        assertTrue(called)
+    }
+
+    @Test
+    fun doOnLayoutWhileLayoutRequested() {
+        // First layout the view
+        view.layout(0, 0, 10, 10)
+        // Then later a layout is requested
+        view.requestLayout()
+
+        var called = false
+        view.doOnLayout {
+            called = true
+        }
+
+        // Assert that we haven't been called while the layout pass is pending
+        assertFalse(called)
+
+        // Now layout the view and assert that we're called
+        view.layout(0, 0, 20, 20)
+        assertTrue(called)
+    }
+
+    @Test
+    fun doOnPreDraw() {
+        var calls = 0
+        view.doOnPreDraw {
+            calls++
+        }
+        view.viewTreeObserver.dispatchOnPreDraw()
+        assertEquals(1, calls)
+
+        // Now dispatch again to make sure that the listener was removed
+        view.viewTreeObserver.dispatchOnPreDraw()
+        assertEquals(1, calls)
+    }
+
+    @Test
+    fun setPadding() {
+        view.setPadding(42)
+        assertEquals(42, view.paddingLeft)
+        assertEquals(42, view.paddingTop)
+        assertEquals(42, view.paddingRight)
+        assertEquals(42, view.paddingBottom)
+    }
+
+    @Test
+    fun updatePadding() {
+        view.updatePadding(top = 10, right = 20)
+        assertEquals(0, view.paddingLeft)
+        assertEquals(10, view.paddingTop)
+        assertEquals(20, view.paddingRight)
+        assertEquals(0, view.paddingBottom)
+    }
+
+    @Test
+    fun updatePaddingNoOp() {
+        view.setPadding(10, 20, 30, 40)
+        view.updatePadding()
+        assertEquals(10, view.paddingLeft)
+        assertEquals(20, view.paddingTop)
+        assertEquals(30, view.paddingRight)
+        assertEquals(40, view.paddingBottom)
+    }
+
+    @Test
+    fun updatePaddingRelative() {
+        view.updatePaddingRelative(start = 10, end = 20)
+        assertEquals(10, view.paddingStart)
+        assertEquals(0, view.paddingTop)
+        assertEquals(20, view.paddingEnd)
+        assertEquals(0, view.paddingBottom)
+    }
+
+    @Test
+    fun updatePaddingRelativeNoOp() {
+        view.setPaddingRelative(10, 20, 30, 40)
+        view.updatePaddingRelative()
+        assertEquals(10, view.paddingStart)
+        assertEquals(20, view.paddingTop)
+        assertEquals(30, view.paddingEnd)
+        assertEquals(40, view.paddingBottom)
+    }
+
+    @Test
+    fun toBitmapBeforeLayout() {
+        assertThrows<IllegalStateException> {
+            view.toBitmap()
+        }
+    }
+
+    @Test
+    fun toBitmap() {
+        view.layout(0, 0, 100, 100)
+        val bitmap = view.toBitmap()
+
+        assertEquals(100, bitmap.width)
+        assertEquals(100, bitmap.height)
+    }
+
+    @Test
+    fun toBitmapCustomConfig() {
+        view.layout(0, 0, 100, 100)
+        val bitmap = view.toBitmap(Bitmap.Config.RGB_565)
+
+        assertSame(Bitmap.Config.RGB_565, bitmap.config)
+    }
+
+    @Test
+    fun toBitmapScrolls() {
+        val scrollView = LayoutInflater.from(context)!!
+                .inflate(R.layout.test_bitmap_scrolls, null, false)
+
+        val size = 100
+
+        scrollView.measure(
+                View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY),
+                View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY))
+        scrollView.layout(0, 0, size, size)
+
+        val noScroll = scrollView.toBitmap()
+        assertEquals(Color.WHITE, noScroll.getPixel(0, 0))
+        assertEquals(Color.WHITE, noScroll.getPixel(size - 1, size - 1))
+
+        scrollView.scrollTo(0, size)
+        val scrolls = scrollView.toBitmap()
+
+        assertEquals(Color.BLACK, scrolls.getPixel(0, 0))
+        assertEquals(Color.BLACK, scrolls.getPixel(size - 1, size - 1))
+    }
+
+    @Test fun isVisible() {
+        view.isVisible = true
+        assertTrue(view.isVisible)
+        assertEquals(View.VISIBLE, view.visibility)
+
+        view.isVisible = false
+        assertFalse(view.isVisible)
+        assertEquals(View.GONE, view.visibility)
+    }
+
+    @Test fun isInvisible() {
+        view.isInvisible = true
+        assertTrue(view.isInvisible)
+        assertEquals(View.INVISIBLE, view.visibility)
+
+        view.isInvisible = false
+        assertFalse(view.isInvisible)
+        assertEquals(View.VISIBLE, view.visibility)
+    }
+
+    @Test fun isGone() {
+        view.isGone = true
+        assertTrue(view.isGone)
+        assertEquals(View.GONE, view.visibility)
+
+        view.isGone = false
+        assertFalse(view.isGone)
+        assertEquals(View.VISIBLE, view.visibility)
+    }
+
+    @Test fun updateLayoutParams() {
+        view.layoutParams = ViewGroup.LayoutParams(0, 0)
+        view.updateLayoutParams {
+            assertSame(view.layoutParams, this)
+
+            width = 500
+            height = 1000
+        }
+
+        assertEquals(500, view.layoutParams.width)
+        assertEquals(1000, view.layoutParams.height)
+    }
+
+    @Test fun updateLayoutParamsAsType() {
+        view.layoutParams = LinearLayout.LayoutParams(0, 0)
+        view.updateLayoutParams<LinearLayout.LayoutParams> {
+            assertSame(view.layoutParams, this)
+
+            weight = 2f
+        }
+
+        assertEquals(2f, (view.layoutParams as LinearLayout.LayoutParams).weight)
+    }
+
+    @Test fun updateLayoutParamsWrongType() {
+        assertThrows<ClassCastException> {
+            view.updateLayoutParams<RelativeLayout.LayoutParams> {
+                fail()
+            }
+        }
+    }
+
+    @Test fun announceForAccessibility() {
+        val testView = AccessibilityAnnouncementCapturingView(context)
+
+        testView.announceForAccessibility(R.string.text)
+
+        val resolvedText = context.getText(R.string.text)
+        assertEquals(testView.announcement, resolvedText)
+    }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/widget/ToastTest.kt b/core/ktx/src/androidTest/java/androidx/core/widget/ToastTest.kt
new file mode 100644
index 0000000..f8e9931
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/widget/ToastTest.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+
+package androidx.core.widget
+
+import android.support.test.InstrumentationRegistry
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import android.widget.Toast
+import androidx.core.ktx.test.R
+import androidx.core.view.forEach
+import androidx.core.view.isVisible
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ToastTest {
+    private val context = InstrumentationRegistry.getContext()
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+
+    @Test
+    fun createToastWithTextShort() = instrumentation.runOnMainSync {
+        val toast = context.toast("Short Toast")
+        assertEquals(Toast.LENGTH_SHORT, toast.duration)
+        assertTrue(containsText(toast.view, "Short Toast"))
+        assertTrue(toast.view.isVisible)
+    }
+
+    @Test
+    fun createToastWithTextLong() = instrumentation.runOnMainSync {
+        val toast = context.toast("Long Toast", Toast.LENGTH_LONG)
+        assertEquals(Toast.LENGTH_LONG, toast.duration)
+        assertTrue(containsText(toast.view, "Long Toast"))
+        assertTrue(toast.view.isVisible)
+    }
+
+    @Test
+    fun createToastWithResIdShort() = instrumentation.runOnMainSync {
+        val toast = context.toast(R.string.text)
+        assertEquals(Toast.LENGTH_SHORT, toast.duration)
+        assertTrue(containsText(toast.view, context.getString(R.string.text)))
+        assertTrue(toast.view.isVisible)
+    }
+
+    @Test
+    fun createToastWithResIdLong() = instrumentation.runOnMainSync {
+        val toast = context.toast(R.string.text, Toast.LENGTH_LONG)
+        assertEquals(Toast.LENGTH_LONG, toast.duration)
+        assertTrue(containsText(toast.view, context.getString(R.string.text)))
+        assertTrue(toast.view.isVisible)
+    }
+
+    private fun containsText(view: View, text: String): Boolean {
+        if (view is TextView && view.text == text) {
+            return true
+        }
+        if (view is ViewGroup) {
+            view.forEach {
+                if (containsText(it, text)) {
+                    return true
+                }
+            }
+        }
+        return false
+    }
+}
diff --git a/core/ktx/src/androidTest/res/drawable/box.xml b/core/ktx/src/androidTest/res/drawable/box.xml
new file mode 100644
index 0000000..3915878
--- /dev/null
+++ b/core/ktx/src/androidTest/res/drawable/box.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <size android:height="10px" android:width="10px"/>
+    <solid android:color="#fff"/>
+</shape>
diff --git a/core/ktx/src/androidTest/res/font/inconsolata_regular.ttf b/core/ktx/src/androidTest/res/font/inconsolata_regular.ttf
new file mode 100644
index 0000000..fc981ce
--- /dev/null
+++ b/core/ktx/src/androidTest/res/font/inconsolata_regular.ttf
Binary files differ
diff --git a/core/ktx/src/androidTest/res/layout/test_activity.xml b/core/ktx/src/androidTest/res/layout/test_activity.xml
new file mode 100644
index 0000000..a477350
--- /dev/null
+++ b/core/ktx/src/androidTest/res/layout/test_activity.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:id="@+id/root"
+              android:orientation="vertical"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent">
+
+    <ImageView android:id="@+id/image_view"
+               android:layout_width="wrap_content"
+               android:layout_height="wrap_content"
+               android:src="@android:drawable/ic_media_play"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/core/ktx/src/androidTest/res/layout/test_attrs.xml b/core/ktx/src/androidTest/res/layout/test_attrs.xml
new file mode 100644
index 0000000..d5975c5
--- /dev/null
+++ b/core/ktx/src/androidTest/res/layout/test_attrs.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<LinearLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:app="http://schemas.android.com/apk/res-auto"
+        android:id="@+id/root"
+        android:orientation="vertical"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:sample="42" />
diff --git a/core/ktx/src/androidTest/res/layout/test_bitmap_scrolls.xml b/core/ktx/src/androidTest/res/layout/test_bitmap_scrolls.xml
new file mode 100644
index 0000000..c2cad5c
--- /dev/null
+++ b/core/ktx/src/androidTest/res/layout/test_bitmap_scrolls.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="100px"
+    android:layout_height="100px"
+    android:scrollbars="none"
+    >
+
+    <LinearLayout
+        android:layout_width="100px"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="100px"
+            android:background="@android:color/white" />
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="100px"
+            android:background="@android:color/black" />
+
+    </LinearLayout>
+
+</ScrollView>
\ No newline at end of file
diff --git a/core/ktx/src/androidTest/res/layout/typed_array.xml b/core/ktx/src/androidTest/res/layout/typed_array.xml
new file mode 100644
index 0000000..e485ffd
--- /dev/null
+++ b/core/ktx/src/androidTest/res/layout/typed_array.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:app="http://schemas.android.com/apk/res-auto"
+      android:layout_width="match_parent" android:layout_height="match_parent"
+      app:boolean_present="true"
+      app:color_present="#fff"
+      app:dimension_present="1px"
+      app:drawable_present="@drawable/box"
+      app:string_present="Hello"
+      app:float_present="0.1"
+      app:integer_present="1"
+      app:resource_present="@font/inconsolata_regular"
+      app:font_present="@font/inconsolata_regular"
+      app:text_array_present="@array/text_array"
+/>
diff --git a/core/ktx/src/androidTest/res/values/attrs.xml b/core/ktx/src/androidTest/res/values/attrs.xml
new file mode 100644
index 0000000..e3f9723
--- /dev/null
+++ b/core/ktx/src/androidTest/res/values/attrs.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <declare-styleable name="SampleAttrs">
+        <attr name="sample" format="integer" />
+    </declare-styleable>
+</resources>
diff --git a/core/ktx/src/androidTest/res/values/strings.xml b/core/ktx/src/androidTest/res/values/strings.xml
new file mode 100644
index 0000000..20877e9
--- /dev/null
+++ b/core/ktx/src/androidTest/res/values/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <string name="text">Hello World</string>
+    <string-array name="text_array">
+        <item>Hello</item>
+        <item>World</item>
+    </string-array>
+</resources>
diff --git a/core/ktx/src/androidTest/res/values/styles.xml b/core/ktx/src/androidTest/res/values/styles.xml
new file mode 100644
index 0000000..ea35e47
--- /dev/null
+++ b/core/ktx/src/androidTest/res/values/styles.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <declare-styleable name="TypedArrayTypes">
+        <attr name="boolean_present" format="boolean"/>
+        <attr name="boolean_absent" format="boolean"/>
+
+        <attr name="color_present" format="color"/>
+        <attr name="color_absent" format="color"/>
+
+        <attr name="dimension_present" format="dimension"/>
+        <attr name="dimension_absent" format="dimension"/>
+
+        <attr name="drawable_present" format="reference"/>
+        <attr name="drawable_absent" format="reference"/>
+
+        <attr name="float_present" format="float"/>
+        <attr name="float_absent" format="float"/>
+
+        <attr name="font_present" format="reference"/>
+        <attr name="font_absent" format="reference"/>
+
+        <attr name="integer_present" format="integer"/>
+        <attr name="integer_absent" format="integer"/>
+
+        <attr name="resource_present" format="reference"/>
+        <attr name="resource_absent" format="reference"/>
+
+        <attr name="string_present" format="string"/>
+        <attr name="string_absent" format="string"/>
+
+        <attr name="text_array_present" format="reference"/>
+        <attr name="text_array_absent" format="reference"/>
+    </declare-styleable>
+</resources>
diff --git a/core/ktx/src/androidTest/res/xml/preferences.xml b/core/ktx/src/androidTest/res/xml/preferences.xml
new file mode 100644
index 0000000..33629e1
--- /dev/null
+++ b/core/ktx/src/androidTest/res/xml/preferences.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<PreferenceScreen/>
\ No newline at end of file
diff --git a/core/ktx/src/main/AndroidManifest.xml b/core/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..7f93e5e
--- /dev/null
+++ b/core/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="androidx.core.ktx"/>
diff --git a/core/ktx/src/main/java/androidx/core/animation/Animator.kt b/core/ktx/src/main/java/androidx/core/animation/Animator.kt
new file mode 100644
index 0000000..1438bcd
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/animation/Animator.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.animation
+
+import android.animation.Animator
+import androidx.annotation.RequiresApi
+
+/**
+ * Add an action which will be invoked when the animation has ended.
+ *
+ * @return the [Animator.AnimatorListener] added to the Animator
+ * @see Animator.end
+ */
+fun Animator.doOnEnd(action: (animator: Animator) -> Unit) = addListener(onEnd = action)
+
+/**
+ * Add an action which will be invoked when the animation has started.
+ *
+ * @return the [Animator.AnimatorListener] added to the Animator
+ * @see Animator.start
+ */
+fun Animator.doOnStart(action: (animator: Animator) -> Unit) = addListener(onStart = action)
+
+/**
+ * Add an action which will be invoked when the animation has been cancelled.
+ *
+ * @return the [Animator.AnimatorListener] added to the Animator
+ * @see Animator.cancel
+ */
+fun Animator.doOnCancel(action: (animator: Animator) -> Unit) = addListener(onCancel = action)
+
+/**
+ * Add an action which will be invoked when the animation has repeated.
+ * @return the [Animator.AnimatorListener] added to the Animator
+ */
+fun Animator.doOnRepeat(action: (animator: Animator) -> Unit) = addListener(onRepeat = action)
+
+/**
+ * Add an action which will be invoked when the animation has resumed after a pause.
+ *
+ * @return the [Animator.AnimatorPauseListener] added to the Animator
+ * @see Animator.resume
+ */
+@RequiresApi(19)
+fun Animator.doOnResume(action: (animator: Animator) -> Unit) = addPauseListener(onResume = action)
+
+/**
+ * Add an action which will be invoked when the animation has been paused.
+ *
+ * @return the [Animator.AnimatorPauseListener] added to the Animator
+ * @see Animator.pause
+ */
+@RequiresApi(19)
+fun Animator.doOnPause(action: (animator: Animator) -> Unit) = addPauseListener(onPause = action)
+
+/**
+ * Add a listener to this Animator using the provided actions.
+ */
+fun Animator.addListener(
+    onEnd: ((animator: Animator) -> Unit)? = null,
+    onStart: ((animator: Animator) -> Unit)? = null,
+    onCancel: ((animator: Animator) -> Unit)? = null,
+    onRepeat: ((animator: Animator) -> Unit)? = null
+): Animator.AnimatorListener {
+    val listener = object : Animator.AnimatorListener {
+        override fun onAnimationRepeat(animator: Animator) {
+            onRepeat?.invoke(animator)
+        }
+
+        override fun onAnimationEnd(animator: Animator) {
+            onEnd?.invoke(animator)
+        }
+
+        override fun onAnimationCancel(animator: Animator) {
+            onCancel?.invoke(animator)
+        }
+
+        override fun onAnimationStart(animator: Animator) {
+            onStart?.invoke(animator)
+        }
+    }
+    addListener(listener)
+    return listener
+}
+
+/**
+ * Add a pause and resume listener to this Animator using the provided actions.
+ */
+@RequiresApi(19)
+fun Animator.addPauseListener(
+    onResume: ((animator: Animator) -> Unit)? = null,
+    onPause: ((animator: Animator) -> Unit)? = null
+): Animator.AnimatorPauseListener {
+    val listener = object : Animator.AnimatorPauseListener {
+        override fun onAnimationPause(animator: Animator) {
+            onPause?.invoke(animator)
+        }
+
+        override fun onAnimationResume(animator: Animator) {
+            onResume?.invoke(animator)
+        }
+    }
+    addPauseListener(listener)
+    return listener
+}
diff --git a/core/ktx/src/main/java/androidx/core/content/ContentValues.kt b/core/ktx/src/main/java/androidx/core/content/ContentValues.kt
new file mode 100644
index 0000000..e1a0d01
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/content/ContentValues.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+package androidx.core.content
+
+import android.content.ContentValues
+
+/**
+ * Returns a new [ContentValues] with the given key/value pairs as elements.
+ *
+ * @throws IllegalArgumentException When a value is not a supported type of [ContentValues].
+ */
+fun contentValuesOf(vararg pairs: Pair<String, Any?>) = ContentValues(pairs.size).apply {
+    for ((key, value) in pairs) {
+        when (value) {
+            null -> putNull(key)
+            is String -> put(key, value)
+            is Int -> put(key, value)
+            is Long -> put(key, value)
+            is Boolean -> put(key, value)
+            is Float -> put(key, value)
+            is Double -> put(key, value)
+            is ByteArray -> put(key, value)
+            is Byte -> put(key, value)
+            is Short -> put(key, value)
+            else -> {
+                val valueType = value.javaClass.canonicalName
+                throw IllegalArgumentException("Illegal value type $valueType for key \"$key\"")
+            }
+        }
+    }
+}
diff --git a/core/ktx/src/main/java/androidx/core/content/Context.kt b/core/ktx/src/main/java/androidx/core/content/Context.kt
new file mode 100644
index 0000000..2ffb17b
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/content/Context.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.content
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import androidx.annotation.AttrRes
+import androidx.annotation.RequiresApi
+import androidx.annotation.StyleRes
+
+/**
+ * Return the handle to a system-level service by class.
+ *
+ * The return type of this function intentionally uses a
+ * [platform type](https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types)
+ * to allow callers to decide whether they require a service be present or can tolerate its absence.
+ *
+ * @see Context.getSystemService(Class)
+ */
+@RequiresApi(23)
+@Suppress("HasPlatformType") // Intentionally propagating platform type with unknown nullability.
+inline fun <reified T> Context.systemService() = getSystemService(T::class.java)
+
+/**
+ * Executes [block] on a [TypedArray] receiver. The [TypedArray] holds the attribute
+ * values in [set] that are listed in [attrs]. In addition, if the given [AttributeSet]
+ * specifies a style class (through the `style` attribute), that style will be applied
+ * on top of the base attributes it defines.
+ *
+ * @param set The base set of attribute values.
+ * @param attrs The desired attributes to be retrieved. These attribute IDs must be
+ *              sorted in ascending order.
+ * @param defStyleAttr An attribute in the current theme that contains a reference to
+ *                     a style resource that supplies defaults values for the [TypedArray].
+ *                     Can be 0 to not look for defaults.
+ * @param defStyleRes A resource identifier of a style resource that supplies default values
+ *                    for the [TypedArray], used only if [defStyleAttr] is 0 or can not be found
+ *                     in the theme. Can be 0 to not look for defaults.
+ *
+ * @see Context.obtainStyledAttributes
+ * @see android.content.res.Resources.Theme.obtainStyledAttributes
+ */
+inline fun Context.withStyledAttributes(
+    set: AttributeSet? = null,
+    attrs: IntArray,
+    @AttrRes defStyleAttr: Int = 0,
+    @StyleRes defStyleRes: Int = 0,
+    block: TypedArray.() -> Unit
+) {
+    val typedArray = obtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes)
+    try {
+        typedArray.block()
+    } finally {
+        typedArray.recycle()
+    }
+}
+
+/**
+ * Executes [block] on a [TypedArray] receiver. The [TypedArray] holds the the values
+ * defined by the style resource [resourceId] which are listed in [attrs].
+ *
+ * @param attrs The desired attributes. These attribute IDs must be sorted in ascending order.
+ *
+ * @see Context.obtainStyledAttributes
+ * @see android.content.res.Resources.Theme.obtainStyledAttributes
+ */
+inline fun Context.withStyledAttributes(
+    @StyleRes resourceId: Int,
+    attrs: IntArray,
+    block: TypedArray.() -> Unit
+) {
+    val typedArray = obtainStyledAttributes(resourceId, attrs)
+    try {
+        typedArray.block()
+    } finally {
+        typedArray.recycle()
+    }
+}
diff --git a/core/ktx/src/main/java/androidx/core/content/SharedPreferences.kt b/core/ktx/src/main/java/androidx/core/content/SharedPreferences.kt
new file mode 100644
index 0000000..c4677ea
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/content/SharedPreferences.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.content
+
+import android.annotation.SuppressLint
+import android.content.SharedPreferences
+
+/**
+ * Allows editing of this preference instance with a call to [apply][SharedPreferences.Editor.apply]
+ * or [commit][SharedPreferences.Editor.commit] to persist the changes.
+ * Default behaviour is [apply][SharedPreferences.Editor.apply].
+ * ```
+ * prefs.edit {
+ *     putString("key", value)
+ * }
+ * ```
+ * To [commit][SharedPreferences.Editor.commit] changes:
+ * ```
+ * prefs.edit(commit = true) {
+ *     putString("key", value)
+ * }
+ * ```
+ */
+@SuppressLint("ApplySharedPref")
+inline fun SharedPreferences.edit(
+    commit: Boolean = false,
+    action: SharedPreferences.Editor.() -> Unit
+) {
+    val editor = edit()
+    action(editor)
+    if (commit) {
+        editor.commit()
+    } else {
+        editor.apply()
+    }
+}
diff --git a/core/ktx/src/main/java/androidx/core/content/res/TypedArray.kt b/core/ktx/src/main/java/androidx/core/content/res/TypedArray.kt
new file mode 100644
index 0000000..a4f6fef
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/content/res/TypedArray.kt
@@ -0,0 +1,235 @@
+/*
+ * 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.
+ */
+
+package androidx.core.content.res
+
+import android.content.res.ColorStateList
+import android.content.res.TypedArray
+import android.graphics.Typeface
+import android.graphics.drawable.Drawable
+import androidx.annotation.AnyRes
+import androidx.annotation.ColorInt
+import androidx.annotation.Dimension
+import androidx.annotation.RequiresApi
+import androidx.annotation.StyleableRes
+
+private fun TypedArray.checkAttribute(@StyleableRes index: Int) {
+    if (!hasValue(index)) {
+        throw IllegalArgumentException("Attribute not defined in set.")
+    }
+}
+
+/**
+ * Retrieve the boolean value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getBoolean
+ */
+fun TypedArray.getBooleanOrThrow(@StyleableRes index: Int): Boolean {
+    checkAttribute(index)
+    return getBoolean(index, false)
+}
+
+/**
+ * Retrieve the color value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getColor
+ */
+@ColorInt
+fun TypedArray.getColorOrThrow(@StyleableRes index: Int): Int {
+    checkAttribute(index)
+    return getColor(index, 0)
+}
+
+/**
+ * Retrieve the color state list value for the attribute at [index] or throws
+ * [IllegalArgumentException] if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getColorStateList
+ */
+fun TypedArray.getColorStateListOrThrow(@StyleableRes index: Int): ColorStateList {
+    checkAttribute(index)
+    return checkNotNull(getColorStateList(index)) {
+        "Attribute value was not a color or color state list."
+    }
+}
+
+/**
+ * Retrieve the dimension value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getDimension
+ */
+fun TypedArray.getDimensionOrThrow(@StyleableRes index: Int): Float {
+    checkAttribute(index)
+    return getDimension(index, 0f)
+}
+
+/**
+ * Retrieve the dimension pixel offset value for the attribute at [index] or throws
+ * [IllegalArgumentException] if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getDimensionPixelOffset
+ */
+@Dimension
+fun TypedArray.getDimensionPixelOffsetOrThrow(@StyleableRes index: Int): Int {
+    checkAttribute(index)
+    return getDimensionPixelOffset(index, 0)
+}
+
+/**
+ * Retrieve the dimension pixel size value for the attribute at [index] or throws
+ * [IllegalArgumentException] if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getDimensionPixelSize
+ */
+@Dimension
+fun TypedArray.getDimensionPixelSizeOrThrow(@StyleableRes index: Int): Int {
+    checkAttribute(index)
+    return getDimensionPixelSize(index, 0)
+}
+
+/**
+ * Retrieve the drawable value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getDrawable
+ */
+fun TypedArray.getDrawableOrThrow(@StyleableRes index: Int): Drawable {
+    checkAttribute(index)
+    return getDrawable(index)
+}
+
+/**
+ * Retrieve the float value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getFloat
+ */
+fun TypedArray.getFloatOrThrow(@StyleableRes index: Int): Float {
+    checkAttribute(index)
+    return getFloat(index, 0f)
+}
+
+/**
+ * Retrieve the font value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getFont
+ */
+@RequiresApi(26)
+fun TypedArray.getFontOrThrow(@StyleableRes index: Int): Typeface {
+    checkAttribute(index)
+    return getFont(index)
+}
+
+/**
+ * Retrieve the integer value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getInt
+ */
+fun TypedArray.getIntOrThrow(@StyleableRes index: Int): Int {
+    checkAttribute(index)
+    return getInt(index, 0)
+}
+
+/**
+ * Retrieve the integer value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getInteger
+ */
+fun TypedArray.getIntegerOrThrow(@StyleableRes index: Int): Int {
+    checkAttribute(index)
+    return getInteger(index, 0)
+}
+
+/**
+ * Retrieves the resource identifier for the attribute at [index] or throws
+ * [IllegalArgumentException] if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getResourceId
+ */
+@AnyRes
+fun TypedArray.getResourceIdOrThrow(@StyleableRes index: Int): Int {
+    checkAttribute(index)
+    return getResourceId(index, 0)
+}
+
+/**
+ * Retrieve the string value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getString
+ */
+fun TypedArray.getStringOrThrow(@StyleableRes index: Int): String {
+    checkAttribute(index)
+    return checkNotNull(getString(index)) {
+        "Attribute value could not be coerced to String."
+    }
+}
+
+/**
+ * Retrieve the text value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getText
+ */
+fun TypedArray.getTextOrThrow(@StyleableRes index: Int): CharSequence {
+    checkAttribute(index)
+    return checkNotNull(getText(index)) {
+        "Attribute value could not be coerced to CharSequence."
+    }
+}
+
+/**
+ * Retrieve the text array value for the attribute at [index] or throws
+ * [IllegalArgumentException] if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getTextArray
+ */
+fun TypedArray.getTextArrayOrThrow(@StyleableRes index: Int): Array<CharSequence> {
+    checkAttribute(index)
+    return getTextArray(index)
+}
+
+/**
+ * Executes the given [block] function on this TypedArray and then recycles it.
+ *
+ * @see kotlin.io.use
+ */
+inline fun <R> TypedArray.use(block: (TypedArray) -> R): R {
+    return block(this).also {
+        recycle()
+    }
+}
diff --git a/core/ktx/src/main/java/androidx/core/database/Cursor.kt b/core/ktx/src/main/java/androidx/core/database/Cursor.kt
new file mode 100644
index 0000000..b63e719
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/database/Cursor.kt
@@ -0,0 +1,269 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to other public API.
+
+package androidx.core.database
+
+import android.database.Cursor
+
+/**
+ * Returns the value of the requested column as a byte array.
+ *
+ * The result and whether this method throws an exception when the column value is null or the
+ * column type is not a blob type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.getBlob
+ */
+inline fun Cursor.getBlob(columnName: String): ByteArray =
+    getBlob(getColumnIndexOrThrow(columnName))
+
+/**
+ * Returns the value of the requested column as a double.
+ *
+ * The result and whether this method throws an exception when the column value is null or the
+ * column type is not a floating-point type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.getDouble
+ */
+inline fun Cursor.getDouble(columnName: String): Double =
+    getDouble(getColumnIndexOrThrow(columnName))
+
+/**
+ * Returns the value of the requested column as a float.
+ *
+ * The result and whether this method throws an exception when the column value is null or the
+ * column type is not a floating-point type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.getFloat
+ */
+inline fun Cursor.getFloat(columnName: String): Float = getFloat(getColumnIndexOrThrow(columnName))
+
+/**
+ * Returns the value of the requested column as an integer.
+ *
+ * The result and whether this method throws an exception when the column value is null or the
+ * column type is not an integral type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.getInt
+ */
+inline fun Cursor.getInt(columnName: String): Int = getInt(getColumnIndexOrThrow(columnName))
+
+/**
+ * Returns the value of the requested column as a long.
+ *
+ * The result and whether this method throws an exception when the column value is null or the
+ * column type is not an integral type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.getLong
+ */
+inline fun Cursor.getLong(columnName: String): Long = getLong(getColumnIndexOrThrow(columnName))
+
+/**
+ * Returns the value of the requested column as a short.
+ *
+ * The result and whether this method throws an exception when the column value is null or the
+ * column type is not an integral type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.getShort
+ */
+inline fun Cursor.getShort(columnName: String): Short = getShort(getColumnIndexOrThrow(columnName))
+
+/**
+ * Returns the value of the requested column as a string.
+ *
+ * The result and whether this method throws an exception when the column value is null or the
+ * column type is not a string type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.getString
+ */
+inline fun Cursor.getString(columnName: String): String =
+    getString(getColumnIndexOrThrow(columnName))
+
+/**
+ * Returns the value of the requested column as a nullable byte array.
+ *
+ * The result and whether this method throws an exception when the column type is not a blob type is
+ * implementation-defined.
+ *
+ * @see Cursor.isNull
+ * @see Cursor.getBlob
+ */
+inline fun Cursor.getBlobOrNull(index: Int) = if (isNull(index)) null else getBlob(index)
+
+/**
+ * Returns the value of the requested column as a nullable double.
+ *
+ * The result and whether this method throws an exception when the column type is not a
+ * floating-point type is implementation-defined.
+ *
+ * @see Cursor.isNull
+ * @see Cursor.getDouble
+ */
+inline fun Cursor.getDoubleOrNull(index: Int) = if (isNull(index)) null else getDouble(index)
+
+/**
+ * Returns the value of the requested column as a nullable float.
+ *
+ * The result and whether this method throws an exception when the column type is not a
+ * floating-point type is implementation-defined.
+ *
+ * @see Cursor.isNull
+ * @see Cursor.getFloat
+ */
+inline fun Cursor.getFloatOrNull(index: Int) = if (isNull(index)) null else getFloat(index)
+
+/**
+ * Returns the value of the requested column as a nullable integer.
+ *
+ * The result and whether this method throws an exception when the column type is not an integral
+ * type is implementation-defined.
+ *
+ * @see Cursor.isNull
+ * @see Cursor.getInt
+ */
+inline fun Cursor.getIntOrNull(index: Int) = if (isNull(index)) null else getInt(index)
+
+/**
+ * Returns the value of the requested column as a nullable long.
+ *
+ * The result and whether this method throws an exception when the column type is not an integral
+ * type is implementation-defined.
+ *
+ * @see Cursor.isNull
+ * @see Cursor.getLong
+ */
+inline fun Cursor.getLongOrNull(index: Int) = if (isNull(index)) null else getLong(index)
+
+/**
+ * Returns the value of the requested column as a nullable short.
+ *
+ * The result and whether this method throws an exception when the column type is not an integral
+ * type is implementation-defined.
+ *
+ * @see Cursor.isNull
+ * @see Cursor.getShort
+ */
+inline fun Cursor.getShortOrNull(index: Int) = if (isNull(index)) null else getShort(index)
+
+/**
+ * Returns the value of the requested column as a nullable string.
+ *
+ * The result and whether this method throws an exception when the column type is not a string type
+ * is implementation-defined.
+ *
+ * @see Cursor.isNull
+ * @see Cursor.getString
+ */
+inline fun Cursor.getStringOrNull(index: Int) = if (isNull(index)) null else getString(index)
+
+/**
+ * Returns the value of the requested column as a nullable byte array.
+ *
+ * The result and whether this method throws an exception when the column type is not a blob type is
+ * implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.isNull
+ * @see Cursor.getBlob
+ */
+inline fun Cursor.getBlobOrNull(columnName: String) =
+    getColumnIndexOrThrow(columnName).let { if (isNull(it)) null else getBlob(it) }
+
+/**
+ * Returns the value of the requested column as a nullable double.
+ *
+ * The result and whether this method throws an exception when the column type is not a
+ * floating-point type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.isNull
+ * @see Cursor.getDouble
+ */
+inline fun Cursor.getDoubleOrNull(columnName: String) =
+    getColumnIndexOrThrow(columnName).let { if (isNull(it)) null else getDouble(it) }
+
+/**
+ * Returns the value of the requested column as a nullable float.
+ *
+ * The result and whether this method throws an exception when the column type is not a
+ * floating-point type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.isNull
+ * @see Cursor.getFloat
+ */
+inline fun Cursor.getFloatOrNull(columnName: String) =
+    getColumnIndexOrThrow(columnName).let { if (isNull(it)) null else getFloat(it) }
+
+/**
+ * Returns the value of the requested column as a nullable integer.
+ *
+ * The result and whether this method throws an exception when the column type is not an integral
+ * type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.isNull
+ * @see Cursor.getInt
+ */
+inline fun Cursor.getIntOrNull(columnName: String) =
+    getColumnIndexOrThrow(columnName).let { if (isNull(it)) null else getInt(it) }
+
+/**
+ * Returns the value of the requested column as a nullable long.
+ *
+ * The result and whether this method throws an exception when the column type is not an integral
+ * type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.isNull
+ * @see Cursor.getLong
+ */
+inline fun Cursor.getLongOrNull(columnName: String) =
+    getColumnIndexOrThrow(columnName).let { if (isNull(it)) null else getLong(it) }
+
+/**
+ * Returns the value of the requested column as a nullable short.
+ *
+ * The result and whether this method throws an exception when the column type is not an integral
+ * type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.isNull
+ * @see Cursor.getShort
+ */
+inline fun Cursor.getShortOrNull(columnName: String) =
+    getColumnIndexOrThrow(columnName).let { if (isNull(it)) null else getShort(it) }
+
+/**
+ * Returns the value of the requested column as a nullable string.
+ *
+ * The result and whether this method throws an exception when the column type is not a string type
+ * is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.isNull
+ * @see Cursor.getString
+ */
+inline fun Cursor.getStringOrNull(columnName: String) =
+    getColumnIndexOrThrow(columnName).let { if (isNull(it)) null else getString(it) }
diff --git a/core/ktx/src/main/java/androidx/core/database/sqlite/SQLiteDatabase.kt b/core/ktx/src/main/java/androidx/core/database/sqlite/SQLiteDatabase.kt
new file mode 100644
index 0000000..bdbd94e
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/database/sqlite/SQLiteDatabase.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+package androidx.core.database.sqlite
+
+import android.database.sqlite.SQLiteDatabase
+
+/**
+ * Run [body] in a transaction marking it as successful if it completes without exception.
+ *
+ * @param exclusive Run in `EXCLUSIVE` mode when true, `IMMEDIATE` mode otherwise.
+ */
+inline fun <T> SQLiteDatabase.transaction(
+    exclusive: Boolean = true,
+    body: SQLiteDatabase.() -> T
+): T {
+    if (exclusive) {
+        beginTransaction()
+    } else {
+        beginTransactionNonExclusive()
+    }
+    try {
+        val result = body()
+        setTransactionSuccessful()
+        return result
+    } finally {
+        endTransaction()
+    }
+}
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Bitmap.kt b/core/ktx/src/main/java/androidx/core/graphics/Bitmap.kt
new file mode 100644
index 0000000..45ab6ba
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Bitmap.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.graphics
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.ColorSpace
+import androidx.annotation.ColorInt
+import androidx.annotation.RequiresApi
+
+/**
+ * Creates a new [Canvas] to draw on this bitmap and executes the specified
+ * [block] on the newly created canvas. Example:
+ *
+ * ```
+ * return Bitmap.createBitmap(…).applyCanvas {
+ *    drawLine(…)
+ *    translate(…)
+ *    drawRect(…)
+ * }
+ * ```
+ */
+inline fun Bitmap.applyCanvas(block: Canvas.() -> Unit): Bitmap {
+    val c = Canvas(this)
+    c.block()
+    return this
+}
+
+/**
+ * Returns the value of the pixel at the specified location. The returned value
+ * is a [color int][android.graphics.Color] in the sRGB color space.
+ */
+inline operator fun Bitmap.get(x: Int, y: Int) = getPixel(x, y)
+
+/**
+ * Writes the specified [color int][android.graphics.Color] into the bitmap
+ * (assuming it is mutable) at the specified `(x, y)` coordinate. The specified
+ * color is converted from sRGB to the bitmap's color space if needed.
+ */
+inline operator fun Bitmap.set(x: Int, y: Int, @ColorInt color: Int) = setPixel(x, y, color)
+
+/**
+ * Creates a new bitmap, scaled from this bitmap, when possible. If the specified
+ * [width] and [height] are the same as the current width and height of this bitmap,
+ * this bitmap is returned and no new bitmap is created.
+ *
+ * @param width The new bitmap's desired width
+ * @param height The new bitmap's desired height
+ * @param filter `true` if the source should be filtered (`true` by default)
+ *
+ * @return The new scaled bitmap or the source bitmap if no scaling is required.
+ */
+inline fun Bitmap.scale(width: Int, height: Int, filter: Boolean = true): Bitmap {
+    return Bitmap.createScaledBitmap(this, width, height, filter)
+}
+
+/**
+ * Returns a mutable bitmap with the specified [width] and [height]. A config
+ * can be optionally specified. If not, the default config is [Bitmap.Config.ARGB_8888].
+ *
+ * @param width The new bitmap's desired width
+ * @param height The new bitmap's desired height
+ * @param config The new bitmap's desired [config][Bitmap.Config]
+ *
+ * @return A new bitmap with the specified dimensions and config
+ */
+inline fun createBitmap(
+    width: Int,
+    height: Int,
+    config: Bitmap.Config = Bitmap.Config.ARGB_8888
+): Bitmap {
+    return Bitmap.createBitmap(width, height, config)
+}
+
+/**
+ * Returns a mutable bitmap with the specified [width] and [height]. The config,
+ * transparency and color space can optionally be specified. They respectively
+ * default to [Bitmap.Config.ARGB_8888], `true` and [sRGB][ColorSpace.Named.SRGB].
+ *
+ * @param width The new bitmap's desired width
+ * @param height The new bitmap's desired height
+ * @param config The new bitmap's desired [config][Bitmap.Config]
+ * @param hasAlpha Whether the new bitmap is opaque or not
+ * @param colorSpace The new bitmap's color space
+ *
+ * @return A new bitmap with the specified dimensions and config
+ */
+@RequiresApi(26)
+inline fun createBitmap(
+    width: Int,
+    height: Int,
+    config: Bitmap.Config = Bitmap.Config.ARGB_8888,
+    hasAlpha: Boolean = true,
+    colorSpace: ColorSpace = ColorSpace.get(ColorSpace.Named.SRGB)
+): Bitmap {
+    return Bitmap.createBitmap(width, height, config, hasAlpha, colorSpace)
+}
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Canvas.kt b/core/ktx/src/main/java/androidx/core/graphics/Canvas.kt
new file mode 100644
index 0000000..71efc08
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Canvas.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Canvas
+import android.graphics.Matrix
+
+/**
+ * Wrap the specified [block] in calls to [Canvas.save]
+ * and [Canvas.restoreToCount].
+ */
+inline fun Canvas.withSave(block: Canvas.() -> Unit) {
+    val checkpoint = save()
+    try {
+        block()
+    } finally {
+        restoreToCount(checkpoint)
+    }
+}
+
+/**
+ * Wrap the specified [block] in calls to [Canvas.save]/[Canvas.translate]
+ * and [Canvas.restoreToCount].
+ */
+inline fun Canvas.withTranslation(
+    x: Float = 0.0f,
+    y: Float = 0.0f,
+    block: Canvas.() -> Unit
+) {
+    val checkpoint = save()
+    translate(x, y)
+    try {
+        block()
+    } finally {
+        restoreToCount(checkpoint)
+    }
+}
+
+/**
+ * Wrap the specified [block] in calls to [Canvas.save]/[Canvas.rotate]
+ * and [Canvas.restoreToCount].
+ */
+inline fun Canvas.withRotation(
+    degrees: Float = 0.0f,
+    pivotX: Float = 0.0f,
+    pivotY: Float = 0.0f,
+    block: Canvas.() -> Unit
+) {
+    val checkpoint = save()
+    rotate(degrees, pivotX, pivotY)
+    try {
+        block()
+    } finally {
+        restoreToCount(checkpoint)
+    }
+}
+
+/**
+ * Wrap the specified [block] in calls to [Canvas.save]/[Canvas.scale]
+ * and [Canvas.restoreToCount].
+ */
+inline fun Canvas.withScale(
+    x: Float = 1.0f,
+    y: Float = 1.0f,
+    pivotX: Float = 0.0f,
+    pivotY: Float = 0.0f,
+    block: Canvas.() -> Unit
+) {
+    val checkpoint = save()
+    scale(x, y, pivotX, pivotY)
+    try {
+        block()
+    } finally {
+        restoreToCount(checkpoint)
+    }
+}
+
+/**
+ * Wrap the specified [block] in calls to [Canvas.save]/[Canvas.skew]
+ * and [Canvas.restoreToCount].
+ */
+inline fun Canvas.withSkew(
+    x: Float = 0.0f,
+    y: Float = 0.0f,
+    block: Canvas.() -> Unit
+) {
+    val checkpoint = save()
+    skew(x, y)
+    try {
+        block()
+    } finally {
+        restoreToCount(checkpoint)
+    }
+}
+
+/**
+ * Wrap the specified [block] in calls to [Canvas.save]/[Canvas.concat]
+ * and [Canvas.restoreToCount].
+ */
+inline fun Canvas.withMatrix(
+    matrix: Matrix = Matrix(),
+    block: Canvas.() -> Unit
+) {
+    val checkpoint = save()
+    concat(matrix)
+    try {
+        block()
+    } finally {
+        restoreToCount(checkpoint)
+    }
+}
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Color.kt b/core/ktx/src/main/java/androidx/core/graphics/Color.kt
new file mode 100644
index 0000000..f853422
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Color.kt
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE", "WRONG_ANNOTATION_TARGET_WITH_USE_SITE_TARGET_ON_TYPE")
+
+package androidx.core.graphics
+
+import android.graphics.Color
+import android.graphics.ColorSpace
+import androidx.annotation.ColorInt
+import androidx.annotation.ColorLong
+import androidx.annotation.RequiresApi
+
+/**
+ * Returns the first component of the color. For instance, when the color model
+ * of the color is [android.graphics.ColorSpace.Model.RGB], the first component
+ * is "red".
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (red, green, blue) = myColor
+ * ```
+ */
+@RequiresApi(26)
+inline operator fun Color.component1() = getComponent(0)
+
+/**
+ * Returns the second component of the color. For instance, when the color model
+ * of the color is [android.graphics.ColorSpace.Model.RGB], the second component
+ * is "green".
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (red, green, blue) = myColor
+ * ```
+ */
+@RequiresApi(26)
+inline operator fun Color.component2() = getComponent(1)
+
+/**
+ * Returns the third component of the color. For instance, when the color model
+ * of the color is [android.graphics.ColorSpace.Model.RGB], the third component
+ * is "blue".
+= *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (red, green, blue) = myColor
+ * ```
+ */
+@RequiresApi(26)
+inline operator fun Color.component3() = getComponent(2)
+
+/**
+ * Returns the fourth component of the color. For instance, when the color model
+ * of the color is [android.graphics.ColorSpace.Model.RGB], the fourth component
+ * is "alpha".
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (red, green, blue, alpha) = myColor
+ * ```
+ */
+@RequiresApi(26)
+inline operator fun Color.component4() = getComponent(3)
+
+/**
+ * Composites two translucent colors together. More specifically, adds two colors using
+ * the [source over][android.graphics.PorterDuff.Mode.SRC_OVER] blending mode. The colors
+ * must not be pre-multiplied and the result is a non pre-multiplied color.
+ *
+ * If the two colors have different color spaces, the color in the right-hand part
+ * of the expression is converted to the color space of the color in left-hand part
+ * of the expression.
+ *
+ * The following example creates a purple color by blending opaque blue with
+ * semi-translucent red:
+ *
+ * ```
+ * val purple = Color.valueOf(0f, 0f, 1f) + Color.valueOf(1f, 0f, 0f, 0.5f)
+ * ```
+ *
+ * @throws IllegalArgumentException if the [color models][android.graphics.Color.getModel]
+ *                                  of the colors do not match
+ */
+@RequiresApi(26)
+operator fun Color.plus(c: Color): Color = ColorUtils.compositeColors(c, this)
+
+/**
+ * Return the alpha component of a color int. This is equivalent to calling:
+ * ```
+ * Color.alpha(myInt)
+ * ```
+ */
+inline val @receiver:ColorInt Int.alpha get() = (this shr 24) and 0xff
+
+/**
+ * Return the red component of a color int. This is equivalent to calling:
+ * ```
+ * Color.red(myInt)
+ * ```
+ */
+inline val @receiver:ColorInt Int.red get() = (this shr 16) and 0xff
+
+/**
+ * Return the green component of a color int. This is equivalent to calling:
+ * ```
+ * Color.green(myInt)
+ * ```
+ */
+inline val @receiver:ColorInt Int.green get() = (this shr 8) and 0xff
+
+/**
+ * Return the blue component of a color int. This is equivalent to calling:
+ * ```
+ * Color.blue(myInt)
+ * ```
+ */
+inline val @receiver:ColorInt Int.blue get() = this and 0xff
+
+/**
+ * Return the alpha component of a color int. This is equivalent to calling:
+ * ```
+ * Color.alpha(myInt)
+ * ```
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (alpha, red, green, blue) = myColor
+ * ```
+ */
+inline operator fun @receiver:ColorInt Int.component1() = (this shr 24) and 0xff
+
+/**
+ * Return the red component of a color int. This is equivalent to calling:
+ * ```
+ * Color.red(myInt)
+ * ```
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (alpha, red, green, blue) = myColor
+ * ```
+ */
+inline operator fun @receiver:ColorInt Int.component2() = (this shr 16) and 0xff
+
+/**
+ * Return the green component of a color int. This is equivalent to calling:
+ * ```
+ * Color.green(myInt)
+ * ```
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (alpha, red, green, blue) = myColor
+ * ```
+ */
+inline operator fun @receiver:ColorInt Int.component3() = (this shr 8) and 0xff
+
+/**
+ * Return the blue component of a color int. This is equivalent to calling:
+ * ```
+ * Color.blue(myInt)
+ * ```
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (alpha, red, green, blue) = myColor
+ * ```
+ */
+inline operator fun @receiver:ColorInt Int.component4() = this and 0xff
+
+/**
+ * Returns the relative luminance of a color int, assuming sRGB encoding.
+ * Based on the formula for relative luminance defined in WCAG 2.0,
+ * W3C Recommendation 11 December 2008.
+ */
+@get:RequiresApi(26)
+inline val @receiver:ColorInt Int.luminance get() = Color.luminance(this)
+
+/**
+ * Creates a new [Color] instance from a color int. The resulting color
+ * is in the [sRGB][android.graphics.ColorSpace.Named.SRGB] color space.
+ */
+@RequiresApi(26)
+inline fun @receiver:ColorInt Int.toColor(): Color = Color.valueOf(this)
+
+/**
+ * Converts the specified ARGB [color int][Color] to an RGBA [color long][Color]
+ * in the [sRGB][android.graphics.ColorSpace.Named.SRGB] color space.
+ */
+@RequiresApi(26)
+@ColorLong
+inline fun @receiver:ColorInt Int.toColorLong() = Color.pack(this)
+
+/**
+ * Returns the first component of the color. For instance, when the color model
+ * of the color is [android.graphics.ColorSpace.Model.RGB], the first component
+ * is "red".
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (red, green, blue, alpha) = myColorLong
+ * ```
+ */
+@RequiresApi(26)
+inline operator fun @receiver:ColorLong Long.component1() = Color.red(this)
+
+/**
+ * Returns the second component of the color. For instance, when the color model
+ * of the color is [android.graphics.ColorSpace.Model.RGB], the second component
+ * is "green".
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (red, green, blue, alpha) = myColorLong
+ * ```
+ */
+@RequiresApi(26)
+inline operator fun @receiver:ColorLong Long.component2() = Color.green(this)
+
+/**
+ * Returns the third component of the color. For instance, when the color model
+ * of the color is [android.graphics.ColorSpace.Model.RGB], the third component
+ * is "blue".
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (red, green, blue, alpha) = myColorLong
+ * ```
+ */
+@RequiresApi(26)
+inline operator fun @receiver:ColorLong Long.component3() = Color.blue(this)
+
+/**
+ * Returns the fourth component of the color. For instance, when the color model
+ * of the color is [android.graphics.ColorSpace.Model.RGB], the fourth component
+ * is "alpha".
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (red, green, blue, alpha) = myColorLong
+ * ```
+ */
+@RequiresApi(26)
+inline operator fun @receiver:ColorLong Long.component4() = Color.alpha(this)
+
+/**
+ * Return the alpha component of a color long. This is equivalent to calling:
+ * ```
+ * Color.alpha(myLong)
+ * ```
+ */
+@get:RequiresApi(26)
+inline val @receiver:ColorLong Long.alpha get() = Color.alpha(this)
+
+/**
+ * Return the red component of a color long. This is equivalent to calling:
+ * ```
+ * Color.red(myLong)
+ * ```
+ */
+@get:RequiresApi(26)
+inline val @receiver:ColorLong Long.red get() = Color.red(this)
+
+/**
+ * Return the green component of a color long. This is equivalent to calling:
+ * ```
+ * Color.green(myLong)
+ * ```
+ */
+@get:RequiresApi(26)
+inline val @receiver:ColorLong Long.green get() = Color.green(this)
+
+/**
+ * Return the blue component of a color long. This is equivalent to calling:
+ * ```
+ * Color.blue(myLong)
+ * ```
+ */
+@get:RequiresApi(26)
+inline val @receiver:ColorLong Long.blue get() = Color.blue(this)
+
+/**
+ * Returns the relative luminance of a color. Based on the formula for
+ * relative luminance defined in WCAG 2.0, W3C Recommendation 11 December 2008.
+ */
+@get:RequiresApi(26)
+inline val @receiver:ColorLong Long.luminance get() = Color.luminance(this)
+
+/**
+ * Creates a new [Color] instance from a [color long][Color].
+ */
+@RequiresApi(26)
+inline fun @receiver:ColorLong Long.toColor(): Color = Color.valueOf(this)
+
+/**
+ * Converts the specified [color long][Color] to an ARGB [color int][Color].
+ */
+@RequiresApi(26)
+@ColorInt
+inline fun @receiver:ColorLong Long.toColorInt() = Color.toArgb(this)
+
+/**
+ * Indicates whether the color is in the [sRGB][android.graphics.ColorSpace.Named.SRGB]
+ * color space.
+ */
+@get:RequiresApi(26)
+inline val @receiver:ColorLong Long.isSrgb get() = Color.isSrgb(this)
+
+/**
+ * Indicates whether the color is in a [wide-gamut][android.graphics.ColorSpace] color space.
+ */
+@get:RequiresApi(26)
+inline val @receiver:ColorLong Long.isWideGamut get() = Color.isWideGamut(this)
+
+/**
+ * Returns the color space encoded in the specified color long.
+ */
+@get:RequiresApi(26)
+inline val @receiver:ColorLong Long.colorSpace: ColorSpace get() = Color.colorSpace(this)
+
+/**
+ * Return a corresponding [Int] color of this [String].
+ *
+ * Supported formats are:
+ * ```
+ * #RRGGBB
+ * #AARRGGBB
+ * ```
+ *
+ * The following names are also accepted: "red", "blue", "green", "black", "white",
+ * "gray", "cyan", "magenta", "yellow", "lightgray", "darkgray",
+ * "grey", "lightgrey", "darkgrey", "aqua", "fuchsia", "lime",
+ * "maroon", "navy", "olive", "purple", "silver", "teal".
+ *
+ * @throws IllegalArgumentException if this [String] cannot be parsed.
+ */
+@ColorInt
+inline fun String.toColorInt(): Int = Color.parseColor(this)
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Matrix.kt b/core/ktx/src/main/java/androidx/core/graphics/Matrix.kt
new file mode 100644
index 0000000..5cb0cdf
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Matrix.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.graphics
+
+import android.graphics.Matrix
+
+/**
+ * Multiplies this [Matrix] by another matrix and returns the result as
+ * a new matrix.
+ */
+inline operator fun Matrix.times(m: Matrix) = Matrix(this).apply { preConcat(m) }
+
+/**
+ * Returns the 9 values of this [Matrix] as a new array of floats.
+ */
+inline fun Matrix.values() = FloatArray(9).apply { getValues(this) }
+
+/**
+ * Creates a translation matrix with the translation amounts [tx] and [ty]
+ * respectively on the `x` and `y` axis.
+ */
+fun translationMatrix(tx: Float = 0.0f, ty: Float = 0.0f) = Matrix().apply { setTranslate(tx, ty) }
+
+/**
+ * Creates a scale matrix with the scale factor [sx] and [sy] respectively on the
+ * `x` and `y` axis.
+ */
+fun scaleMatrix(sx: Float = 1.0f, sy: Float = 1.0f) = Matrix().apply { setScale(sx, sy) }
+
+/**
+ * Creates a rotation matrix, defined by a rotation angle in degrees around the pivot
+ * point located at the coordinates ([px], [py]).
+ */
+fun rotationMatrix(degrees: Float, px: Float = 0.0f, py: Float = 0.0f) =
+    Matrix().apply { setRotate(degrees, px, py) }
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Path.kt b/core/ktx/src/main/java/androidx/core/graphics/Path.kt
new file mode 100644
index 0000000..ca70533
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Path.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.graphics
+
+import android.graphics.Path
+import androidx.annotation.RequiresApi
+
+/**
+ * Flattens (or approximate) the [Path] with a series of line segments.
+ *
+ * @param error The acceptable error for a line on the Path. Typically this would be
+ *              0.5 so that the error is less than half a pixel. This value must be
+ *              positive and is set to 0.5 by default.
+ *
+ * @see Path.approximate
+ */
+@RequiresApi(26)
+fun Path.flatten(error: Float = 0.5f): Iterable<PathSegment> = PathUtils.flatten(this, error)
+
+/**
+ * Returns the union of two paths as a new [Path].
+ */
+@RequiresApi(19)
+inline operator fun Path.plus(p: Path): Path {
+    return Path(this).apply {
+        op(p, Path.Op.UNION)
+    }
+}
+
+/**
+ * Returns the difference of two paths as a new [Path].
+ */
+@RequiresApi(19)
+inline operator fun Path.minus(p: Path): Path {
+    return Path(this).apply {
+        op(p, Path.Op.DIFFERENCE)
+    }
+}
+
+/**
+ * Returns the union of two paths as a new [Path].
+ */
+@RequiresApi(19)
+inline infix fun Path.and(p: Path) = this + p
+
+/**
+ * Returns the intersection of two paths as a new [Path].
+ * If the paths do not intersect, returns an empty path.
+ */
+@RequiresApi(19)
+inline infix fun Path.or(p: Path): Path {
+    return Path().apply {
+        op(this@or, p, Path.Op.INTERSECT)
+    }
+}
+
+/**
+ * Returns the union minus the intersection of two paths as a new [Path].
+ */
+@RequiresApi(19)
+inline infix fun Path.xor(p: Path): Path {
+    return Path(this).apply {
+        op(p, Path.Op.XOR)
+    }
+}
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Picture.kt b/core/ktx/src/main/java/androidx/core/graphics/Picture.kt
new file mode 100644
index 0000000..d1e8c99
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Picture.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 20188 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.
+ */
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.graphics
+
+import android.graphics.Canvas
+import android.graphics.Picture
+
+/**
+ * Creates a new [Canvas] to record commands in this [Picture], executes the specified
+ * [block] on the newly created canvas and returns this [Picture]. Example:
+ *
+ * ```
+ * return myPicture.record(1280, 720) {
+ *    drawLine(…)
+ *    translate(…)
+ *    drawRect(…)
+ * }
+ * ```
+ */
+inline fun Picture.record(width: Int, height: Int, block: Canvas.() -> Unit): Picture {
+    val c = beginRecording(width, height)
+    try {
+        c.block()
+    } finally {
+        endRecording()
+    }
+    return this
+}
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Point.kt b/core/ktx/src/main/java/androidx/core/graphics/Point.kt
new file mode 100644
index 0000000..b6e35e6
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Point.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.graphics
+
+import android.graphics.Point
+import android.graphics.PointF
+
+/**
+ * Returns the x coordinate of this point.
+ *
+ * This method allows to use destructuring declarations when working with points,
+ * for example:
+ * ```
+ * val (x, y) = myPoint
+ * ```
+ */
+inline operator fun Point.component1() = this.x
+
+/**
+ * Returns the y coordinate of this point.
+ *
+ * This method allows to use destructuring declarations when working with points,
+ * for example:
+ * ```
+ * val (x, y) = myPoint
+ * ```
+ */
+inline operator fun Point.component2() = this.y
+
+/**
+ * Returns the x coordinate of this point.
+ *
+ * This method allows to use destructuring declarations when working with points,
+ * for example:
+ * ```
+ * val (x, y) = myPoint
+ * ```
+ */
+inline operator fun PointF.component1() = this.x
+
+/**
+ * Returns the y coordinate of this point.
+ *
+ * This method allows to use destructuring declarations when working with points,
+ * for example:
+ * ```
+ * val (x, y) = myPoint
+ * ```
+ */
+inline operator fun PointF.component2() = this.y
+
+/**
+ * Offsets this point by the specified point and returns the result as a new point.
+ */
+inline operator fun Point.plus(p: Point): Point {
+    return Point(x, y).apply {
+        offset(p.x, p.y)
+    }
+}
+
+/**
+ * Offsets this point by the specified point and returns the result as a new point.
+ */
+inline operator fun PointF.plus(p: PointF): PointF {
+    return PointF(x, y).apply {
+        offset(p.x, p.y)
+    }
+}
+
+/**
+ * Offsets this point by the specified amount on both X and Y axis and returns the
+ * result as a new point.
+ */
+inline operator fun Point.plus(xy: Int): Point {
+    return Point(x, y).apply {
+        offset(xy, xy)
+    }
+}
+
+/**
+ * Offsets this point by the specified amount on both X and Y axis and returns the
+ * result as a new point.
+ */
+inline operator fun PointF.plus(xy: Float): PointF {
+    return PointF(x, y).apply {
+        offset(xy, xy)
+    }
+}
+
+/**
+ * Offsets this point by the negation of the specified point and returns the result
+ * as a new point.
+ */
+inline operator fun Point.minus(p: Point): Point {
+    return Point(x, y).apply {
+        offset(-p.x, -p.y)
+    }
+}
+
+/**
+ * Offsets this point by the negation of the specified point and returns the result
+ * as a new point.
+ */
+inline operator fun PointF.minus(p: PointF): PointF {
+    return PointF(x, y).apply {
+        offset(-p.x, -p.y)
+    }
+}
+
+/**
+ * Offsets this point by the negation of the specified amount on both X and Y axis and
+ * returns the result as a new point.
+ */
+inline operator fun Point.minus(xy: Int): Point {
+    return Point(x, y).apply {
+        offset(-xy, -xy)
+    }
+}
+
+/**
+ * Offsets this point by the negation of the specified amount on both X and Y axis and
+ * returns the result as a new point.
+ */
+inline operator fun PointF.minus(xy: Float): PointF {
+    return PointF(x, y).apply {
+        offset(-xy, -xy)
+    }
+}
+
+/**
+ * Returns a new point representing the negation of this point.
+ */
+inline operator fun Point.unaryMinus() = Point(-x, -y)
+
+/**
+ * Returns a new point representing the negation of this point.
+ */
+inline operator fun PointF.unaryMinus() = PointF(-x, -y)
+
+/**
+ * Returns a [PointF] representation of this point.
+ */
+inline fun Point.toPointF() = PointF(this)
+
+/**
+ * Returns a [Point] representation of this point.
+ */
+inline fun PointF.toPoint() = Point(x.toInt(), y.toInt())
diff --git a/core/ktx/src/main/java/androidx/core/graphics/PorterDuff.kt b/core/ktx/src/main/java/androidx/core/graphics/PorterDuff.kt
new file mode 100644
index 0000000..a34d6f9
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/PorterDuff.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.graphics
+
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.PorterDuffXfermode
+
+/**
+ * Creates a new [PorterDuffXfermode] that uses this [PorterDuff.Mode] as the
+ * alpha compositing or blending mode.
+ */
+inline fun PorterDuff.Mode.toXfermode() = PorterDuffXfermode(this)
+
+/**
+ * Creates a new [PorterDuffColorFilter] that uses this [PorterDuff.Mode] as the
+ * alpha compositing or blending mode, and the specified [color].
+ */
+inline fun PorterDuff.Mode.toColorFilter(color: Int) = PorterDuffColorFilter(color, this)
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Rect.kt b/core/ktx/src/main/java/androidx/core/graphics/Rect.kt
new file mode 100644
index 0000000..960c0c5
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Rect.kt
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.graphics
+
+import android.annotation.SuppressLint
+import android.graphics.Matrix
+import android.graphics.Point
+import android.graphics.PointF
+import android.graphics.Rect
+import android.graphics.RectF
+import android.graphics.Region
+
+/**
+ * Returns "left", the first component of the rectangle.
+ *
+ * This method allows to use destructuring declarations when working with rectangles,
+ * for example:
+ * ```
+ * val (left, top, right, bottom) = myRectangle
+ * ```
+ */
+inline operator fun Rect.component1() = this.left
+
+/**
+ * Returns "top", the second component of the rectangle.
+ *
+ * This method allows to use destructuring declarations when working with rectangles,
+ * for example:
+ * ```
+ * val (left, top, right, bottom) = myRectangle
+ * ```
+ */
+inline operator fun Rect.component2() = this.top
+
+/**
+ * Returns "right", the third component of the rectangle.
+ *
+ * This method allows to use destructuring declarations when working with rectangles,
+ * for example:
+ * ```
+ * val (left, top, right, bottom) = myRectangle
+ * ```
+ */
+inline operator fun Rect.component3() = this.right
+
+/**
+ * Returns "bottom", the fourth component of the rectangle.
+ *
+ * This method allows to use destructuring declarations when working with rectangles,
+ * for example:
+ * ```
+ * val (left, top, right, bottom) = myRectangle
+ * ```
+ */
+inline operator fun Rect.component4() = this.bottom
+
+/**
+ * Returns "left", the first component of the rectangle.
+ *
+ * This method allows to use destructuring declarations when working with rectangles,
+ * for example:
+ * ```
+ * val (left, top, right, bottom) = myRectangle
+ * ```
+ */
+inline operator fun RectF.component1() = this.left
+
+/**
+ * Returns "top", the second component of the rectangle.
+ *
+ * This method allows to use destructuring declarations when working with rectangles,
+ * for example:
+ * ```
+ * val (left, top, right, bottom) = myRectangle
+ * ```
+ */
+inline operator fun RectF.component2() = this.top
+
+/**
+ * Returns "right", the third component of the rectangle.
+ *
+ * This method allows to use destructuring declarations when working with rectangles,
+ * for example:
+ * ```
+ * val (left, top, right, bottom) = myRectangle
+ * ```
+ */
+inline operator fun RectF.component3() = this.right
+
+/**
+ * Returns "bottom", the fourth component of the rectangle.
+ *
+ * This method allows to use destructuring declarations when working with rectangles,
+ * for example:
+ * ```
+ * val (left, top, right, bottom) = myRectangle
+ * ```
+ */
+inline operator fun RectF.component4() = this.bottom
+
+/**
+ * Performs the union of this rectangle and the specified rectangle and returns
+ * the result as a new rectangle.
+ */
+inline operator fun Rect.plus(r: Rect): Rect {
+    return Rect(this).apply {
+        union(r)
+    }
+}
+
+/**
+ * Performs the union of this rectangle and the specified rectangle and returns
+ * the result as a new rectangle.
+ */
+inline operator fun RectF.plus(r: RectF): RectF {
+    return RectF(this).apply {
+        union(r)
+    }
+}
+
+/**
+ * Returns a new rectangle representing this rectangle offset by the specified
+ * amount on both X and Y axis.
+ */
+inline operator fun Rect.plus(xy: Int): Rect {
+    return Rect(this).apply {
+        offset(xy, xy)
+    }
+}
+
+/**
+ * Returns a new rectangle representing this rectangle offset by the specified
+ * amount on both X and Y axis.
+ */
+inline operator fun RectF.plus(xy: Float): RectF {
+    return RectF(this).apply {
+        offset(xy, xy)
+    }
+}
+
+/**
+ * Returns a new rectangle representing this rectangle offset by the specified
+ * point.
+ */
+inline operator fun Rect.plus(xy: Point): Rect {
+    return Rect(this).apply {
+        offset(xy.x, xy.y)
+    }
+}
+
+/**
+ * Returns a new rectangle representing this rectangle offset by the specified
+ * point.
+ */
+inline operator fun RectF.plus(xy: PointF): RectF {
+    return RectF(this).apply {
+        offset(xy.x, xy.y)
+    }
+}
+
+/**
+ * Returns the difference of this rectangle and the specified rectangle as a new region.
+ */
+inline operator fun Rect.minus(r: Rect): Region {
+    return Region(this).apply {
+        op(r, Region.Op.DIFFERENCE)
+    }
+}
+
+/**
+ * Returns the difference of this rectangle and the specified rectangle as a new region.
+ * This rectangle is first converted to a [Rect] using [RectF.toRect].
+ */
+inline operator fun RectF.minus(r: RectF): Region {
+    return Region(this.toRect()).apply {
+        op(r.toRect(), Region.Op.DIFFERENCE)
+    }
+}
+
+/**
+ * Returns a new rectangle representing this rectangle offset by the negation
+ * of the specified amount on both X and Y axis.
+ */
+inline operator fun Rect.minus(xy: Int): Rect {
+    return Rect(this).apply {
+        offset(-xy, -xy)
+    }
+}
+
+/**
+ * Returns a new rectangle representing this rectangle offset by the negation
+ * of the specified amount on both X and Y axis.
+ */
+inline operator fun RectF.minus(xy: Float): RectF {
+    return RectF(this).apply {
+        offset(-xy, -xy)
+    }
+}
+
+/**
+ * Returns a new rectangle representing this rectangle offset by the negation of
+ * the specified point.
+ */
+inline operator fun Rect.minus(xy: Point): Rect {
+    return Rect(this).apply {
+        offset(-xy.x, -xy.y)
+    }
+}
+
+/**
+ * Returns a new rectangle representing this rectangle offset by the negation of
+ * the specified point.
+ */
+inline operator fun RectF.minus(xy: PointF): RectF {
+    return RectF(this).apply {
+        offset(-xy.x, -xy.y)
+    }
+}
+
+/**
+ * Returns the union of two rectangles as a new rectangle.
+ */
+inline infix fun Rect.and(r: Rect) = this + r
+
+/**
+ * Returns the union of two rectangles as a new rectangle.
+ */
+inline infix fun RectF.and(r: RectF) = this + r
+
+/**
+ * Returns the intersection of two rectangles as a new rectangle.
+ * If the rectangles do not intersect, returns a copy of the left hand side
+ * rectangle.
+ */
+@SuppressLint("CheckResult")
+inline infix fun Rect.or(r: Rect): Rect {
+    return Rect(this).apply {
+        intersect(r)
+    }
+}
+
+/**
+ * Returns the intersection of two rectangles as a new rectangle.
+ * If the rectangles do not intersect, returns a copy of the left hand side
+ * rectangle.
+ */
+@SuppressLint("CheckResult")
+inline infix fun RectF.or(r: RectF): RectF {
+    return RectF(this).apply {
+        intersect(r)
+    }
+}
+
+/**
+ * Returns the union minus the intersection of two rectangles as a new region.
+ */
+inline infix fun Rect.xor(r: Rect): Region {
+    return Region(this).apply {
+        op(r, Region.Op.XOR)
+    }
+}
+
+/**
+ * Returns the union minus the intersection of two rectangles as a new region.
+ * The two rectangles are first converted to [Rect] using [RectF.toRect].
+ */
+inline infix fun RectF.xor(r: RectF): Region {
+    return Region(this.toRect()).apply {
+        op(r.toRect(), Region.Op.XOR)
+    }
+}
+
+/**
+ * Returns true if the specified point is inside the rectangle.
+ * The left and top are considered to be inside, while the right and bottom are not.
+ * This means that for a point to be contained: left <= x < right and top <= y < bottom.
+ * An empty rectangle never contains any point.
+ */
+inline operator fun Rect.contains(p: Point) = contains(p.x, p.y)
+
+/**
+ * Returns true if the specified point is inside the rectangle.
+ * The left and top are considered to be inside, while the right and bottom are not.
+ * This means that for a point to be contained: left <= x < right and top <= y < bottom.
+ * An empty rectangle never contains any point.
+ */
+inline operator fun RectF.contains(p: PointF) = contains(p.x, p.y)
+
+/**
+ * Returns a [RectF] representation of this rectangle.
+ */
+inline fun Rect.toRectF(): RectF = RectF(this)
+
+/**
+ * Returns a [Rect] representation of this rectangle. The resulting rect will be sized such
+ * that this rect can fit within it.
+ */
+inline fun RectF.toRect(): Rect {
+    val r = Rect()
+    roundOut(r)
+    return r
+}
+
+/**
+ * Returns a [Region] representation of this rectangle.
+ */
+inline fun Rect.toRegion() = Region(this)
+
+/**
+ * Returns a [Region] representation of this rectangle. The resulting rect will be sized such
+ * that this rect can fit within it.
+ */
+inline fun RectF.toRegion() = Region(this.toRect())
+
+/**
+ * Transform this rectangle in place using the supplied [Matrix] and returns
+ * this rectangle.
+ */
+inline fun RectF.transform(m: Matrix) = apply { m.mapRect(this@transform) }
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Region.kt b/core/ktx/src/main/java/androidx/core/graphics/Region.kt
new file mode 100644
index 0000000..4fc5179
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Region.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.graphics
+
+import android.graphics.Point
+import android.graphics.Rect
+import android.graphics.Region
+import android.graphics.RegionIterator
+
+/**
+ * Return true if the region contains the specified [Point].
+ */
+inline operator fun Region.contains(p: Point) = contains(p.x, p.y)
+
+/**
+ * Return the union of this region and the specified [Rect] as a new region.
+ */
+inline operator fun Region.plus(r: Rect): Region {
+    return Region(this).apply {
+        union(r)
+    }
+}
+
+/**
+ * Return the union of this region and the specified region as a new region.
+ */
+inline operator fun Region.plus(r: Region): Region {
+    return Region(this).apply {
+        op(r, Region.Op.UNION)
+    }
+}
+
+/**
+ * Return the difference of this region and the specified [Rect] as a new region.
+ */
+inline operator fun Region.minus(r: Rect): Region {
+    return Region(this).apply {
+        op(r, Region.Op.DIFFERENCE)
+    }
+}
+
+/**
+ * Return the difference of this region and the specified region as a new region.
+ */
+inline operator fun Region.minus(r: Region): Region {
+    return Region(this).apply {
+        op(r, Region.Op.DIFFERENCE)
+    }
+}
+
+/**
+ * Returns the negation of this region as a new region.
+ */
+inline operator fun Region.unaryMinus(): Region {
+    return Region(bounds).apply {
+        op(this@unaryMinus, Region.Op.DIFFERENCE)
+    }
+}
+
+/**
+ * Returns the negation of this region as a new region.
+ */
+inline operator fun Region.not() = -this
+
+/**
+ * Return the union of this region and the specified [Rect] as a new region.
+ */
+inline infix fun Region.and(r: Rect) = this + r
+
+/**
+ * Return the union of this region and the specified region as a new region.
+ */
+inline infix fun Region.and(r: Region) = this + r
+
+/**
+ * Return the intersection of this region and the specified [Rect] as a new region.
+ */
+inline infix fun Region.or(r: Rect): Region {
+    return Region(this).apply {
+        op(r, Region.Op.INTERSECT)
+    }
+}
+
+/**
+ * Return the intersection of this region and the specified region as a new region.
+ */
+inline infix fun Region.or(r: Region): Region {
+    return Region(this).apply {
+        op(r, Region.Op.INTERSECT)
+    }
+}
+
+/**
+ * Return the union minus the intersection of this region and the specified [Rect]
+ * as a new region.
+ */
+inline infix fun Region.xor(r: Rect): Region {
+    return Region(this).apply {
+        op(r, Region.Op.XOR)
+    }
+}
+
+/**
+ * Return the union minus the intersection of this region and the specified region
+ * as a new region.
+ */
+inline infix fun Region.xor(r: Region): Region {
+    return Region(this).apply {
+        op(r, Region.Op.XOR)
+    }
+}
+
+/** Performs the given action on each rect in this region. */
+inline fun Region.forEach(action: (rect: Rect) -> Unit) {
+    val iterator = RegionIterator(this)
+    while (true) {
+        val r = Rect()
+        if (!iterator.next(r)) {
+            break
+        }
+        action(r)
+    }
+}
+
+/** Returns an [Iterator] over the rects in this region. */
+operator fun Region.iterator() = object : Iterator<Rect> {
+    private val iterator = RegionIterator(this@iterator)
+    private val rect = Rect()
+    private var hasMore = iterator.next(rect)
+
+    override fun hasNext() = hasMore
+
+    override fun next(): Rect {
+        if (hasMore) {
+            val r = Rect(rect)
+            hasMore = iterator.next(rect)
+            return r
+        }
+        throw IndexOutOfBoundsException()
+    }
+}
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Shader.kt b/core/ktx/src/main/java/androidx/core/graphics/Shader.kt
new file mode 100644
index 0000000..0fb93cb
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Shader.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Matrix
+import android.graphics.Shader
+
+/**
+ * Wrap the specified [block] in calls to [Shader.getLocalMatrix] and [Shader.setLocalMatrix].
+ */
+inline fun Shader.transform(block: Matrix.() -> Unit) {
+    val matrix = Matrix()
+    getLocalMatrix(matrix)
+    block(matrix)
+    setLocalMatrix(matrix)
+}
diff --git a/core/ktx/src/main/java/androidx/core/graphics/drawable/BitmapDrawable.kt b/core/ktx/src/main/java/androidx/core/graphics/drawable/BitmapDrawable.kt
new file mode 100644
index 0000000..4a96aab
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/drawable/BitmapDrawable.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE", "WRONG_ANNOTATION_TARGET_WITH_USE_SITE_TARGET_ON_TYPE")
+
+package androidx.core.graphics.drawable
+
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
+
+/** Create a [BitmapDrawable] from this [Bitmap]. */
+inline fun Bitmap.toDrawable(resources: Resources) = BitmapDrawable(resources, this)
diff --git a/core/ktx/src/main/java/androidx/core/graphics/drawable/ColorDrawable.kt b/core/ktx/src/main/java/androidx/core/graphics/drawable/ColorDrawable.kt
new file mode 100644
index 0000000..bef04df
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/drawable/ColorDrawable.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE", "WRONG_ANNOTATION_TARGET_WITH_USE_SITE_TARGET_ON_TYPE")
+
+package androidx.core.graphics.drawable
+
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import androidx.annotation.ColorInt
+import androidx.annotation.RequiresApi
+
+/** Create a [ColorDrawable] from this color value. */
+inline fun @receiver:ColorInt Int.toDrawable() = ColorDrawable(this)
+
+/** Create a [ColorDrawable] from this [Color] (via [Color.toArgb]). */
+@RequiresApi(26)
+inline fun Color.toDrawable() = ColorDrawable(toArgb())
diff --git a/core/ktx/src/main/java/androidx/core/graphics/drawable/Drawable.kt b/core/ktx/src/main/java/androidx/core/graphics/drawable/Drawable.kt
new file mode 100644
index 0000000..9e63bbd
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/drawable/Drawable.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+
+package androidx.core.graphics.drawable
+
+import android.graphics.Bitmap
+import android.graphics.Bitmap.Config
+import android.graphics.Canvas
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import androidx.annotation.Px
+import androidx.core.graphics.component1
+import androidx.core.graphics.component2
+import androidx.core.graphics.component3
+import androidx.core.graphics.component4
+
+/**
+ * Return a [Bitmap] representation of this [Drawable].
+ *
+ * If this instance is a [BitmapDrawable] and the [width], [height], and [config] match, the
+ * underlying [Bitmap] instance will be returned directly. If any of those three properties differ
+ * then a new [Bitmap] is created. For all other [Drawable] types, a new [Bitmap] is created.
+ *
+ * @param width Width of the desired bitmap. Defaults to [Drawable.getIntrinsicWidth].
+ * @param height Height of the desired bitmap. Defaults to [Drawable.getIntrinsicHeight].
+ * @param config Bitmap config of the desired bitmap. Null attempts to use the native config, if
+ * any. Defaults to [Config.ARGB_8888] otherwise.
+ */
+fun Drawable.toBitmap(
+    @Px width: Int = intrinsicWidth,
+    @Px height: Int = intrinsicHeight,
+    config: Config? = null
+): Bitmap {
+    if (this is BitmapDrawable) {
+        if (config == null || bitmap.config == config) {
+            // Fast-path to return original. Bitmap.createScaledBitmap will do this check, but it
+            // involves allocation and two jumps into native code so we perform the check ourselves.
+            if (width == intrinsicWidth && height == intrinsicHeight) {
+                return bitmap
+            }
+            return Bitmap.createScaledBitmap(bitmap, width, height, true)
+        }
+    }
+
+    val (oldLeft, oldTop, oldRight, oldBottom) = bounds
+
+    val bitmap = Bitmap.createBitmap(width, height, config ?: Config.ARGB_8888)
+    setBounds(0, 0, width, height)
+    draw(Canvas(bitmap))
+
+    setBounds(oldLeft, oldTop, oldRight, oldBottom)
+    return bitmap
+}
+
+/**
+ * Updates this drawable's bounds. This version of the method allows using named parameters
+ * to just set one or more axes.
+ *
+ * @see Drawable.setBounds
+ */
+fun Drawable.updateBounds(
+    @Px left: Int = bounds.left,
+    @Px top: Int = bounds.top,
+    @Px right: Int = bounds.right,
+    @Px bottom: Int = bounds.bottom
+) {
+    setBounds(left, top, right, bottom)
+}
diff --git a/core/ktx/src/main/java/androidx/core/graphics/drawable/Icon.kt b/core/ktx/src/main/java/androidx/core/graphics/drawable/Icon.kt
new file mode 100644
index 0000000..a9077e2
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/drawable/Icon.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.graphics.drawable
+
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.net.Uri
+import androidx.annotation.RequiresApi
+
+/**
+ * Create an [Icon] from this adaptive [Bitmap].
+ *
+ * @see Icon.createWithAdaptiveBitmap
+ */
+@RequiresApi(26)
+inline fun Bitmap.toAdaptiveIcon(): Icon = Icon.createWithAdaptiveBitmap(this)
+
+/**
+ * Create an [Icon] from this [Bitmap].
+ *
+ * @see Icon.createWithBitmap
+ */
+@RequiresApi(26)
+inline fun Bitmap.toIcon(): Icon = Icon.createWithBitmap(this)
+
+/**
+ * Create an [Icon] from this [Uri].
+ *
+ * @see Icon.createWithContentUri
+ */
+@RequiresApi(26)
+inline fun Uri.toIcon(): Icon = Icon.createWithContentUri(this)
+
+/**
+ * Create an [Icon] from this [ByteArray].
+ *
+ * @see Icon.createWithData
+ */
+@RequiresApi(26)
+inline fun ByteArray.toIcon(): Icon = Icon.createWithData(this, 0, size)
diff --git a/core/ktx/src/main/java/androidx/core/location/Location.kt b/core/ktx/src/main/java/androidx/core/location/Location.kt
new file mode 100644
index 0000000..fe48322
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/location/Location.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.location
+
+import android.location.Location
+
+/**
+ * Returns the latitude of this [Location].
+ *
+ * This method allows to use destructuring declarations when working with [Location],
+ * for example:
+ * ```
+ * val (lat, lon) = myLocation
+ * ```
+ */
+inline operator fun Location.component1() = this.latitude
+
+/**
+ * Returns the longitude of this [Location].
+ *
+ * This method allows to use destructuring declarations when working with [Location],
+ * for example:
+ * ```
+ * val (lat, lon) = myLocation
+ * ```
+ */
+inline operator fun Location.component2() = this.longitude
diff --git a/core/ktx/src/main/java/androidx/core/net/Uri.kt b/core/ktx/src/main/java/androidx/core/net/Uri.kt
new file mode 100644
index 0000000..a5d9f1d
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/net/Uri.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.net
+
+import android.net.Uri
+import java.io.File
+
+/**
+ * Creates a Uri from the given encoded URI string.
+ *
+ * @see Uri.parse
+ */
+inline fun String.toUri(): Uri = Uri.parse(this)
+
+/**
+ * Creates a Uri from the given file.
+ *
+ * @see Uri.fromFile
+ */
+inline fun File.toUri(): Uri = Uri.fromFile(this)
+
+/** Creates a [File] from the given [Uri]. */
+inline fun Uri.toFile(): File = File(path)
diff --git a/core/ktx/src/main/java/androidx/core/os/Bundle.kt b/core/ktx/src/main/java/androidx/core/os/Bundle.kt
new file mode 100644
index 0000000..53da45d
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/os/Bundle.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+
+package androidx.core.os
+
+import android.os.Binder
+import android.os.Build
+import android.os.Bundle
+import android.os.Parcelable
+import android.util.Size
+import android.util.SizeF
+import java.io.Serializable
+
+/**
+ * Returns a new [Bundle] with the given key/value pairs as elements.
+ *
+ * @throws IllegalArgumentException When a value is not a supported type of [Bundle].
+ */
+fun bundleOf(vararg pairs: Pair<String, Any?>) = Bundle(pairs.size).apply {
+    for ((key, value) in pairs) {
+        when (value) {
+            null -> putString(key, null) // Any nullable type will suffice.
+
+            // Scalars
+            is Boolean -> putBoolean(key, value)
+            is Byte -> putByte(key, value)
+            is Char -> putChar(key, value)
+            is Double -> putDouble(key, value)
+            is Float -> putFloat(key, value)
+            is Int -> putInt(key, value)
+            is Long -> putLong(key, value)
+            is Short -> putShort(key, value)
+
+            // References
+            is Bundle -> putBundle(key, value)
+            is CharSequence -> putCharSequence(key, value)
+            is Parcelable -> putParcelable(key, value)
+
+            // Scalar arrays
+            is BooleanArray -> putBooleanArray(key, value)
+            is ByteArray -> putByteArray(key, value)
+            is CharArray -> putCharArray(key, value)
+            is DoubleArray -> putDoubleArray(key, value)
+            is FloatArray -> putFloatArray(key, value)
+            is IntArray -> putIntArray(key, value)
+            is LongArray -> putLongArray(key, value)
+            is ShortArray -> putShortArray(key, value)
+
+            // Reference arrays
+            is Array<*> -> {
+                val componentType = value::class.java.componentType
+                @Suppress("UNCHECKED_CAST") // Checked by reflection.
+                when {
+                    Parcelable::class.java.isAssignableFrom(componentType) -> {
+                        putParcelableArray(key, value as Array<Parcelable>)
+                    }
+                    String::class.java.isAssignableFrom(componentType) -> {
+                        putStringArray(key, value as Array<String>)
+                    }
+                    CharSequence::class.java.isAssignableFrom(componentType) -> {
+                        putCharSequenceArray(key, value as Array<CharSequence>)
+                    }
+                    Serializable::class.java.isAssignableFrom(componentType) -> {
+                        putSerializable(key, value)
+                    }
+                    else -> {
+                        val valueType = componentType.canonicalName
+                        throw IllegalArgumentException(
+                            "Illegal value array type $valueType for key \"$key\"")
+                    }
+                }
+            }
+
+            // Last resort. Also we must check this after Array<*> as all arrays are serializable.
+            is Serializable -> putSerializable(key, value)
+
+            else -> {
+                if (Build.VERSION.SDK_INT >= 18 && value is Binder) {
+                    putBinder(key, value)
+                } else if (Build.VERSION.SDK_INT >= 21 && value is Size) {
+                    putSize(key, value)
+                } else if (Build.VERSION.SDK_INT >= 21 && value is SizeF) {
+                    putSizeF(key, value)
+                } else {
+                    val valueType = value.javaClass.canonicalName
+                    throw IllegalArgumentException("Illegal value type $valueType for key \"$key\"")
+                }
+            }
+        }
+    }
+}
diff --git a/core/ktx/src/main/java/androidx/core/os/Handler.kt b/core/ktx/src/main/java/androidx/core/os/Handler.kt
new file mode 100644
index 0000000..de09a9b
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/os/Handler.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.os
+
+import android.os.Handler
+
+/**
+ * Version of [Handler.postDelayed] which re-orders the parameters, allowing the action to be
+ * placed outside of parentheses.
+ *
+ * ```
+ * handler.postDelayed(200) {
+ *     doSomething()
+ * }
+ * ```
+ *
+ * @return the created Runnable
+ */
+inline fun Handler.postDelayed(
+    delayInMillis: Long,
+    token: Any? = null,
+    crossinline action: () -> Unit
+): Runnable {
+    val runnable = Runnable { action() }
+    if (token == null) {
+        postDelayed(runnable, delayInMillis)
+    } else {
+        HandlerCompat.postDelayed(this, runnable, token, delayInMillis)
+    }
+    return runnable
+}
+
+/**
+ * Version of [Handler.postAtTime] which re-orders the parameters, allowing the action to be
+ * placed outside of parentheses.
+ *
+ * ```
+ * handler.postAtTime(200) {
+ *     doSomething()
+ * }
+ * ```
+ *
+ * @param token An optional object with which the posted message will be associated.
+ * @return the created Runnable
+ */
+inline fun Handler.postAtTime(
+    uptimeMillis: Long,
+    token: Any? = null,
+    crossinline action: () -> Unit
+): Runnable {
+    val runnable = Runnable { action() }
+    postAtTime(runnable, token, uptimeMillis)
+    return runnable
+}
diff --git a/core/ktx/src/main/java/androidx/core/os/PersistableBundle.kt b/core/ktx/src/main/java/androidx/core/os/PersistableBundle.kt
new file mode 100644
index 0000000..261e72b
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/os/PersistableBundle.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+
+package androidx.core.os
+
+import android.os.Build
+import android.os.PersistableBundle
+import androidx.annotation.RequiresApi
+
+/**
+ * Returns a new [PersistableBundle] with the given key/value pairs as elements.
+ *
+ * @throws IllegalArgumentException When a value is not a supported type of [PersistableBundle].
+ */
+@RequiresApi(21)
+fun persistableBundleOf(vararg pairs: Pair<String, Any?>) = PersistableBundle(pairs.size).apply {
+    for ((key, value) in pairs) {
+        when (value) {
+            null -> putString(key, null) // Any nullable type will suffice.
+
+            // Scalars
+            is Boolean -> {
+                if (Build.VERSION.SDK_INT >= 22) {
+                    putBoolean(key, value)
+                } else {
+                    throw IllegalArgumentException("Illegal value type boolean for key \"$key\"")
+                }
+            }
+            is Double -> putDouble(key, value)
+            is Int -> putInt(key, value)
+            is Long -> putLong(key, value)
+
+            // References
+            is String -> putString(key, value)
+
+            // Scalar arrays
+            is BooleanArray -> {
+                if (Build.VERSION.SDK_INT >= 22) {
+                    putBooleanArray(key, value)
+                } else {
+                    throw IllegalArgumentException("Illegal value type boolean[] for key \"$key\"")
+                }
+            }
+            is DoubleArray -> putDoubleArray(key, value)
+            is IntArray -> putIntArray(key, value)
+            is LongArray -> putLongArray(key, value)
+
+            // Reference arrays
+            is Array<*> -> {
+                val componentType = value::class.java.componentType
+                @Suppress("UNCHECKED_CAST") // Checked by reflection.
+                when {
+                    String::class.java.isAssignableFrom(componentType) -> {
+                        putStringArray(key, value as Array<String>)
+                    }
+                    else -> {
+                        val valueType = componentType.canonicalName
+                        throw IllegalArgumentException(
+                            "Illegal value array type $valueType for key \"$key\"")
+                    }
+                }
+            }
+
+            else -> {
+                val valueType = value.javaClass.canonicalName
+                throw IllegalArgumentException("Illegal value type $valueType for key \"$key\"")
+            }
+        }
+    }
+}
diff --git a/core/ktx/src/main/java/androidx/core/os/Trace.kt b/core/ktx/src/main/java/androidx/core/os/Trace.kt
new file mode 100644
index 0000000..43bf85e
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/os/Trace.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.os
+
+import android.os.Trace
+
+/**
+ * Wrap the specified [block] in calls to [Trace.beginSection] (with the supplied [sectionName])
+ * and [Trace.endSection].
+ */
+inline fun <T> trace(sectionName: String, block: () -> T): T {
+    TraceCompat.beginSection(sectionName)
+    try {
+        return block()
+    } finally {
+        TraceCompat.endSection()
+    }
+}
diff --git a/core/ktx/src/main/java/androidx/core/preference/PreferenceGroup.kt b/core/ktx/src/main/java/androidx/core/preference/PreferenceGroup.kt
new file mode 100644
index 0000000..2efab64
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/preference/PreferenceGroup.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.preference
+
+import android.preference.Preference
+import android.preference.PreferenceGroup
+
+/**
+ * Returns the preference with `key`.
+ *
+ * @throws NullPointerException if no preference is found with that key.
+ */
+inline operator fun PreferenceGroup.get(key: CharSequence): Preference = findPreference(key)
+
+/**
+ * Returns the preference at `index`.
+ *
+ * @throws IndexOutOfBoundsException if index is less than 0 or greater than or equal to the count.
+ */
+operator fun PreferenceGroup.get(index: Int): Preference = getPreference(index)
+        ?: throw IndexOutOfBoundsException("Index: $index, Size: $preferenceCount")
+
+/** Returns `true` if `preference` is found in this preference group. */
+operator fun PreferenceGroup.contains(preference: Preference): Boolean {
+    for (index in 0 until preferenceCount) {
+        if (getPreference(index) == preference) {
+            return true
+        }
+    }
+    return false
+}
+
+/** Adds `preference` to this preference group. */
+inline operator fun PreferenceGroup.plusAssign(preference: Preference) {
+    addPreference(preference)
+}
+
+/** Removes `preference` from this preference group. */
+inline operator fun PreferenceGroup.minusAssign(preference: Preference) {
+    removePreference(preference)
+}
+
+/** Returns the number of preferences in this preference group. */
+inline val PreferenceGroup.size: Int get() = preferenceCount
+
+/** Returns true if this preference group contains no preferences. */
+inline fun PreferenceGroup.isEmpty(): Boolean = size == 0
+
+/** Returns true if this preference group contains one or more preferences. */
+inline fun PreferenceGroup.isNotEmpty(): Boolean = size != 0
+
+/** Performs the given action on each preference in this preference group. */
+inline fun PreferenceGroup.forEach(action: (preference: Preference) -> Unit) {
+    for (index in 0 until preferenceCount) {
+        action(get(index))
+    }
+}
+
+/** Performs the given action on each preference in this preference group, providing its sequential index. */
+inline fun PreferenceGroup.forEachIndexed(action: (index: Int, preference: Preference) -> Unit) {
+    for (index in 0 until preferenceCount) {
+        action(index, get(index))
+    }
+}
+
+/** Returns a [MutableIterator] over the preferences in this preference group. */
+operator fun PreferenceGroup.iterator() = object : MutableIterator<Preference> {
+    private var index = 0
+    override fun hasNext() = index < size
+    override fun next() = getPreference(index++) ?: throw IndexOutOfBoundsException()
+    override fun remove() {
+        removePreference(getPreference(--index))
+    }
+}
+
+/** Returns a [Sequence] over the preferences in this preference group. */
+val PreferenceGroup.children: Sequence<Preference>
+    get() = object : Sequence<Preference> {
+        override fun iterator() = this@children.iterator()
+    }
diff --git a/core/ktx/src/main/java/androidx/core/text/CharSequence.kt b/core/ktx/src/main/java/androidx/core/text/CharSequence.kt
new file mode 100644
index 0000000..d41aed2
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/text/CharSequence.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.text
+
+import android.text.TextUtils
+
+/**
+ * Returns whether the given [CharSequence] contains only digits.
+ *
+ * @see TextUtils.isDigitsOnly
+ */
+inline fun CharSequence.isDigitsOnly() = TextUtils.isDigitsOnly(this)
+
+/**
+ * Returns the length that the specified [CharSequence] would have if spaces and ASCII control
+ * characters were trimmed from the start and end, as by [String.trim].
+ *
+ * @see TextUtils.getTrimmedLength
+ */
+inline fun CharSequence.trimmedLength() = TextUtils.getTrimmedLength(this)
diff --git a/core/ktx/src/main/java/androidx/core/text/Html.kt b/core/ktx/src/main/java/androidx/core/text/Html.kt
new file mode 100644
index 0000000..d2636ee
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/text/Html.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+package androidx.core.text
+
+import android.annotation.SuppressLint
+import android.text.Html
+import android.text.Html.FROM_HTML_MODE_LEGACY
+import android.text.Html.ImageGetter
+import android.text.Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE
+import android.text.Html.TagHandler
+import android.text.Spanned
+
+/**
+ * Returns a [Spanned] from parsing this string as HTML.
+ *
+ * @param flags Additional option to set the behavior of the HTML parsing. Default is set to
+ * [Html.FROM_HTML_MODE_LEGACY] which was introduced in API 24.
+ * @param imageGetter Returns displayable styled text from the provided HTML string.
+ * @param tagHandler Notified when HTML tags are encountered a tag the parser does
+ * not know how to interpret.
+ *
+ * @see Html.fromHtml
+ */
+fun String.parseAsHtml(
+    @SuppressLint("InlinedApi") flags: Int = FROM_HTML_MODE_LEGACY,
+    imageGetter: ImageGetter? = null,
+    tagHandler: TagHandler? = null
+): Spanned = HtmlCompat.fromHtml(this, flags, imageGetter, tagHandler)
+
+/**
+ * Returns a string of HTML from the spans in this [Spanned].
+ *
+ * @see Html.toHtml
+ */
+fun Spanned.toHtml(
+    @SuppressLint("InlinedApi") option: Int = TO_HTML_PARAGRAPH_LINES_CONSECUTIVE
+): String = HtmlCompat.toHtml(this, option)
diff --git a/core/ktx/src/main/java/androidx/core/text/SpannableString.kt b/core/ktx/src/main/java/androidx/core/text/SpannableString.kt
new file mode 100644
index 0000000..29aa9c2
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/text/SpannableString.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.text
+
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+
+/**
+ * Returns a new [Spannable] from [CharSequence],
+ * or the source itself if it is already an instance of [SpannableString].
+ */
+inline fun CharSequence.toSpannable(): Spannable = SpannableString.valueOf(this)
+
+/** Adds [span] to the entire text. */
+inline operator fun Spannable.plusAssign(span: Any) =
+    setSpan(span, 0, length, SPAN_INCLUSIVE_EXCLUSIVE)
+
+/** Removes [span] from this text. */
+inline operator fun Spannable.minusAssign(span: Any) = removeSpan(span)
+
+/** Clear all spans from this text. */
+inline fun Spannable.clearSpans() = getSpans<Any>().forEach { removeSpan(it) }
+
+/**
+ * Add [span] to the range [start]&hellip;[end] of the text.
+ *
+ * ```
+ * val s = "Hello, World!".toSpannable()
+ * s[0, 5] = UnderlineSpan()
+ * ```
+ *
+ * Note: The [end] value is exclusive.
+ *
+ * @see Spannable.setSpan
+ */
+inline operator fun Spannable.set(start: Int, end: Int, span: Any) {
+    setSpan(span, start, end, SPAN_INCLUSIVE_EXCLUSIVE)
+}
+
+/**
+ * Add [span] to the [range] of the text.
+ *
+ * ```
+ * val s = "Hello, World!".toSpannable()
+ * s[0..5] = UnderlineSpan()
+ * ```
+ *
+ * Note: The range end value is exclusive.
+ *
+ * @see Spannable.setSpan
+ */
+inline operator fun Spannable.set(range: IntRange, span: Any) {
+    // This looks weird, but endInclusive is just the exact upper value.
+    setSpan(span, range.start, range.endInclusive, SPAN_INCLUSIVE_EXCLUSIVE)
+}
diff --git a/core/ktx/src/main/java/androidx/core/text/SpannableStringBuilder.kt b/core/ktx/src/main/java/androidx/core/text/SpannableStringBuilder.kt
new file mode 100644
index 0000000..e49ccd2
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/text/SpannableStringBuilder.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.text
+
+import android.graphics.Typeface.BOLD
+import android.graphics.Typeface.ITALIC
+import android.text.Spannable.SPAN_INCLUSIVE_EXCLUSIVE
+import android.text.SpannableStringBuilder
+import android.text.SpannedString
+import android.text.style.BackgroundColorSpan
+import android.text.style.ForegroundColorSpan
+import android.text.style.RelativeSizeSpan
+import android.text.style.StrikethroughSpan
+import android.text.style.StyleSpan
+import android.text.style.SubscriptSpan
+import android.text.style.SuperscriptSpan
+import android.text.style.UnderlineSpan
+import androidx.annotation.ColorInt
+
+/**
+ * Builds new string by populating a newly created [SpannableStringBuilder] using the provided
+ * [builderAction] and then converting it to [SpannedString].
+ */
+inline fun buildSpannedString(builderAction: SpannableStringBuilder.() -> Unit): SpannedString {
+    val builder = SpannableStringBuilder()
+    builder.builderAction()
+    return SpannedString(builder)
+}
+
+/**
+ * Wrap appended text in [builderAction] in [spans].
+ *
+ * Note: the spans will only have the correct position if the [builderAction] only appends or
+ * replaces text. Inserting, deleting, or clearing the text will cause the span to be placed at
+ * an incorrect position.
+ */
+inline fun SpannableStringBuilder.inSpans(
+    vararg spans: Any,
+    builderAction: SpannableStringBuilder.() -> Unit
+): SpannableStringBuilder {
+    val start = length
+    builderAction()
+    for (span in spans) setSpan(span, start, length, SPAN_INCLUSIVE_EXCLUSIVE)
+    return this
+}
+
+/**
+ * Wrap appended text in [builderAction] in [span].
+ *
+ * Note: the span will only have the correct position if the `builderAction` only appends or
+ * replaces text. Inserting, deleting, or clearing the text will cause the span to be placed at
+ * an incorrect position.
+ */
+inline fun SpannableStringBuilder.inSpans(
+    span: Any,
+    builderAction: SpannableStringBuilder.() -> Unit
+): SpannableStringBuilder {
+    val start = length
+    builderAction()
+    setSpan(span, start, length, SPAN_INCLUSIVE_EXCLUSIVE)
+    return this
+}
+
+/**
+ * Wrap appended text in [builderAction] in a bold [StyleSpan].
+ *
+ * @see SpannableStringBuilder.inSpans
+ */
+inline fun SpannableStringBuilder.bold(builderAction: SpannableStringBuilder.() -> Unit) =
+    inSpans(StyleSpan(BOLD), builderAction = builderAction)
+
+/**
+ * Wrap appended text in [builderAction] in an italic [StyleSpan].
+ *
+ * @see SpannableStringBuilder.inSpans
+ */
+inline fun SpannableStringBuilder.italic(builderAction: SpannableStringBuilder.() -> Unit) =
+    inSpans(StyleSpan(ITALIC), builderAction = builderAction)
+
+/**
+ * Wrap appended text in [builderAction] in an [UnderlineSpan].
+ *
+ * @see SpannableStringBuilder.inSpans
+ */
+inline fun SpannableStringBuilder.underline(builderAction: SpannableStringBuilder.() -> Unit) =
+    inSpans(UnderlineSpan(), builderAction = builderAction)
+
+/**
+ * Wrap appended text in [builderAction] in a [ForegroundColorSpan].
+ *
+ * @see SpannableStringBuilder.inSpans
+ */
+inline fun SpannableStringBuilder.color(
+    @ColorInt color: Int,
+    builderAction: SpannableStringBuilder.() -> Unit
+) = inSpans(ForegroundColorSpan(color), builderAction = builderAction)
+
+/**
+ * Wrap appended text in [builderAction] in a [BackgroundColorSpan].
+ *
+ * @see SpannableStringBuilder.inSpans
+ */
+inline fun SpannableStringBuilder.backgroundColor(
+    @ColorInt color: Int,
+    builderAction: SpannableStringBuilder.() -> Unit
+) = inSpans(BackgroundColorSpan(color), builderAction = builderAction)
+
+/**
+ * Wrap appended text in [builderAction] in a [StrikethroughSpan].
+ *
+ * @see SpannableStringBuilder.inSpans
+ */
+inline fun SpannableStringBuilder.strikeThrough(builderAction: SpannableStringBuilder.() -> Unit) =
+    inSpans(StrikethroughSpan(), builderAction = builderAction)
+
+/**
+ * Wrap appended text in [builderAction] in a [RelativeSizeSpan].
+ *
+ * @see SpannableStringBuilder.inSpans
+ */
+inline fun SpannableStringBuilder.scale(
+    proportion: Float,
+    builderAction: SpannableStringBuilder.() -> Unit
+) = inSpans(RelativeSizeSpan(proportion), builderAction = builderAction)
+
+/**
+ * Wrap appended text in [builderAction] in a [SuperscriptSpan].
+ *
+ * @see SpannableStringBuilder.inSpans
+ */
+inline fun SpannableStringBuilder.superscript(builderAction: SpannableStringBuilder.() -> Unit) =
+    inSpans(SuperscriptSpan(), builderAction = builderAction)
+
+/**
+ * Wrap appended text in [builderAction] in a [SubscriptSpan].
+ *
+ * @see SpannableStringBuilder.inSpans
+ */
+inline fun SpannableStringBuilder.subscript(builderAction: SpannableStringBuilder.() -> Unit) =
+    inSpans(SubscriptSpan(), builderAction = builderAction)
diff --git a/core/ktx/src/main/java/androidx/core/text/SpannedString.kt b/core/ktx/src/main/java/androidx/core/text/SpannedString.kt
new file mode 100644
index 0000000..db27511
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/text/SpannedString.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.text
+
+import android.text.Spanned
+import android.text.SpannedString
+
+/**
+ * Returns a new [Spanned] from [CharSequence],
+ * or the source itself if it is already an instance of [SpannedString].
+ */
+inline fun CharSequence.toSpanned(): Spanned = SpannedString.valueOf(this)
+
+/** Get all spans that are instance of [T]. */
+inline fun <reified T : Any> Spanned.getSpans(start: Int = 0, end: Int = length): Array<out T> =
+    getSpans(start, end, T::class.java)
diff --git a/core/ktx/src/main/java/androidx/core/text/String.kt b/core/ktx/src/main/java/androidx/core/text/String.kt
new file mode 100644
index 0000000..8bffdd0
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/text/String.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.text
+
+import android.text.TextUtils
+
+/**
+ * Html-encode the string.
+ *
+ * @see TextUtils.htmlEncode
+ */
+inline fun String.htmlEncode(): String = TextUtils.htmlEncode(this)
diff --git a/core/ktx/src/main/java/androidx/core/transition/Transition.kt b/core/ktx/src/main/java/androidx/core/transition/Transition.kt
new file mode 100644
index 0000000..b79f2cb
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/transition/Transition.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.transition
+
+import android.transition.Transition
+import androidx.annotation.RequiresApi
+
+/**
+ * Add an action which will be invoked when this transition has ended.
+ */
+@RequiresApi(19)
+fun Transition.doOnEnd(action: (transition: Transition) -> Unit) {
+    addListener(onEnd = action)
+}
+
+/**
+ * Add an action which will be invoked when this transition has started.
+ */
+@RequiresApi(19)
+fun Transition.doOnStart(action: (transition: Transition) -> Unit) {
+    addListener(onStart = action)
+}
+
+/**
+ * Add an action which will be invoked when this transition has been cancelled.
+ */
+@RequiresApi(19)
+fun Transition.doOnCancel(action: (transition: Transition) -> Unit) {
+    addListener(onCancel = action)
+}
+
+/**
+ * Add an action which will be invoked when this transition has resumed after a pause.
+ */
+@RequiresApi(19)
+fun Transition.doOnResume(action: (transition: Transition) -> Unit) {
+    addListener(onResume = action)
+}
+
+/**
+ * Add an action which will be invoked when this transition has been paused.
+ */
+@RequiresApi(19)
+fun Transition.doOnPause(action: (transition: Transition) -> Unit) {
+    addListener(onPause = action)
+}
+
+/**
+ * Add a listener to this Transition using the provided actions.
+ */
+@RequiresApi(19)
+fun Transition.addListener(
+    onEnd: ((transition: Transition) -> Unit)? = null,
+    onStart: ((transition: Transition) -> Unit)? = null,
+    onCancel: ((transition: Transition) -> Unit)? = null,
+    onResume: ((transition: Transition) -> Unit)? = null,
+    onPause: ((transition: Transition) -> Unit)? = null
+) {
+    addListener(object : Transition.TransitionListener {
+        override fun onTransitionEnd(transition: Transition) {
+            onEnd?.invoke(transition)
+        }
+
+        override fun onTransitionResume(transition: Transition) {
+            onResume?.invoke(transition)
+        }
+
+        override fun onTransitionPause(transition: Transition) {
+            onPause?.invoke(transition)
+        }
+
+        override fun onTransitionCancel(transition: Transition) {
+            onCancel?.invoke(transition)
+        }
+
+        override fun onTransitionStart(transition: Transition) {
+            onStart?.invoke(transition)
+        }
+    })
+}
diff --git a/core/ktx/src/main/java/androidx/core/util/ArrayMap.kt b/core/ktx/src/main/java/androidx/core/util/ArrayMap.kt
new file mode 100644
index 0000000..b676403
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/ArrayMap.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.util
+
+import android.util.ArrayMap
+import androidx.annotation.RequiresApi
+import kotlin.Pair
+
+/** Returns an empty new [ArrayMap]. */
+@RequiresApi(19)
+inline fun <K, V> arrayMapOf(): ArrayMap<K, V> = ArrayMap()
+
+/**
+ * Returns a new [ArrayMap] with the specified contents, given as a list of pairs where the first
+ * component is the key and the second component is the value.
+ *
+ * If multiple pairs have the same key, the resulting map will contain the value from the last of
+ * those pairs.
+ */
+@RequiresApi(19)
+fun <K, V> arrayMapOf(vararg pairs: Pair<K, V>): ArrayMap<K, V> {
+    val map = ArrayMap<K, V>(pairs.size)
+    for (pair in pairs) {
+        map[pair.first] = pair.second
+    }
+    return map
+}
diff --git a/core/ktx/src/main/java/androidx/core/util/ArraySet.kt b/core/ktx/src/main/java/androidx/core/util/ArraySet.kt
new file mode 100644
index 0000000..6773d23
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/ArraySet.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.util
+
+import android.util.ArraySet
+import androidx.annotation.RequiresApi
+
+/** Returns an empty new [ArraySet]. */
+@RequiresApi(23)
+inline fun <T> arraySetOf(): ArraySet<T> = ArraySet()
+
+/** Returns a new [ArraySet] with the specified contents. */
+@RequiresApi(23)
+fun <T> arraySetOf(vararg values: T): ArraySet<T> {
+    val set = ArraySet<T>(values.size)
+    @Suppress("LoopToCallChain") // Causes needless copy to a list.
+    for (value in values) {
+        set.add(value)
+    }
+    return set
+}
diff --git a/core/ktx/src/main/java/androidx/core/util/AtomicFile.kt b/core/ktx/src/main/java/androidx/core/util/AtomicFile.kt
new file mode 100644
index 0000000..b8fbba7
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/AtomicFile.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to other public API.
+
+package androidx.core.util
+
+import android.util.AtomicFile
+import androidx.annotation.RequiresApi
+import java.io.FileOutputStream
+import java.nio.charset.Charset
+
+/**
+ * Perform the write operations inside [block] on this file. If [block] throws an exception the
+ * write will be failed. Otherwise the write will be applied atomically to the file.
+ */
+@RequiresApi(17)
+inline fun AtomicFile.tryWrite(block: (out: FileOutputStream) -> Unit) {
+    val stream = startWrite()
+    var success = false
+    try {
+        block(stream)
+        success = true
+    } finally {
+        if (success) {
+            finishWrite(stream)
+        } else {
+            failWrite(stream)
+        }
+    }
+}
+
+/**
+ * Sets the content of this file as an [array] of bytes.
+ */
+@RequiresApi(17)
+fun AtomicFile.writeBytes(array: ByteArray) {
+    tryWrite {
+        it.write(array)
+    }
+}
+
+/**
+ * Sets the content of this file as [text] encoded using UTF-8 or specified [charset].
+ * If this file exists, it becomes overwritten.
+ */
+@RequiresApi(17)
+fun AtomicFile.writeText(text: String, charset: Charset = Charsets.UTF_8) {
+    writeBytes(text.toByteArray(charset))
+}
+
+/**
+ * Gets the entire content of this file as a byte array.
+ *
+ * This method is not recommended on huge files. It has an internal limitation of 2 GB file size.
+ */
+@RequiresApi(17)
+inline fun AtomicFile.readBytes(): ByteArray = readFully()
+
+/**
+ * Gets the entire content of this file as a String using UTF-8 or specified [charset].
+ *
+ * This method is not recommended on huge files. It has an internal limitation of 2 GB file size.
+ */
+@RequiresApi(17)
+fun AtomicFile.readText(charset: Charset = Charsets.UTF_8): String {
+    return readFully().toString(charset)
+}
diff --git a/core/ktx/src/main/java/androidx/core/util/Half.kt b/core/ktx/src/main/java/androidx/core/util/Half.kt
new file mode 100644
index 0000000..409c01e
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/Half.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to other public API.
+
+package androidx.core.util
+
+import android.util.Half
+import androidx.annotation.HalfFloat
+import androidx.annotation.RequiresApi
+
+/**
+ * Returns a [Half] instance representing given [Short].
+ *
+ * @see Half.valueOf
+ */
+// TODO https://youtrack.jetbrains.com/issue/KT-21696
+@Suppress("WRONG_ANNOTATION_TARGET_WITH_USE_SITE_TARGET_ON_TYPE")
+@RequiresApi(26)
+inline fun @receiver:HalfFloat Short.toHalf(): Half = Half.valueOf(this)
+
+/**
+ * Returns a [Half] instance representing given [Float].
+ *
+ * @see Half.valueOf
+ */
+@RequiresApi(26)
+inline fun Float.toHalf(): Half = Half.valueOf(this)
+
+/**
+ * Returns a [Half] instance representing given [Double].
+ *
+ * @see Half.valueOf
+ */
+@RequiresApi(26)
+inline fun Double.toHalf(): Half = toFloat().toHalf()
+
+/**
+ * Returns a [Half] instance representing given [String].
+ *
+ * @see Half.valueOf
+ */
+@RequiresApi(26)
+inline fun String.toHalf(): Half = Half.valueOf(this)
diff --git a/core/ktx/src/main/java/androidx/core/util/Locale.kt b/core/ktx/src/main/java/androidx/core/util/Locale.kt
new file mode 100644
index 0000000..2b4312e
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/Locale.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+package androidx.core.util
+
+import android.text.TextUtils
+import androidx.annotation.RequiresApi
+import java.util.Locale
+
+/**
+ * Returns layout direction for a given locale.
+ * @see TextUtils.getLayoutDirectionFromLocale
+ */
+val Locale.layoutDirection: Int
+        @RequiresApi(17)
+        get() = TextUtils.getLayoutDirectionFromLocale(this)
diff --git a/core/ktx/src/main/java/androidx/core/util/LongSparseArray.kt b/core/ktx/src/main/java/androidx/core/util/LongSparseArray.kt
new file mode 100644
index 0000000..cd9eb4c
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/LongSparseArray.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.util
+
+import android.util.LongSparseArray
+import androidx.annotation.RequiresApi
+
+/** Returns the number of key/value pairs in the collection. */
+@get:RequiresApi(16)
+inline val <T> LongSparseArray<T>.size get() = size()
+
+/** Returns true if the collection contains [key]. */
+@RequiresApi(16)
+inline operator fun <T> LongSparseArray<T>.contains(key: Long) = indexOfKey(key) >= 0
+
+/** Allows the use of the index operator for storing values in the collection. */
+@RequiresApi(16)
+inline operator fun <T> LongSparseArray<T>.set(key: Long, value: T) = put(key, value)
+
+/** Creates a new collection by adding or replacing entries from [other]. */
+@RequiresApi(16)
+operator fun <T> LongSparseArray<T>.plus(other: LongSparseArray<T>): LongSparseArray<T> {
+    val new = LongSparseArray<T>(size() + other.size())
+    new.putAll(this)
+    new.putAll(other)
+    return new
+}
+
+/** Returns true if the collection contains [key]. */
+@RequiresApi(16)
+inline fun <T> LongSparseArray<T>.containsKey(key: Long) = indexOfKey(key) >= 0
+
+/** Returns true if the collection contains [value]. */
+@RequiresApi(16)
+inline fun <T> LongSparseArray<T>.containsValue(value: T) = indexOfValue(value) != -1
+
+/** Return the value corresponding to [key], or [defaultValue] when not present. */
+@RequiresApi(16)
+inline fun <T> LongSparseArray<T>.getOrDefault(key: Long, defaultValue: T) =
+    get(key) ?: defaultValue
+
+/** Return the value corresponding to [key], or from [defaultValue] when not present. */
+@RequiresApi(16)
+inline fun <T> LongSparseArray<T>.getOrElse(key: Long, defaultValue: () -> T) =
+    get(key) ?: defaultValue()
+
+/** Return true when the collection contains no elements. */
+@RequiresApi(16)
+inline fun <T> LongSparseArray<T>.isEmpty() = size() == 0
+
+/** Return true when the collection contains elements. */
+@RequiresApi(16)
+inline fun <T> LongSparseArray<T>.isNotEmpty() = size() != 0
+
+/** Removes the entry for [key] only if it is mapped to [value]. */
+@RequiresApi(16)
+fun <T> LongSparseArray<T>.remove(key: Long, value: T): Boolean {
+    val index = indexOfKey(key)
+    if (index != -1 && value == valueAt(index)) {
+        removeAt(index)
+        return true
+    }
+    return false
+}
+
+/** Update this collection by adding or replacing entries from [other]. */
+@RequiresApi(16)
+fun <T> LongSparseArray<T>.putAll(other: LongSparseArray<T>) = other.forEach(::put)
+
+/** Performs the given [action] for each key/value entry. */
+@RequiresApi(16)
+inline fun <T> LongSparseArray<T>.forEach(action: (key: Long, value: T) -> Unit) {
+    for (index in 0 until size()) {
+        action(keyAt(index), valueAt(index))
+    }
+}
+
+/** Return an iterator over the collection's keys. */
+@RequiresApi(16)
+fun <T> LongSparseArray<T>.keyIterator(): LongIterator = object : LongIterator() {
+    var index = 0
+    override fun hasNext() = index < size()
+    override fun nextLong() = keyAt(index++)
+}
+
+/** Return an iterator over the collection's values. */
+@RequiresApi(16)
+fun <T> LongSparseArray<T>.valueIterator(): Iterator<T> = object : Iterator<T> {
+    var index = 0
+    override fun hasNext() = index < size()
+    override fun next() = valueAt(index++)
+}
diff --git a/core/ktx/src/main/java/androidx/core/util/LruCache.kt b/core/ktx/src/main/java/androidx/core/util/LruCache.kt
new file mode 100644
index 0000000..44c2f8d
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/LruCache.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+package androidx.core.util
+
+import android.util.LruCache
+
+/**
+ * Creates an [LruCache] with the given parameters.
+ *
+ * @param maxSize for caches that do not specify [sizeOf], this is
+ * the maximum number of entries in the cache. For all other caches,
+ * this is the maximum sum of the sizes of the entries in this cache.
+ * @param sizeOf function that returns the size of the entry for key and value in
+ * user-defined units. The default implementation returns 1.
+ * @param create a create called after a cache miss to compute a value for the corresponding key.
+ * Returns the computed value or null if no value can be computed. The default implementation
+ * returns null.
+ * @param onEntryRemoved a function called for entries that have been evicted or removed.
+ *
+ * @see LruCache.sizeOf
+ * @see LruCache.create
+ * @see LruCache.entryRemoved
+ */
+inline fun <K : Any, V : Any> lruCache(
+    maxSize: Int,
+    crossinline sizeOf: (key: K, value: V) -> Int = { _, _ -> 1 },
+    @Suppress("USELESS_CAST") // https://youtrack.jetbrains.com/issue/KT-21946
+    crossinline create: (key: K) -> V? = { null as V? },
+    crossinline onEntryRemoved: (evicted: Boolean, key: K, oldValue: V, newValue: V?) -> Unit =
+        { _, _, _, _ -> }
+): LruCache<K, V> {
+    return object : LruCache<K, V>(maxSize) {
+        override fun sizeOf(key: K, value: V) = sizeOf(key, value)
+        override fun create(key: K) = create(key)
+        override fun entryRemoved(evicted: Boolean, key: K, oldValue: V, newValue: V?) {
+            onEntryRemoved(evicted, key, oldValue, newValue)
+        }
+    }
+}
diff --git a/core/ktx/src/main/java/androidx/core/util/Pair.kt b/core/ktx/src/main/java/androidx/core/util/Pair.kt
new file mode 100644
index 0000000..cf12156
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/Pair.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.util
+
+import android.util.Pair
+
+/**
+ * Returns the first component of the pair.
+ *
+ * This method allows to use destructuring declarations when working with pairs, for example:
+ * ```
+ * val (first, second) = myPair
+ * ```
+ */
+@Suppress("HasPlatformType") // Intentionally propagating platform type with unknown nullability.
+inline operator fun <F, S> Pair<F, S>.component1() = first
+
+/**
+ * Returns the second component of the pair.
+ *
+ * This method allows to use destructuring declarations when working with pairs, for example:
+ * ```
+ * val (first, second) = myPair
+ * ```
+ */
+@Suppress("HasPlatformType") // Intentionally propagating platform type with unknown nullability.
+inline operator fun <F, S> Pair<F, S>.component2() = second
+
+/** Returns this [Pair] as a [kotlin.Pair]. */
+inline fun <F, S> Pair<F, S>.toKotlinPair() = kotlin.Pair(first, second)
+
+/** Returns this [kotlin.Pair] as an Android [Pair]. */
+// Note: the return type is explicitly specified here to prevent always seeing platform types.
+inline fun <F, S> kotlin.Pair<F, S>.toAndroidPair(): Pair<F, S> = Pair(first, second)
diff --git a/core/ktx/src/main/java/androidx/core/util/Range.kt b/core/ktx/src/main/java/androidx/core/util/Range.kt
new file mode 100644
index 0000000..b5f7eb6
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/Range.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.util
+
+import android.util.Range
+import androidx.annotation.RequiresApi
+
+/**
+ * Creates a range from this [Comparable] value to [that].
+ *
+ * @throws IllegalArgumentException if this value is comparatively smaller than [that].
+ */
+@RequiresApi(21)
+inline infix fun <T : Comparable<T>> T.rangeTo(that: T): Range<T> = Range(this, that)
+
+/** Return the smallest range that includes this and [value]. */
+@RequiresApi(21)
+inline operator fun <T : Comparable<T>> Range<T>.plus(value: T): Range<T> = extend(value)
+
+/** Return the smallest range that includes this and [other]. */
+@RequiresApi(21)
+inline operator fun <T : Comparable<T>> Range<T>.plus(other: Range<T>): Range<T> = extend(other)
+
+/**
+ * Return the intersection of this range and [other].
+ *
+ * @throws IllegalArgumentException if this is disjoint from [other].
+ */
+@RequiresApi(21)
+inline infix fun <T : Comparable<T>> Range<T>.and(other: Range<T>): Range<T> = intersect(other)
+
+/** Returns this [Range] as a [ClosedRange]. */
+@RequiresApi(21)
+fun <T : Comparable<T>> Range<T>.toClosedRange(): ClosedRange<T> = object : ClosedRange<T> {
+    override val endInclusive get() = upper
+    override val start get() = lower
+}
+
+/** Returns this [ClosedRange] as a [Range]. */
+@RequiresApi(21)
+fun <T : Comparable<T>> ClosedRange<T>.toRange(): Range<T> = Range(start, endInclusive)
diff --git a/core/ktx/src/main/java/androidx/core/util/Size.kt b/core/ktx/src/main/java/androidx/core/util/Size.kt
new file mode 100644
index 0000000..34f6854
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/Size.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.util
+
+import android.util.Size
+import android.util.SizeF
+import androidx.annotation.RequiresApi
+
+/**
+ * Returns "width", the first component of this [Size].
+ *
+ * This method allows to use destructuring declarations when working with
+ * sizes, for example:
+ * ```
+ * val (w, h) = mySize
+ * ```
+ */
+@RequiresApi(21)
+inline operator fun Size.component1() = width
+
+/**
+ * Returns "height", the second component of this [Size].
+ *
+ * This method allows to use destructuring declarations when working with
+ * sizes, for example:
+ * ```
+ * val (w, h) = mySize
+ * ```
+ */
+@RequiresApi(21)
+inline operator fun Size.component2() = height
+
+/**
+ * Returns "width", the first component of this [SizeF].
+ *
+ * This method allows to use destructuring declarations when working with
+ * sizes, for example:
+ * ```
+ * val (w, h) = mySize
+ * ```
+ */
+@RequiresApi(21)
+inline operator fun SizeF.component1() = width
+
+/**
+ * Returns "height", the second component of this [SizeF].
+ *
+ * This method allows to use destructuring declarations when working with
+ * sizes, for example:
+ * ```
+ * val (w, h) = mySize
+ * ```
+ */
+@RequiresApi(21)
+inline operator fun SizeF.component2() = height
diff --git a/core/ktx/src/main/java/androidx/core/util/SparseArray.kt b/core/ktx/src/main/java/androidx/core/util/SparseArray.kt
new file mode 100644
index 0000000..2b34a46
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/SparseArray.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.util
+
+import android.util.SparseArray
+
+/** Returns the number of key/value pairs in the collection. */
+inline val <T> SparseArray<T>.size get() = size()
+
+/** Returns true if the collection contains [key]. */
+inline operator fun <T> SparseArray<T>.contains(key: Int) = indexOfKey(key) >= 0
+
+/** Allows the use of the index operator for storing values in the collection. */
+inline operator fun <T> SparseArray<T>.set(key: Int, value: T) = put(key, value)
+
+/** Creates a new collection by adding or replacing entries from [other]. */
+operator fun <T> SparseArray<T>.plus(other: SparseArray<T>): SparseArray<T> {
+    val new = SparseArray<T>(size() + other.size())
+    new.putAll(this)
+    new.putAll(other)
+    return new
+}
+
+/** Returns true if the collection contains [key]. */
+inline fun <T> SparseArray<T>.containsKey(key: Int) = indexOfKey(key) >= 0
+
+/** Returns true if the collection contains [value]. */
+inline fun <T> SparseArray<T>.containsValue(value: T) = indexOfValue(value) != -1
+
+/** Return the value corresponding to [key], or [defaultValue] when not present. */
+inline fun <T> SparseArray<T>.getOrDefault(key: Int, defaultValue: T) = get(key) ?: defaultValue
+
+/** Return the value corresponding to [key], or from [defaultValue] when not present. */
+inline fun <T> SparseArray<T>.getOrElse(key: Int, defaultValue: () -> T) =
+    get(key) ?: defaultValue()
+
+/** Return true when the collection contains no elements. */
+inline fun <T> SparseArray<T>.isEmpty() = size() == 0
+
+/** Return true when the collection contains elements. */
+inline fun <T> SparseArray<T>.isNotEmpty() = size() != 0
+
+/** Removes the entry for [key] only if it is mapped to [value]. */
+fun <T> SparseArray<T>.remove(key: Int, value: T): Boolean {
+    val index = indexOfKey(key)
+    if (index != -1 && value == valueAt(index)) {
+        removeAt(index)
+        return true
+    }
+    return false
+}
+
+/** Update this collection by adding or replacing entries from [other]. */
+fun <T> SparseArray<T>.putAll(other: SparseArray<T>) = other.forEach(::put)
+
+/** Performs the given [action] for each key/value entry. */
+inline fun <T> SparseArray<T>.forEach(action: (key: Int, value: T) -> Unit) {
+    for (index in 0 until size()) {
+        action(keyAt(index), valueAt(index))
+    }
+}
+
+/** Return an iterator over the collection's keys. */
+fun <T> SparseArray<T>.keyIterator(): IntIterator = object : IntIterator() {
+    var index = 0
+    override fun hasNext() = index < size()
+    override fun nextInt() = keyAt(index++)
+}
+
+/** Return an iterator over the collection's values. */
+fun <T> SparseArray<T>.valueIterator(): Iterator<T> = object : Iterator<T> {
+    var index = 0
+    override fun hasNext() = index < size()
+    override fun next() = valueAt(index++)
+}
diff --git a/core/ktx/src/main/java/androidx/core/util/SparseBooleanArray.kt b/core/ktx/src/main/java/androidx/core/util/SparseBooleanArray.kt
new file mode 100644
index 0000000..b349326
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/SparseBooleanArray.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.util
+
+import android.util.SparseBooleanArray
+
+/** Returns the number of key/value pairs in the collection. */
+inline val SparseBooleanArray.size get() = size()
+
+/** Returns true if the collection contains [key]. */
+inline operator fun SparseBooleanArray.contains(key: Int) = indexOfKey(key) >= 0
+
+/** Allows the use of the index operator for storing values in the collection. */
+inline operator fun SparseBooleanArray.set(key: Int, value: Boolean) = put(key, value)
+
+/** Creates a new collection by adding or replacing entries from [other]. */
+operator fun SparseBooleanArray.plus(other: SparseBooleanArray): SparseBooleanArray {
+    val new = SparseBooleanArray(size() + other.size())
+    new.putAll(this)
+    new.putAll(other)
+    return new
+}
+
+/** Returns true if the collection contains [key]. */
+inline fun SparseBooleanArray.containsKey(key: Int) = indexOfKey(key) >= 0
+
+/** Returns true if the collection contains [value]. */
+inline fun SparseBooleanArray.containsValue(value: Boolean) = indexOfValue(value) != -1
+
+/** Return the value corresponding to [key], or [defaultValue] when not present. */
+inline fun SparseBooleanArray.getOrDefault(key: Int, defaultValue: Boolean) = get(key, defaultValue)
+
+/** Return the value corresponding to [key], or from [defaultValue] when not present. */
+inline fun SparseBooleanArray.getOrElse(key: Int, defaultValue: () -> Boolean) =
+    indexOfKey(key).let { if (it != -1) valueAt(it) else defaultValue() }
+
+/** Return true when the collection contains no elements. */
+inline fun SparseBooleanArray.isEmpty() = size() == 0
+
+/** Return true when the collection contains elements. */
+inline fun SparseBooleanArray.isNotEmpty() = size() != 0
+
+/** Removes the entry for [key] only if it is mapped to [value]. */
+fun SparseBooleanArray.remove(key: Int, value: Boolean): Boolean {
+    val index = indexOfKey(key)
+    if (index != -1 && value == valueAt(index)) {
+        // Delete by key because of https://issuetracker.google.com/issues/70934959.
+        delete(key)
+        return true
+    }
+    return false
+}
+
+/** Update this collection by adding or replacing entries from [other]. */
+fun SparseBooleanArray.putAll(other: SparseBooleanArray) = other.forEach(::put)
+
+/** Performs the given [action] for each key/value entry. */
+inline fun SparseBooleanArray.forEach(action: (key: Int, value: Boolean) -> Unit) {
+    for (index in 0 until size()) {
+        action(keyAt(index), valueAt(index))
+    }
+}
+
+/** Return an iterator over the collection's keys. */
+fun SparseBooleanArray.keyIterator(): IntIterator = object : IntIterator() {
+    var index = 0
+    override fun hasNext() = index < size()
+    override fun nextInt() = keyAt(index++)
+}
+
+/** Return an iterator over the collection's values. */
+fun SparseBooleanArray.valueIterator(): BooleanIterator = object : BooleanIterator() {
+    var index = 0
+    override fun hasNext() = index < size()
+    override fun nextBoolean() = valueAt(index++)
+}
diff --git a/core/ktx/src/main/java/androidx/core/util/SparseIntArray.kt b/core/ktx/src/main/java/androidx/core/util/SparseIntArray.kt
new file mode 100644
index 0000000..bba57c6
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/SparseIntArray.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.util
+
+import android.util.SparseIntArray
+
+/** Returns the number of key/value pairs in the collection. */
+inline val SparseIntArray.size get() = size()
+
+/** Returns true if the collection contains [key]. */
+inline operator fun SparseIntArray.contains(key: Int) = indexOfKey(key) >= 0
+
+/** Allows the use of the index operator for storing values in the collection. */
+inline operator fun SparseIntArray.set(key: Int, value: Int) = put(key, value)
+
+/** Creates a new collection by adding or replacing entries from [other]. */
+operator fun SparseIntArray.plus(other: SparseIntArray): SparseIntArray {
+    val new = SparseIntArray(size() + other.size())
+    new.putAll(this)
+    new.putAll(other)
+    return new
+}
+
+/** Returns true if the collection contains [key]. */
+inline fun SparseIntArray.containsKey(key: Int) = indexOfKey(key) >= 0
+
+/** Returns true if the collection contains [value]. */
+inline fun SparseIntArray.containsValue(value: Int) = indexOfValue(value) != -1
+
+/** Return the value corresponding to [key], or [defaultValue] when not present. */
+inline fun SparseIntArray.getOrDefault(key: Int, defaultValue: Int) = get(key, defaultValue)
+
+/** Return the value corresponding to [key], or from [defaultValue] when not present. */
+inline fun SparseIntArray.getOrElse(key: Int, defaultValue: () -> Int) =
+    indexOfKey(key).let { if (it != -1) valueAt(it) else defaultValue() }
+
+/** Return true when the collection contains no elements. */
+inline fun SparseIntArray.isEmpty() = size() == 0
+
+/** Return true when the collection contains elements. */
+inline fun SparseIntArray.isNotEmpty() = size() != 0
+
+/** Removes the entry for [key] only if it is mapped to [value]. */
+fun SparseIntArray.remove(key: Int, value: Int): Boolean {
+    val index = indexOfKey(key)
+    if (index != -1 && value == valueAt(index)) {
+        removeAt(index)
+        return true
+    }
+    return false
+}
+
+/** Update this collection by adding or replacing entries from [other]. */
+fun SparseIntArray.putAll(other: SparseIntArray) = other.forEach(::put)
+
+/** Performs the given [action] for each key/value entry. */
+inline fun SparseIntArray.forEach(action: (key: Int, value: Int) -> Unit) {
+    for (index in 0 until size()) {
+        action(keyAt(index), valueAt(index))
+    }
+}
+
+/** Return an iterator over the collection's keys. */
+fun SparseIntArray.keyIterator(): IntIterator = object : IntIterator() {
+    var index = 0
+    override fun hasNext() = index < size()
+    override fun nextInt() = keyAt(index++)
+}
+
+/** Return an iterator over the collection's values. */
+fun SparseIntArray.valueIterator(): IntIterator = object : IntIterator() {
+    var index = 0
+    override fun hasNext() = index < size()
+    override fun nextInt() = valueAt(index++)
+}
diff --git a/core/ktx/src/main/java/androidx/core/util/SparseLongArray.kt b/core/ktx/src/main/java/androidx/core/util/SparseLongArray.kt
new file mode 100644
index 0000000..68b4763
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/SparseLongArray.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.util
+
+import android.util.SparseLongArray
+import androidx.annotation.RequiresApi
+
+/** Returns the number of key/value entries in the collection. */
+@get:RequiresApi(18)
+inline val SparseLongArray.size get() = size()
+
+/** Returns true if the collection contains [key]. */
+@RequiresApi(18)
+inline operator fun SparseLongArray.contains(key: Int) = indexOfKey(key) >= 0
+
+/** Allows the use of the index operator for storing values in the collection. */
+@RequiresApi(18)
+inline operator fun SparseLongArray.set(key: Int, value: Long) = put(key, value)
+
+/** Creates a new collection by adding or replacing entries from [other]. */
+@RequiresApi(18)
+operator fun SparseLongArray.plus(other: SparseLongArray): SparseLongArray {
+    val new = SparseLongArray(size() + other.size())
+    new.putAll(this)
+    new.putAll(other)
+    return new
+}
+
+/** Returns true if the collection contains [key]. */
+@RequiresApi(18)
+inline fun SparseLongArray.containsKey(key: Int) = indexOfKey(key) >= 0
+
+/** Returns true if the collection contains [value]. */
+@RequiresApi(18)
+inline fun SparseLongArray.containsValue(value: Long) = indexOfValue(value) != -1
+
+/** Return the value corresponding to [key], or [defaultValue] when not present. */
+@RequiresApi(18)
+inline fun SparseLongArray.getOrDefault(key: Int, defaultValue: Long) = get(key, defaultValue)
+
+/** Return the value corresponding to [key], or from [defaultValue] when not present. */
+@RequiresApi(18)
+inline fun SparseLongArray.getOrElse(key: Int, defaultValue: () -> Long) =
+    indexOfKey(key).let { if (it != -1) valueAt(it) else defaultValue() }
+
+/** Return true when the collection contains no elements. */
+@RequiresApi(18)
+inline fun SparseLongArray.isEmpty() = size() == 0
+
+/** Return true when the collection contains elements. */
+@RequiresApi(18)
+inline fun SparseLongArray.isNotEmpty() = size() != 0
+
+/** Removes the entry for [key] only if it is set to [value]. */
+@RequiresApi(18)
+fun SparseLongArray.remove(key: Int, value: Long): Boolean {
+    val index = indexOfKey(key)
+    if (index != -1 && value == valueAt(index)) {
+        removeAt(index)
+        return true
+    }
+    return false
+}
+
+/** Update this collection by adding or replacing entries from [other]. */
+@RequiresApi(18)
+fun SparseLongArray.putAll(other: SparseLongArray) = other.forEach(::put)
+
+/** Performs the given [action] for each key/value entry. */
+@RequiresApi(18)
+inline fun SparseLongArray.forEach(action: (key: Int, value: Long) -> Unit) {
+    for (index in 0 until size()) {
+        action(keyAt(index), valueAt(index))
+    }
+}
+
+/** Return an iterator over the collection's keys. */
+@RequiresApi(18)
+fun SparseLongArray.keyIterator(): IntIterator = object : IntIterator() {
+    var index = 0
+    override fun hasNext() = index < size()
+    override fun nextInt() = keyAt(index++)
+}
+
+/** Return an iterator over the collection's values. */
+@RequiresApi(18)
+fun SparseLongArray.valueIterator(): LongIterator = object : LongIterator() {
+    var index = 0
+    override fun hasNext() = index < size()
+    override fun nextLong() = valueAt(index++)
+}
diff --git a/core/ktx/src/main/java/androidx/core/view/Menu.kt b/core/ktx/src/main/java/androidx/core/view/Menu.kt
new file mode 100644
index 0000000..069e61d
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/view/Menu.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.view
+
+import android.view.Menu
+import android.view.MenuItem
+
+/**
+ * Returns the menu at [index].
+ *
+ * @throws IndexOutOfBoundsException if index is less than 0 or greater than or equal to the count.
+ */
+inline operator fun Menu.get(index: Int): MenuItem = getItem(index)
+
+/** Returns `true` if [item] is found in this menu. */
+operator fun Menu.contains(item: MenuItem): Boolean {
+    @Suppress("LoopToCallChain")
+    for (index in 0 until size()) {
+        if (getItem(index) == item) {
+            return true
+        }
+    }
+    return false
+}
+
+/** Removes [item] from this menu. */
+inline operator fun Menu.minusAssign(item: MenuItem) = removeItem(item.itemId)
+
+/** Returns the number of items in this menu. */
+inline val Menu.size get() = size()
+
+/** Returns true if this menu contains no items. */
+inline fun Menu.isEmpty() = size() == 0
+
+/** Returns true if this menu contains one or more items. */
+inline fun Menu.isNotEmpty() = size() != 0
+
+/** Performs the given action on each item in this menu. */
+inline fun Menu.forEach(action: (item: MenuItem) -> Unit) {
+    for (index in 0 until size()) {
+        action(getItem(index))
+    }
+}
+
+/** Performs the given action on each item in this menu, providing its sequential index. */
+inline fun Menu.forEachIndexed(action: (index: Int, item: MenuItem) -> Unit) {
+    for (index in 0 until size()) {
+        action(index, getItem(index))
+    }
+}
+
+/** Returns a [MutableIterator] over the items in this menu. */
+operator fun Menu.iterator() = object : MutableIterator<MenuItem> {
+    private var index = 0
+    override fun hasNext() = index < size()
+    override fun next() = getItem(index++) ?: throw IndexOutOfBoundsException()
+    override fun remove() = removeItem(--index)
+}
+
+/** Returns a [Sequence] over the items in this menu. */
+val Menu.children: Sequence<MenuItem>
+    get() = object : Sequence<MenuItem> {
+        override fun iterator() = this@children.iterator()
+    }
diff --git a/core/ktx/src/main/java/androidx/core/view/View.kt b/core/ktx/src/main/java/androidx/core/view/View.kt
new file mode 100644
index 0000000..bc2e34d
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/view/View.kt
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to other public API.
+
+package androidx.core.view
+
+import android.graphics.Bitmap
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import android.view.accessibility.AccessibilityEvent
+import androidx.annotation.Px
+import androidx.annotation.RequiresApi
+import androidx.annotation.StringRes
+import androidx.core.graphics.applyCanvas
+
+/**
+ * Performs the given action when this view is next laid out.
+ *
+ * @see doOnLayout
+ */
+inline fun View.doOnNextLayout(crossinline action: (view: View) -> Unit) {
+    addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
+        override fun onLayoutChange(
+            view: View,
+            left: Int,
+            top: Int,
+            right: Int,
+            bottom: Int,
+            oldLeft: Int,
+            oldTop: Int,
+            oldRight: Int,
+            oldBottom: Int
+        ) {
+            view.removeOnLayoutChangeListener(this)
+            action(view)
+        }
+    })
+}
+
+/**
+ * Performs the given action when this view is laid out. If the view has been laid out and it
+ * has not requested a layout, the action will be performed straight away, otherwise the
+ * action will be performed after the view is next laid out.
+ *
+ * @see doOnNextLayout
+ */
+inline fun View.doOnLayout(crossinline action: (view: View) -> Unit) {
+    if (ViewCompat.isLaidOut(this) && !isLayoutRequested) {
+        action(this)
+    } else {
+        doOnNextLayout {
+            action(it)
+        }
+    }
+}
+
+/**
+ * Performs the given action when the view tree is about to be drawn.
+ */
+inline fun View.doOnPreDraw(crossinline action: (view: View) -> Unit) {
+    val vto = viewTreeObserver
+    vto.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
+        override fun onPreDraw(): Boolean {
+            action(this@doOnPreDraw)
+            when {
+                vto.isAlive -> vto.removeOnPreDrawListener(this)
+                else -> viewTreeObserver.removeOnPreDrawListener(this)
+            }
+            return true
+        }
+    })
+}
+
+/**
+ * Sends [AccessibilityEvent] of type [AccessibilityEvent.TYPE_ANNOUNCEMENT].
+ *
+ * @see View.announceForAccessibility
+ */
+@RequiresApi(16)
+inline fun View.announceForAccessibility(@StringRes resource: Int) {
+    val announcement = resources.getString(resource)
+    announceForAccessibility(announcement)
+}
+
+/**
+ * Updates this view's relative padding. This version of the method allows using named parameters
+ * to just set one or more axes.
+ *
+ * @see View.setPaddingRelative
+ */
+@RequiresApi(17)
+inline fun View.updatePaddingRelative(
+    @Px start: Int = paddingStart,
+    @Px top: Int = paddingTop,
+    @Px end: Int = paddingEnd,
+    @Px bottom: Int = paddingBottom
+) {
+    setPaddingRelative(start, top, end, bottom)
+}
+
+/**
+ * Updates this view's padding. This version of the method allows using named parameters
+ * to just set one or more axes.
+ *
+ * @see View.setPadding
+ */
+inline fun View.updatePadding(
+    @Px left: Int = paddingLeft,
+    @Px top: Int = paddingTop,
+    @Px right: Int = paddingRight,
+    @Px bottom: Int = paddingBottom
+) {
+    setPadding(left, top, right, bottom)
+}
+
+/**
+ * Sets the view's padding. This version of the method sets all axes to the provided size.
+ *
+ * @see View.setPadding
+ */
+inline fun View.setPadding(@Px size: Int) {
+    setPadding(size, size, size, size)
+}
+
+/**
+ * Version of [View.postDelayed] which re-orders the parameters, allowing the action to be placed
+ * outside of parentheses.
+ *
+ * ```
+ * view.postDelayed(200) {
+ *     doSomething()
+ * }
+ * ```
+ *
+ * @return the created Runnable
+ */
+inline fun View.postDelayed(delayInMillis: Long, crossinline action: () -> Unit): Runnable {
+    val runnable = Runnable { action() }
+    postDelayed(runnable, delayInMillis)
+    return runnable
+}
+
+/**
+ * Version of [View.postOnAnimationDelayed] which re-orders the parameters, allowing the action
+ * to be placed outside of parentheses.
+ *
+ * ```
+ * view.postOnAnimationDelayed(16) {
+ *     doSomething()
+ * }
+ * ```
+ *
+ * @return the created Runnable
+ */
+@RequiresApi(16)
+inline fun View.postOnAnimationDelayed(
+    delayInMillis: Long,
+    crossinline action: () -> Unit
+): Runnable {
+    val runnable = Runnable { action() }
+    postOnAnimationDelayed(runnable, delayInMillis)
+    return runnable
+}
+
+/**
+ * Return a [Bitmap] representation of this [View].
+ *
+ * The resulting bitmap will be the same width and height as this view's current layout
+ * dimensions. This does not take into account any transformations such as scale or translation.
+ *
+ * Note, this will use the software rendering pipeline to draw the view to the bitmap. This may
+ * result with different drawing to what is rendered on a hardware accelerated canvas (such as
+ * the device screen).
+ *
+ * If this view has not been laid out this method will throw a [IllegalStateException].
+ *
+ * @param config Bitmap config of the desired bitmap. Defaults to [Bitmap.Config.ARGB_8888].
+ */
+fun View.toBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888): Bitmap {
+    if (!ViewCompat.isLaidOut(this)) {
+        throw IllegalStateException("View needs to be laid out before calling toBitmap()")
+    }
+    return Bitmap.createBitmap(width, height, config).applyCanvas {
+        translate(-scrollX.toFloat(), -scrollY.toFloat())
+        draw(this)
+    }
+}
+
+/**
+ * Returns true when this view's visibility is [View.VISIBLE], false otherwise.
+ *
+ * ```
+ * if (view.isVisible) {
+ *     // Behavior...
+ * }
+ * ```
+ *
+ * Setting this property to true sets the visibility to [View.VISIBLE], false to [View.GONE].
+ *
+ * ```
+ * view.isVisible = true
+ * ```
+ */
+inline var View.isVisible: Boolean
+    get() = visibility == View.VISIBLE
+    set(value) {
+        visibility = if (value) View.VISIBLE else View.GONE
+    }
+
+/**
+ * Returns true when this view's visibility is [View.INVISIBLE], false otherwise.
+ *
+ * ```
+ * if (view.isInvisible) {
+ *     // Behavior...
+ * }
+ * ```
+ *
+ * Setting this property to true sets the visibility to [View.INVISIBLE], false to [View.VISIBLE].
+ *
+ * ```
+ * view.isInvisible = true
+ * ```
+ */
+inline var View.isInvisible: Boolean
+    get() = visibility == View.INVISIBLE
+    set(value) {
+        visibility = if (value) View.INVISIBLE else View.VISIBLE
+    }
+
+/**
+ * Returns true when this view's visibility is [View.GONE], false otherwise.
+ *
+ * ```
+ * if (view.isGone) {
+ *     // Behavior...
+ * }
+ * ```
+ *
+ * Setting this property to true sets the visibility to [View.GONE], false to [View.VISIBLE].
+ *
+ * ```
+ * view.isGone = true
+ * ```
+ */
+inline var View.isGone: Boolean
+    get() = visibility == View.GONE
+    set(value) {
+        visibility = if (value) View.GONE else View.VISIBLE
+    }
+
+/**
+ * Executes [block] with the View's layoutParams and reassigns the layoutParams with the
+ * updated version.
+ *
+ * @see View.getLayoutParams
+ * @see View.setLayoutParams
+ **/
+inline fun View.updateLayoutParams(block: ViewGroup.LayoutParams.() -> Unit) {
+    updateLayoutParams<ViewGroup.LayoutParams>(block)
+}
+
+/**
+ * Executes [block] with a typed version of the View's layoutParams and reassigns the
+ * layoutParams with the updated version.
+ *
+ * @see View.getLayoutParams
+ * @see View.setLayoutParams
+ **/
+@JvmName("updateLayoutParamsTyped")
+inline fun <reified T : ViewGroup.LayoutParams> View.updateLayoutParams(block: T.() -> Unit) {
+    val params = layoutParams as T
+    block(params)
+    layoutParams = params
+}
diff --git a/core/ktx/src/main/java/androidx/core/view/ViewGroup.kt b/core/ktx/src/main/java/androidx/core/view/ViewGroup.kt
new file mode 100644
index 0000000..8a7152a
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/view/ViewGroup.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to other public API.
+
+package androidx.core.view
+
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.Px
+import androidx.annotation.RequiresApi
+
+/**
+ * Returns the view at [index].
+ *
+ * @throws IndexOutOfBoundsException if index is less than 0 or greater than or equal to the count.
+ */
+operator fun ViewGroup.get(index: Int) =
+    getChildAt(index) ?: throw IndexOutOfBoundsException("Index: $index, Size: $childCount")
+
+/** Returns `true` if [view] is found in this view group. */
+inline operator fun ViewGroup.contains(view: View) = indexOfChild(view) != -1
+
+/** Adds [view] to this view group. */
+inline operator fun ViewGroup.plusAssign(view: View) = addView(view)
+
+/** Removes [view] from this view group. */
+inline operator fun ViewGroup.minusAssign(view: View) = removeView(view)
+
+/** Returns the number of views in this view group. */
+inline val ViewGroup.size get() = childCount
+
+/** Returns true if this view group contains no views. */
+inline fun ViewGroup.isEmpty() = childCount == 0
+
+/** Returns true if this view group contains one or more views. */
+inline fun ViewGroup.isNotEmpty() = childCount != 0
+
+/** Performs the given action on each view in this view group. */
+inline fun ViewGroup.forEach(action: (view: View) -> Unit) {
+    for (index in 0 until childCount) {
+        action(getChildAt(index))
+    }
+}
+
+/** Performs the given action on each view in this view group, providing its sequential index. */
+inline fun ViewGroup.forEachIndexed(action: (index: Int, view: View) -> Unit) {
+    for (index in 0 until childCount) {
+        action(index, getChildAt(index))
+    }
+}
+
+/** Returns a [MutableIterator] over the views in this view group. */
+operator fun ViewGroup.iterator() = object : MutableIterator<View> {
+    private var index = 0
+    override fun hasNext() = index < childCount
+    override fun next() = getChildAt(index++) ?: throw IndexOutOfBoundsException()
+    override fun remove() = removeViewAt(--index)
+}
+
+/** Returns a [Sequence] over the child views in this view group. */
+val ViewGroup.children: Sequence<View>
+    get() = object : Sequence<View> {
+        override fun iterator() = this@children.iterator()
+    }
+
+/**
+ * Sets the margins in the ViewGroup's MarginLayoutParams. This version of the method sets all axes
+ * to the provided size.
+ *
+ * @see ViewGroup.MarginLayoutParams.setMargins
+ */
+inline fun ViewGroup.MarginLayoutParams.setMargins(@Px size: Int) {
+    setMargins(size, size, size, size)
+}
+
+/**
+ * Updates the margins in the [ViewGroup]'s [ViewGroup.MarginLayoutParams].
+ * This version of the method allows using named parameters to just set one or more axes.
+ *
+ * @see ViewGroup.MarginLayoutParams.setMargins
+ */
+inline fun ViewGroup.MarginLayoutParams.updateMargins(
+    @Px left: Int = leftMargin,
+    @Px top: Int = topMargin,
+    @Px right: Int = rightMargin,
+    @Px bottom: Int = bottomMargin
+) {
+    setMargins(left, top, right, bottom)
+}
+
+/**
+ * Updates the relative margins in the ViewGroup's MarginLayoutParams.
+ * This version of the method allows using named parameters to just set one or more axes.
+ *
+ * @see ViewGroup.MarginLayoutParams.setMargins
+ */
+@RequiresApi(17)
+inline fun ViewGroup.MarginLayoutParams.updateMarginsRelative(
+    @Px start: Int = marginStart,
+    @Px top: Int = topMargin,
+    @Px end: Int = marginEnd,
+    @Px bottom: Int = bottomMargin
+) {
+    marginStart = start
+    topMargin = top
+    marginEnd = end
+    bottomMargin = bottom
+}
diff --git a/core/ktx/src/main/java/androidx/core/widget/Toast.kt b/core/ktx/src/main/java/androidx/core/widget/Toast.kt
new file mode 100644
index 0000000..b0ddfd1
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/widget/Toast.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.widget
+
+import android.content.Context
+import android.widget.Toast
+import androidx.annotation.StringRes
+
+/**
+ * Creates and shows a [Toast] with the given [text]
+ *
+ * @param duration Toast duration, defaults to [Toast.LENGTH_SHORT]
+ */
+inline fun Context.toast(text: CharSequence, duration: Int = Toast.LENGTH_SHORT): Toast {
+    return Toast.makeText(this, text, duration).apply { show() }
+}
+
+/**
+ * Creates and shows a [Toast] with text from a resource
+ *
+ * @param resId Resource id of the string resource to use
+ * @param duration Toast duration, defaults to [Toast.LENGTH_SHORT]
+ */
+inline fun Context.toast(@StringRes resId: Int, duration: Int = Toast.LENGTH_SHORT): Toast {
+    return Toast.makeText(this, resId, duration).apply { show() }
+}
diff --git a/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java b/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java
index b49afc8..dbe8455 100644
--- a/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java
+++ b/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java
@@ -188,7 +188,7 @@
                 updateHoveredVirtualView(virtualViewId);
                 return (virtualViewId != INVALID_ID);
             case MotionEvent.ACTION_HOVER_EXIT:
-                if (mAccessibilityFocusedVirtualViewId != INVALID_ID) {
+                if (mHoveredVirtualViewId != INVALID_ID) {
                     updateHoveredVirtualView(INVALID_ID);
                     return true;
                 }
diff --git a/emoji/core/api/current.txt b/emoji/core/api/current.txt
index 785f44a..7b343a1 100644
--- a/emoji/core/api/current.txt
+++ b/emoji/core/api/current.txt
@@ -9,6 +9,7 @@
     method public boolean hasEmojiGlyph(java.lang.CharSequence);
     method public boolean hasEmojiGlyph(java.lang.CharSequence, int);
     method public static androidx.emoji.text.EmojiCompat init(androidx.emoji.text.EmojiCompat.Config);
+    method public void load();
     method public java.lang.CharSequence process(java.lang.CharSequence);
     method public java.lang.CharSequence process(java.lang.CharSequence, int, int);
     method public java.lang.CharSequence process(java.lang.CharSequence, int, int, int);
@@ -17,9 +18,12 @@
     method public void unregisterInitCallback(androidx.emoji.text.EmojiCompat.InitCallback);
     field public static final java.lang.String EDITOR_INFO_METAVERSION_KEY = "android.support.text.emoji.emojiCompat_metadataVersion";
     field public static final java.lang.String EDITOR_INFO_REPLACE_ALL_KEY = "android.support.text.emoji.emojiCompat_replaceAll";
+    field public static final int LOAD_STATE_DEFAULT = 3; // 0x3
     field public static final int LOAD_STATE_FAILED = 2; // 0x2
     field public static final int LOAD_STATE_LOADING = 0; // 0x0
     field public static final int LOAD_STATE_SUCCEEDED = 1; // 0x1
+    field public static final int LOAD_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int LOAD_STRATEGY_MANUAL = 1; // 0x1
     field public static final int REPLACE_STRATEGY_ALL = 1; // 0x1
     field public static final int REPLACE_STRATEGY_DEFAULT = 0; // 0x0
     field public static final int REPLACE_STRATEGY_NON_EXISTENT = 2; // 0x2
@@ -31,6 +35,7 @@
     method public androidx.emoji.text.EmojiCompat.Config registerInitCallback(androidx.emoji.text.EmojiCompat.InitCallback);
     method public androidx.emoji.text.EmojiCompat.Config setEmojiSpanIndicatorColor(int);
     method public androidx.emoji.text.EmojiCompat.Config setEmojiSpanIndicatorEnabled(boolean);
+    method public androidx.emoji.text.EmojiCompat.Config setMetadataLoadStrategy(int);
     method public androidx.emoji.text.EmojiCompat.Config setReplaceAll(boolean);
     method public androidx.emoji.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean);
     method public androidx.emoji.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean, java.util.List<java.lang.Integer>);
diff --git a/emoji/core/src/androidTest/java/androidx/emoji/text/ConfigTest.java b/emoji/core/src/androidTest/java/androidx/emoji/text/ConfigTest.java
index 34f31fd..c8d3dad 100644
--- a/emoji/core/src/androidTest/java/androidx/emoji/text/ConfigTest.java
+++ b/emoji/core/src/androidTest/java/androidx/emoji/text/ConfigTest.java
@@ -26,6 +26,7 @@
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -148,9 +149,25 @@
         assertTrue(emojiCompat.isEmojiSpanIndicatorEnabled());
     }
 
+    @Test
+    public void testBuild_manualLoadStrategy_doesNotCallMetadataLoaderLoad() {
+        final EmojiCompat.MetadataRepoLoader loader = mock(EmojiCompat.MetadataRepoLoader.class);
+        final EmojiCompat.Config config = new ValidTestConfig(loader)
+                .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
+
+        EmojiCompat.reset(config);
+
+        verify(loader, never()).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+        assertEquals(EmojiCompat.LOAD_STATE_DEFAULT, EmojiCompat.get().getLoadState());
+    }
+
     private static class ValidTestConfig extends EmojiCompat.Config {
         ValidTestConfig() {
             super(new TestConfigBuilder.TestEmojiDataLoader());
         }
+
+        ValidTestConfig(EmojiCompat.MetadataRepoLoader loader) {
+            super(loader);
+        }
     }
 }
diff --git a/emoji/core/src/androidTest/java/androidx/emoji/text/EmojiCompatTest.java b/emoji/core/src/androidTest/java/androidx/emoji/text/EmojiCompatTest.java
index dd8f3b9..fd9df56 100644
--- a/emoji/core/src/androidTest/java/androidx/emoji/text/EmojiCompatTest.java
+++ b/emoji/core/src/androidTest/java/androidx/emoji/text/EmojiCompatTest.java
@@ -53,6 +53,7 @@
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -691,6 +692,97 @@
         metadataLoader.getLoaderLatch().countDown();
     }
 
+    @Test(expected = IllegalStateException.class)
+    public void testLoad_throwsException_whenLoadStrategyDefault() {
+        final EmojiCompat.MetadataRepoLoader loader = mock(EmojiCompat.MetadataRepoLoader.class);
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader);
+        EmojiCompat.reset(config);
+
+        EmojiCompat.get().load();
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 18)
+    public void testLoad_pre19() {
+        final EmojiCompat.MetadataRepoLoader loader = spy(new TestConfigBuilder
+                .TestEmojiDataLoader());
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
+                .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
+
+        EmojiCompat.reset(config);
+
+        verify(loader, never()).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+        assertEquals(EmojiCompat.LOAD_STATE_DEFAULT, EmojiCompat.get().getLoadState());
+
+        EmojiCompat.get().load();
+        assertEquals(EmojiCompat.LOAD_STATE_SUCCEEDED, EmojiCompat.get().getLoadState());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_startsLoading() {
+        final EmojiCompat.MetadataRepoLoader loader = spy(new TestConfigBuilder
+                .TestEmojiDataLoader());
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
+                .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
+
+        EmojiCompat.reset(config);
+
+        verify(loader, never()).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+        assertEquals(EmojiCompat.LOAD_STATE_DEFAULT, EmojiCompat.get().getLoadState());
+
+        EmojiCompat.get().load();
+        verify(loader, times(1)).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+        assertEquals(EmojiCompat.LOAD_STATE_SUCCEEDED, EmojiCompat.get().getLoadState());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_onceSuccessDoesNotStartLoading() {
+        final EmojiCompat.MetadataRepoLoader loader = spy(new TestConfigBuilder
+                .TestEmojiDataLoader());
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
+                .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
+
+        EmojiCompat.reset(config);
+
+        EmojiCompat.get().load();
+        verify(loader, times(1)).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+        assertEquals(EmojiCompat.LOAD_STATE_SUCCEEDED, EmojiCompat.get().getLoadState());
+
+        reset(loader);
+        EmojiCompat.get().load();
+        verify(loader, never()).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+        assertEquals(EmojiCompat.LOAD_STATE_SUCCEEDED, EmojiCompat.get().getLoadState());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_onceLoadingDoesNotStartLoading() throws InterruptedException {
+        final TestConfigBuilder.WaitingDataLoader loader = spy(
+                new TestConfigBuilder.WaitingDataLoader(true /*success*/));
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
+                .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
+
+        EmojiCompat.reset(config);
+
+        verify(loader, never()).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+
+        EmojiCompat.get().load();
+        verify(loader, times(1)).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+        assertEquals(EmojiCompat.get().getLoadState(), EmojiCompat.LOAD_STATE_LOADING);
+
+        reset(loader);
+
+        EmojiCompat.get().load();
+        verify(loader, never()).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+
+        loader.getLoaderLatch().countDown();
+        loader.getTestLatch().await();
+
+        assertEquals(EmojiCompat.get().getLoadState(), EmojiCompat.LOAD_STATE_SUCCEEDED);
+    }
+
     @Test
     @SdkSuppress(maxSdkVersion = 18)
     public void testGetAssetSignature() {
diff --git a/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiInputFilterTest.java b/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiInputFilterTest.java
index 27ea7c6..2882858 100644
--- a/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiInputFilterTest.java
+++ b/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiInputFilterTest.java
@@ -114,4 +114,16 @@
         verify(mEmojiCompat, times(0)).process(any(Spannable.class), anyInt(), anyInt());
         verify(mEmojiCompat, times(0)).registerInitCallback(any(EmojiCompat.InitCallback.class));
     }
+
+    @Test
+    public void testFilter_withManualLoadStrategy() {
+        final Spannable testString = new SpannableString("abc");
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_DEFAULT);
+
+        final CharSequence result = mInputFilter.filter(testString, 0, 1, null, 0, 1);
+
+        assertNotNull(result);
+        verify(mEmojiCompat, times(0)).process(any(Spannable.class), anyInt(), anyInt());
+        verify(mEmojiCompat, times(1)).registerInitCallback(any(EmojiCompat.InitCallback.class));
+    }
 }
diff --git a/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiTextWatcherTest.java b/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiTextWatcherTest.java
index fade6f7..b8f0154 100644
--- a/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiTextWatcherTest.java
+++ b/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiTextWatcherTest.java
@@ -109,4 +109,15 @@
         verify(mEmojiCompat, times(1)).process(any(Spannable.class), anyInt(), anyInt(), anyInt(),
                 eq(EmojiCompat.REPLACE_STRATEGY_ALL));
     }
+
+    @Test
+    public void testFilter_withManualLoadStrategy() {
+        final Spannable testString = new SpannableString("abc");
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_DEFAULT);
+
+        mTextWatcher.onTextChanged(testString, 0, 0, 1);
+
+        verify(mEmojiCompat, times(0)).process(any(Spannable.class), anyInt(), anyInt());
+        verify(mEmojiCompat, times(1)).registerInitCallback(any(EmojiCompat.InitCallback.class));
+    }
 }
diff --git a/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiTransformationMethodTest.java b/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiTransformationMethodTest.java
new file mode 100644
index 0000000..cf74331
--- /dev/null
+++ b/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiTransformationMethodTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.emoji.widget;
+
+import static junit.framework.TestCase.assertSame;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import static androidx.emoji.util.EmojiMatcher.sameCharSequence;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.method.TransformationMethod;
+import android.view.View;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import androidx.emoji.text.EmojiCompat;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class EmojiTransformationMethodTest {
+
+    private EmojiTransformationMethod mTransformationMethod;
+    private TransformationMethod mWrappedTransformationMethod;
+    private View mView;
+    private EmojiCompat mEmojiCompat;
+    private final String mTestString = "abc";
+
+    @Before
+    public void setup() {
+        mEmojiCompat = mock(EmojiCompat.class);
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_SUCCEEDED);
+        when(mEmojiCompat.process(any(CharSequence.class))).thenAnswer(new Answer<CharSequence>() {
+            @Override
+            public CharSequence answer(InvocationOnMock invocation) {
+                Object[] args = invocation.getArguments();
+                return new SpannableString((String) args[0]);
+            }
+        });
+        EmojiCompat.reset(mEmojiCompat);
+
+        mView = mock(View.class);
+        when(mView.isInEditMode()).thenReturn(false);
+
+        mWrappedTransformationMethod = mock(TransformationMethod.class);
+        when(mWrappedTransformationMethod.getTransformation(any(CharSequence.class),
+                any(View.class))).thenAnswer(new Answer<CharSequence>() {
+            @Override
+            public CharSequence answer(InvocationOnMock invocation) {
+                Object[] args = invocation.getArguments();
+                return (String) args[0];
+            }
+        });
+
+        mTransformationMethod = new EmojiTransformationMethod(mWrappedTransformationMethod);
+    }
+
+    @Test
+    public void testFilter_withNullSource() {
+        assertNull(mTransformationMethod.getTransformation(null, mView));
+        verify(mEmojiCompat, never()).process(any(CharSequence.class));
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testFilter_withNullView() {
+        mTransformationMethod.getTransformation("", null);
+    }
+
+    @Test
+    public void testFilter_withNullTransformationMethod() {
+        mTransformationMethod = new EmojiTransformationMethod(null);
+
+        final CharSequence result = mTransformationMethod.getTransformation(mTestString, mView);
+
+        assertTrue(TextUtils.equals(new SpannableString(mTestString), result));
+        verify(mEmojiCompat, times(1)).process(sameCharSequence(mTestString));
+    }
+
+    @Test
+    public void testFilter() {
+        final CharSequence result = mTransformationMethod.getTransformation(mTestString, mView);
+
+        assertTrue(TextUtils.equals(new SpannableString(mTestString), result));
+        assertTrue(result instanceof Spannable);
+        verify(mWrappedTransformationMethod, times(1)).getTransformation(
+                sameCharSequence(mTestString), same(mView));
+        verify(mEmojiCompat, times(1)).process(sameCharSequence(mTestString));
+        verify(mEmojiCompat, never()).registerInitCallback(any(EmojiCompat.InitCallback.class));
+    }
+
+    @Test
+    public void testFilter_whenEmojiCompatLoading() {
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_LOADING);
+
+        final CharSequence result = mTransformationMethod.getTransformation(mTestString, mView);
+
+        assertSame(mTestString, result);
+        verify(mWrappedTransformationMethod, times(1)).getTransformation(
+                sameCharSequence(mTestString), same(mView));
+        verify(mEmojiCompat, never()).process(sameCharSequence(mTestString));
+        verify(mEmojiCompat, never()).registerInitCallback(any(EmojiCompat.InitCallback.class));
+    }
+
+    @Test
+    public void testFilter_whenEmojiCompatLoadFailed() {
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_FAILED);
+
+        final CharSequence result = mTransformationMethod.getTransformation(mTestString, mView);
+
+        assertSame(mTestString, result);
+        verify(mWrappedTransformationMethod, times(1)).getTransformation(
+                sameCharSequence(mTestString), same(mView));
+        verify(mEmojiCompat, never()).process(sameCharSequence(mTestString));
+        verify(mEmojiCompat, never()).registerInitCallback(any(EmojiCompat.InitCallback.class));
+    }
+
+    @Test
+    public void testFilter_withManualLoadStrategy() {
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_DEFAULT);
+
+        final CharSequence result = mTransformationMethod.getTransformation(mTestString, mView);
+
+        assertSame(mTestString, result);
+        verify(mWrappedTransformationMethod, times(1)).getTransformation(
+                sameCharSequence(mTestString), same(mView));
+        verify(mEmojiCompat, never()).process(sameCharSequence(mTestString));
+        verify(mEmojiCompat, never()).registerInitCallback(any(EmojiCompat.InitCallback.class));
+    }
+}
diff --git a/emoji/core/src/main/java/androidx/emoji/text/EmojiCompat.java b/emoji/core/src/main/java/androidx/emoji/text/EmojiCompat.java
index 31d359d..572606f 100644
--- a/emoji/core/src/main/java/androidx/emoji/text/EmojiCompat.java
+++ b/emoji/core/src/main/java/androidx/emoji/text/EmojiCompat.java
@@ -61,7 +61,9 @@
  * <pre><code>EmojiCompat.init(&#47;* a config instance *&#47;);</code></pre>
  * <p/>
  * It is suggested to make the initialization as early as possible in your app. Please check {@link
- * EmojiCompat.Config} for more configuration parameters.
+ * EmojiCompat.Config} for more configuration parameters. Once {@link #init(EmojiCompat.Config)} is
+ * called a singleton instance will be created. Any call after that will not create a new instance
+ * and will return immediately.
  * <p/>
  * During initialization information about emojis is loaded on a background thread. Before the
  * EmojiCompat instance is initialized, calls to functions such as {@link
@@ -96,6 +98,11 @@
             "android.support.text.emoji.emojiCompat_replaceAll";
 
     /**
+     * EmojiCompat instance is constructed, however the initialization did not start yet.
+     */
+    public static final int LOAD_STATE_DEFAULT = 3;
+
+    /**
      * EmojiCompat is initializing.
      */
     public static final int LOAD_STATE_LOADING = 0;
@@ -115,7 +122,7 @@
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    @IntDef({LOAD_STATE_LOADING, LOAD_STATE_SUCCEEDED, LOAD_STATE_FAILED})
+    @IntDef({LOAD_STATE_DEFAULT, LOAD_STATE_LOADING, LOAD_STATE_SUCCEEDED, LOAD_STATE_FAILED})
     @Retention(RetentionPolicy.SOURCE)
     public @interface LoadState {
     }
@@ -145,6 +152,26 @@
     }
 
     /**
+     * {@link EmojiCompat} will start loading metadata when {@link #init(Config)} is called.
+     */
+    public static final int LOAD_STRATEGY_DEFAULT = 0;
+
+    /**
+     * {@link EmojiCompat} will wait for {@link #load()} to be called by developer in order to
+     * start loading metadata.
+     */
+    public static final int LOAD_STRATEGY_MANUAL = 1;
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @IntDef({LOAD_STRATEGY_DEFAULT, LOAD_STRATEGY_MANUAL})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface LoadStrategy {
+    }
+
+    /**
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
@@ -205,18 +232,25 @@
     private final int mEmojiSpanIndicatorColor;
 
     /**
+     * @see Config#setMetadataLoadStrategy(int)
+     */
+    @LoadStrategy private final int mMetadataLoadStrategy;
+
+    /**
      * Private constructor for singleton instance.
      *
      * @see #init(Config)
      */
     private EmojiCompat(@NonNull final Config config) {
         mInitLock = new ReentrantReadWriteLock();
+        mLoadState = LOAD_STATE_DEFAULT;
         mReplaceAll = config.mReplaceAll;
         mUseEmojiAsDefaultStyle = config.mUseEmojiAsDefaultStyle;
         mEmojiAsDefaultStyleExceptions = config.mEmojiAsDefaultStyleExceptions;
         mEmojiSpanIndicatorEnabled = config.mEmojiSpanIndicatorEnabled;
         mEmojiSpanIndicatorColor = config.mEmojiSpanIndicatorColor;
         mMetadataLoader = config.mMetadataLoader;
+        mMetadataLoadStrategy = config.mMetadataLoadStrategy;
         mMainHandler = new Handler(Looper.getMainLooper());
         mInitCallbacks = new ArraySet<>();
         if (config.mInitCallbacks != null && !config.mInitCallbacks.isEmpty()) {
@@ -230,7 +264,9 @@
     /**
      * Initialize the singleton instance with a configuration. When used on devices running API 18
      * or below, the singleton instance is immediately moved into {@link #LOAD_STATE_SUCCEEDED}
-     * state without loading any metadata.
+     * state without loading any metadata. When called for the first time, the library will create
+     * the singleton instance and any call after that will not create a new instance and return
+     * immediately.
      *
      * @see EmojiCompat.Config
      */
@@ -304,9 +340,30 @@
         }
     }
 
-    private void loadMetadata() {
+    /**
+     * When {@link Config#setMetadataLoadStrategy(int)} is set to {@link #LOAD_STRATEGY_MANUAL},
+     * this function starts loading the metadata. Calling the function when
+     * {@link Config#setMetadataLoadStrategy(int)} is {@code not} set to
+     * {@link #LOAD_STRATEGY_MANUAL} will throw an exception. The load will {@code not} start if:
+     * <ul>
+     *     <li>the metadata is already loaded successfully and {@link #getLoadState()} is
+     *     {@link #LOAD_STATE_SUCCEEDED}.
+     *     </li>
+     *      <li>a previous load attempt is not finished yet and {@link #getLoadState()} is
+     *     {@link #LOAD_STATE_LOADING}.</li>
+     * </ul>
+     *
+     * @throws IllegalStateException when {@link Config#setMetadataLoadStrategy(int)} is not set
+     * to {@link #LOAD_STRATEGY_MANUAL}
+     */
+    public void load() {
+        Preconditions.checkState(mMetadataLoadStrategy == LOAD_STRATEGY_MANUAL,
+                "Set metadataLoadStrategy to LOAD_STRATEGY_MANUAL to execute manual loading");
+        if (isInitialized()) return;
+
         mInitLock.writeLock().lock();
         try {
+            if (mLoadState == LOAD_STATE_LOADING) return;
             mLoadState = LOAD_STATE_LOADING;
         } finally {
             mInitLock.writeLock().unlock();
@@ -315,6 +372,21 @@
         mHelper.loadMetadata();
     }
 
+    private void loadMetadata() {
+        mInitLock.writeLock().lock();
+        try {
+            if (mMetadataLoadStrategy == LOAD_STRATEGY_DEFAULT) {
+                mLoadState = LOAD_STATE_LOADING;
+            }
+        } finally {
+            mInitLock.writeLock().unlock();
+        }
+
+        if (getLoadState() == LOAD_STATE_LOADING) {
+            mHelper.loadMetadata();
+        }
+    }
+
     private void onMetadataLoadSuccess() {
         final Collection<InitCallback> initCallbacks = new ArrayList<>();
         mInitLock.writeLock().lock();
@@ -389,8 +461,8 @@
      * Returns loading state of the EmojiCompat instance. When used on devices running API 18 or
      * below always returns {@link #LOAD_STATE_SUCCEEDED}.
      *
-     * @return one of {@link #LOAD_STATE_LOADING}, {@link #LOAD_STATE_SUCCEEDED},
-     * {@link #LOAD_STATE_FAILED}
+     * @return one of {@link #LOAD_STATE_DEFAULT}, {@link #LOAD_STATE_LOADING},
+     * {@link #LOAD_STATE_SUCCEEDED}, {@link #LOAD_STATE_FAILED}
      */
     public @LoadState int getLoadState() {
         mInitLock.readLock().lock();
@@ -805,6 +877,7 @@
         private Set<InitCallback> mInitCallbacks;
         private boolean mEmojiSpanIndicatorEnabled;
         private int mEmojiSpanIndicatorColor = Color.GREEN;
+        @LoadStrategy private int mMetadataLoadStrategy = LOAD_STRATEGY_DEFAULT;
 
         /**
          * Default constructor.
@@ -938,6 +1011,21 @@
         }
 
         /**
+         * Determines the strategy to start loading the metadata. By default {@link EmojiCompat}
+         * will start loading the metadata during {@link EmojiCompat#init(Config)}. When set to
+         * {@link EmojiCompat#LOAD_STRATEGY_MANUAL}, you should call {@link EmojiCompat#load()} to
+         * initiate metadata loading.
+         *
+         * @param strategy one of {@link EmojiCompat#LOAD_STRATEGY_DEFAULT},
+         *                  {@link EmojiCompat#LOAD_STRATEGY_MANUAL}
+         *
+         */
+        public Config setMetadataLoadStrategy(@LoadStrategy int strategy) {
+            mMetadataLoadStrategy = strategy;
+            return this;
+        }
+
+        /**
          * Returns the {@link MetadataRepoLoader}.
          */
         protected final MetadataRepoLoader getMetadataRepoLoader() {
diff --git a/emoji/core/src/main/java/androidx/emoji/widget/EmojiInputFilter.java b/emoji/core/src/main/java/androidx/emoji/widget/EmojiInputFilter.java
index 22837a6..b44e859 100644
--- a/emoji/core/src/main/java/androidx/emoji/widget/EmojiInputFilter.java
+++ b/emoji/core/src/main/java/androidx/emoji/widget/EmojiInputFilter.java
@@ -79,6 +79,7 @@
 
                 return source;
             case EmojiCompat.LOAD_STATE_LOADING:
+            case EmojiCompat.LOAD_STATE_DEFAULT:
                 EmojiCompat.get().registerInitCallback(getInitCallback());
                 return source;
 
diff --git a/emoji/core/src/main/java/androidx/emoji/widget/EmojiTextWatcher.java b/emoji/core/src/main/java/androidx/emoji/widget/EmojiTextWatcher.java
index 5d0e552..2197aff 100644
--- a/emoji/core/src/main/java/androidx/emoji/widget/EmojiTextWatcher.java
+++ b/emoji/core/src/main/java/androidx/emoji/widget/EmojiTextWatcher.java
@@ -80,6 +80,7 @@
                             mEmojiReplaceStrategy);
                     break;
                 case EmojiCompat.LOAD_STATE_LOADING:
+                case EmojiCompat.LOAD_STATE_DEFAULT:
                     EmojiCompat.get().registerInitCallback(getInitCallback());
                     break;
                 case EmojiCompat.LOAD_STATE_FAILED:
diff --git a/emoji/core/src/main/java/androidx/emoji/widget/EmojiTransformationMethod.java b/emoji/core/src/main/java/androidx/emoji/widget/EmojiTransformationMethod.java
index c082c83..81fe615 100644
--- a/emoji/core/src/main/java/androidx/emoji/widget/EmojiTransformationMethod.java
+++ b/emoji/core/src/main/java/androidx/emoji/widget/EmojiTransformationMethod.java
@@ -57,6 +57,7 @@
                     return EmojiCompat.get().process(source);
                 case EmojiCompat.LOAD_STATE_LOADING:
                 case EmojiCompat.LOAD_STATE_FAILED:
+                case EmojiCompat.LOAD_STATE_DEFAULT:
                 default:
                     break;
             }
diff --git a/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java b/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
index f697f24..d685bf1 100644
--- a/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -3920,7 +3920,7 @@
                         break;
                     }
                     case IFD_FORMAT_USHORT: {
-                        final String[] values = value.split(",");
+                        final String[] values = value.split(",", -1);
                         final int[] intArray = new int[values.length];
                         for (int j = 0; j < values.length; ++j) {
                             intArray[j] = Integer.parseInt(values[j]);
@@ -3930,7 +3930,7 @@
                         break;
                     }
                     case IFD_FORMAT_SLONG: {
-                        final String[] values = value.split(",");
+                        final String[] values = value.split(",", -1);
                         final int[] intArray = new int[values.length];
                         for (int j = 0; j < values.length; ++j) {
                             intArray[j] = Integer.parseInt(values[j]);
@@ -3940,7 +3940,7 @@
                         break;
                     }
                     case IFD_FORMAT_ULONG: {
-                        final String[] values = value.split(",");
+                        final String[] values = value.split(",", -1);
                         final long[] longArray = new long[values.length];
                         for (int j = 0; j < values.length; ++j) {
                             longArray[j] = Long.parseLong(values[j]);
@@ -3950,10 +3950,10 @@
                         break;
                     }
                     case IFD_FORMAT_URATIONAL: {
-                        final String[] values = value.split(",");
+                        final String[] values = value.split(",", -1);
                         final Rational[] rationalArray = new Rational[values.length];
                         for (int j = 0; j < values.length; ++j) {
-                            final String[] numbers = values[j].split("/");
+                            final String[] numbers = values[j].split("/", -1);
                             rationalArray[j] = new Rational((long) Double.parseDouble(numbers[0]),
                                     (long) Double.parseDouble(numbers[1]));
                         }
@@ -3962,10 +3962,10 @@
                         break;
                     }
                     case IFD_FORMAT_SRATIONAL: {
-                        final String[] values = value.split(",");
+                        final String[] values = value.split(",", -1);
                         final Rational[] rationalArray = new Rational[values.length];
                         for (int j = 0; j < values.length; ++j) {
-                            final String[] numbers = values[j].split("/");
+                            final String[] numbers = values[j].split("/", -1);
                             rationalArray[j] = new Rational((long) Double.parseDouble(numbers[0]),
                                     (long) Double.parseDouble(numbers[1]));
                         }
@@ -3974,7 +3974,7 @@
                         break;
                     }
                     case IFD_FORMAT_DOUBLE: {
-                        final String[] values = value.split(",");
+                        final String[] values = value.split(",", -1);
                         final double[] doubleArray = new double[values.length];
                         for (int j = 0; j < values.length; ++j) {
                             doubleArray[j] = Double.parseDouble(values[j]);
@@ -4510,7 +4510,7 @@
         setAttribute(TAG_GPS_SPEED_REF, "K");
         setAttribute(TAG_GPS_SPEED, new Rational(location.getSpeed()
                 * TimeUnit.HOURS.toSeconds(1) / 1000).toString());
-        String[] dateTime = sFormatter.format(new Date(location.getTime())).split("\\s+");
+        String[] dateTime = sFormatter.format(new Date(location.getTime())).split("\\s+", -1);
         setAttribute(ExifInterface.TAG_GPS_DATESTAMP, dateTime[0]);
         setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, dateTime[1]);
     }
@@ -4643,18 +4643,18 @@
 
     private static double convertRationalLatLonToDouble(String rationalString, String ref) {
         try {
-            String [] parts = rationalString.split(",");
+            String [] parts = rationalString.split(",", -1);
 
             String [] pair;
-            pair = parts[0].split("/");
+            pair = parts[0].split("/", -1);
             double degrees = Double.parseDouble(pair[0].trim())
                     / Double.parseDouble(pair[1].trim());
 
-            pair = parts[1].split("/");
+            pair = parts[1].split("/", -1);
             double minutes = Double.parseDouble(pair[0].trim())
                     / Double.parseDouble(pair[1].trim());
 
-            pair = parts[2].split("/");
+            pair = parts[2].split("/", -1);
             double seconds = Double.parseDouble(pair[0].trim())
                     / Double.parseDouble(pair[1].trim());
 
@@ -6017,7 +6017,7 @@
         // See TIFF 6.0 Section 2, "Image File Directory".
         // Take the first component if there are more than one component.
         if (entryValue.contains(",")) {
-            String[] entryValues = entryValue.split(",");
+            String[] entryValues = entryValue.split(",", -1);
             Pair<Integer, Integer> dataFormat = guessDataFormat(entryValues[0]);
             if (dataFormat.first == IFD_FORMAT_STRING) {
                 return dataFormat;
@@ -6049,7 +6049,7 @@
         }
 
         if (entryValue.contains("/")) {
-            String[] rationalNumber = entryValue.split("/");
+            String[] rationalNumber = entryValue.split("/", -1);
             if (rationalNumber.length == 2) {
                 try {
                     long numerator = (long) Double.parseDouble(rationalNumber[0]);
diff --git a/fragment/ktx/OWNERS b/fragment/ktx/OWNERS
new file mode 100644
index 0000000..e450f4c
--- /dev/null
+++ b/fragment/ktx/OWNERS
@@ -0,0 +1 @@
+jakew@google.com
diff --git a/fragment/ktx/build.gradle b/fragment/ktx/build.gradle
new file mode 100644
index 0000000..52cb265
--- /dev/null
+++ b/fragment/ktx/build.gradle
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+    id("org.jetbrains.kotlin.android")
+}
+
+android {
+    buildTypes {
+        debug {
+            testCoverageEnabled = false // Breaks Kotlin compiler.
+        }
+    }
+}
+
+dependencies {
+    api(project(":fragment"))
+    api(KOTLIN_STDLIB)
+    androidTestImplementation(JUNIT)
+    androidTestImplementation(TRUTH)
+    androidTestImplementation(TEST_RUNNER_TMP, libs.exclude_for_espresso)
+    androidTestImplementation(TEST_RULES_TMP, libs.exclude_for_espresso)
+}
+
+supportLibrary {
+    name = "Fragment Kotlin Extensions"
+    publish = true
+    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenGroup = LibraryGroups.FRAGMENT
+    inceptionYear = "2018"
+    description = "Kotlin extensions for 'fragment' artifact"
+}
diff --git a/fragment/ktx/src/androidTest/AndroidManifest.xml b/fragment/ktx/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..0a62e3e
--- /dev/null
+++ b/fragment/ktx/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="androidx.fragment.ktx">
+    <application>
+        <activity android:name="androidx.fragment.app.TestActivity"/>
+    </application>
+</manifest>
diff --git a/fragment/ktx/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt b/fragment/ktx/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt
new file mode 100644
index 0000000..7097955
--- /dev/null
+++ b/fragment/ktx/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt
@@ -0,0 +1,59 @@
+package androidx.fragment.app
+
+import android.support.test.annotation.UiThreadTest
+import android.support.test.filters.MediumTest
+import android.support.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+
+@MediumTest
+class FragmentManagerTest {
+    @get:Rule val activityRule = ActivityTestRule<TestActivity>(TestActivity::class.java)
+    private val fragmentManager get() = activityRule.activity.supportFragmentManager
+
+    @UiThreadTest
+    @Test fun transaction() {
+        val fragment = TestFragment()
+        fragmentManager.transaction {
+            add(fragment, null)
+        }
+        assertThat(fragmentManager.fragments).doesNotContain(fragment)
+        fragmentManager.executePendingTransactions()
+        assertThat(fragmentManager.fragments).contains(fragment)
+    }
+
+    @UiThreadTest
+    @Test fun transactionNow() {
+        val fragment = TestFragment()
+        fragmentManager.transaction(now = true) {
+            add(fragment, null)
+        }
+        assertThat(fragmentManager.fragments).contains(fragment)
+    }
+
+    @UiThreadTest
+    @Test fun transactionAllowingStateLoss() {
+        // Use a detached FragmentManager to ensure state loss.
+        val fragmentManager = FragmentManagerImpl()
+
+        fragmentManager.transaction(allowStateLoss = true) {
+            add(TestFragment(), null)
+        }
+        assertThat(fragmentManager.fragments).isEmpty()
+    }
+
+    @UiThreadTest
+    @Test fun transactionNowAllowingStateLoss() {
+        // Use a detached FragmentManager to ensure state loss.
+        val fragmentManager = FragmentManagerImpl()
+
+        fragmentManager.transaction(now = true, allowStateLoss = true) {
+            add(TestFragment(), null)
+        }
+        assertThat(fragmentManager.fragments).isEmpty()
+    }
+}
+
+class TestActivity : FragmentActivity()
+class TestFragment : Fragment()
diff --git a/fragment/ktx/src/main/AndroidManifest.xml b/fragment/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..cd21eb1
--- /dev/null
+++ b/fragment/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest package="androidx.fragment.ktx"/>
diff --git a/fragment/ktx/src/main/java/androidx/fragment/app/FragmentManager.kt b/fragment/ktx/src/main/java/androidx/fragment/app/FragmentManager.kt
new file mode 100644
index 0000000..d9e8e96
--- /dev/null
+++ b/fragment/ktx/src/main/java/androidx/fragment/app/FragmentManager.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.fragment.app
+
+/**
+ * Run [body] in a [FragmentTransaction] which is automatically committed if it completes without
+ * exception.
+ *
+ * One of four commit functions will be used based on the values of `now` and `allowStateLoss`:
+ *
+ * | `now` | `allowStateLoss` | Method                         |
+ * | ----- | ---------------- | ------------------------------ |
+ * | false | false            | `commit()`                     |
+ * | false | true             | `commitAllowingStateLoss()`    |
+ * | true  | false            | `commitNow()`                  |
+ * | true  | true             | `commitNowAllowingStateLoss()` |
+ */
+inline fun FragmentManager.transaction(
+    now: Boolean = false,
+    allowStateLoss: Boolean = false,
+    body: FragmentTransaction.() -> Unit
+) {
+    val transaction = beginTransaction()
+    transaction.body()
+    if (now) {
+        if (allowStateLoss) {
+            transaction.commitNowAllowingStateLoss()
+        } else {
+            transaction.commitNow()
+        }
+    } else {
+        if (allowStateLoss) {
+            transaction.commitAllowingStateLoss()
+        } else {
+            transaction.commit()
+        }
+    }
+}
diff --git a/heifwriter/api/current.txt b/heifwriter/api/current.txt
index c7311ba..3c39776 100644
--- a/heifwriter/api/current.txt
+++ b/heifwriter/api/current.txt
@@ -1,8 +1,6 @@
 package androidx.heifwriter {
 
   public final class HeifWriter implements java.lang.AutoCloseable {
-    ctor public HeifWriter(java.lang.String, int, int, boolean, int, int, int, int, android.os.Handler) throws java.io.IOException;
-    ctor public HeifWriter(java.io.FileDescriptor, int, int, boolean, int, int, int, int, android.os.Handler) throws java.io.IOException;
     method public void addBitmap(android.graphics.Bitmap);
     method public void addYuvBuffer(int, byte[]);
     method public void close();
@@ -15,5 +13,17 @@
     field public static final int INPUT_MODE_SURFACE = 1; // 0x1
   }
 
+  public static final class HeifWriter.Builder {
+    ctor public HeifWriter.Builder(java.lang.String, int, int, int);
+    ctor public HeifWriter.Builder(java.io.FileDescriptor, int, int, int);
+    method public androidx.heifwriter.HeifWriter build() throws java.io.IOException;
+    method public androidx.heifwriter.HeifWriter.Builder setGridEnabled(boolean);
+    method public androidx.heifwriter.HeifWriter.Builder setHandler(android.os.Handler);
+    method public androidx.heifwriter.HeifWriter.Builder setMaxImages(int);
+    method public androidx.heifwriter.HeifWriter.Builder setPrimaryIndex(int);
+    method public androidx.heifwriter.HeifWriter.Builder setQuality(int);
+    method public androidx.heifwriter.HeifWriter.Builder setRotation(int);
+  }
+
 }
 
diff --git a/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java b/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
index 99f47d2..127ba1c 100644
--- a/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
+++ b/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
@@ -18,12 +18,6 @@
 
 import static android.support.test.InstrumentationRegistry.getContext;
 
-import static androidx.heifwriter.HeifWriter.INPUT_MODE_BITMAP;
-import static androidx.heifwriter.HeifWriter.INPUT_MODE_BUFFER;
-import static androidx.heifwriter.HeifWriter.INPUT_MODE_SURFACE;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
 import android.graphics.Bitmap;
 import android.graphics.ImageFormat;
 import android.media.MediaExtractor;
@@ -34,13 +28,21 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Process;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import android.support.test.filters.LargeTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.util.Log;
+
+import static androidx.heifwriter.HeifWriter.INPUT_MODE_BITMAP;
+import static androidx.heifwriter.HeifWriter.INPUT_MODE_BUFFER;
+import static androidx.heifwriter.HeifWriter.INPUT_MODE_SURFACE;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.heifwriter.test.R;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -222,7 +224,11 @@
     }
 
     private void doTestForVariousNumberImages(TestConfig.Builder builder) throws Exception {
-        doTest(builder.setNumImages(4).build());
+        builder.setNumImages(4);
+        doTest(builder.setRotation(270).build());
+        doTest(builder.setRotation(180).build());
+        doTest(builder.setRotation(90).build());
+        doTest(builder.setRotation(0).build());
         doTest(builder.setNumImages(1).build());
         doTest(builder.setNumImages(8).build());
     }
@@ -250,102 +256,109 @@
     }
 
     private static class TestConfig {
-        final int inputMode;
-        final boolean useGrid;
-        final boolean useHandler;
-        final int maxNumImages;
-        final int numImages;
-        final int width;
-        final int height;
-        final int quality;
-        final String inputPath;
-        final String outputPath;
-        final Bitmap[] bitmaps;
+        final int mInputMode;
+        final boolean mUseGrid;
+        final boolean mUseHandler;
+        final int mMaxNumImages;
+        final int mNumImages;
+        final int mWidth;
+        final int mHeight;
+        final int mRotation;
+        final int mQuality;
+        final String mInputPath;
+        final String mOutputPath;
+        final Bitmap[] mBitmaps;
 
-        TestConfig(int _inputMode, boolean _useGrid, boolean _useHandler,
-                   int _maxNumImage, int _numImages, int _width, int _height, int _quality,
-                   String _inputPath, String _outputPath, Bitmap[] _bitmaps) {
-            inputMode = _inputMode;
-            useGrid = _useGrid;
-            useHandler = _useHandler;
-            maxNumImages = _maxNumImage;
-            numImages = _numImages;
-            width = _width;
-            height = _height;
-            quality = _quality;
-            inputPath = _inputPath;
-            outputPath = _outputPath;
-            bitmaps = _bitmaps;
+        TestConfig(int inputMode, boolean useGrid, boolean useHandler,
+                   int maxNumImages, int numImages, int width, int height,
+                   int rotation, int quality,
+                   String inputPath, String outputPath, Bitmap[] bitmaps) {
+            mInputMode = inputMode;
+            mUseGrid = useGrid;
+            mUseHandler = useHandler;
+            mMaxNumImages = maxNumImages;
+            mNumImages = numImages;
+            mWidth = width;
+            mHeight = height;
+            mRotation = rotation;
+            mQuality = quality;
+            mInputPath = inputPath;
+            mOutputPath = outputPath;
+            mBitmaps = bitmaps;
         }
 
         static class Builder {
-            final int inputMode;
-            final boolean useGrid;
-            final boolean useHandler;
-            int maxNumImages;
-            int numImages;
-            int width;
-            int height;
-            int quality;
-            String inputPath;
-            final String outputPath;
-            Bitmap[] bitmaps;
-
-            boolean numImagesSetExplicitly;
+            final int mInputMode;
+            final boolean mUseGrid;
+            final boolean mUseHandler;
+            int mMaxNumImages;
+            int mNumImages;
+            int mWidth;
+            int mHeight;
+            int mRotation;
+            final int mQuality;
+            String mInputPath;
+            final String mOutputPath;
+            Bitmap[] mBitmaps;
+            boolean mNumImagesSetExplicitly;
 
 
-            Builder(int _inputMode, boolean _useGrids, boolean _useHandler) {
-                inputMode = _inputMode;
-                useGrid = _useGrids;
-                useHandler = _useHandler;
-                maxNumImages = numImages = 4;
-                width = 1920;
-                height = 1080;
-                quality = 100;
-                outputPath = new File(Environment.getExternalStorageDirectory(),
+            Builder(int inputMode, boolean useGrids, boolean useHandler) {
+                mInputMode = inputMode;
+                mUseGrid = useGrids;
+                mUseHandler = useHandler;
+                mMaxNumImages = mNumImages = 4;
+                mWidth = 1920;
+                mHeight = 1080;
+                mRotation = 0;
+                mQuality = 100;
+                mOutputPath = new File(Environment.getExternalStorageDirectory(),
                         OUTPUT_FILENAME).getAbsolutePath();
             }
 
-            Builder setInputPath(String _inputPath) {
-                inputPath = (inputMode == INPUT_MODE_BITMAP) ? _inputPath : null;
+            Builder setInputPath(String inputPath) {
+                mInputPath = (mInputMode == INPUT_MODE_BITMAP) ? inputPath : null;
                 return this;
             }
 
-            Builder setNumImages(int _numImages) {
-                numImagesSetExplicitly = true;
-                numImages = _numImages;
+            Builder setNumImages(int numImages) {
+                mNumImagesSetExplicitly = true;
+                mNumImages = numImages;
+                return this;
+            }
+
+            Builder setRotation(int rotation) {
+                mRotation = rotation;
                 return this;
             }
 
             private void loadBitmapInputs() {
-                if (inputMode != INPUT_MODE_BITMAP) {
+                if (mInputMode != INPUT_MODE_BITMAP) {
                     return;
                 }
                 MediaMetadataRetriever retriever = new MediaMetadataRetriever();
-                retriever.setDataSource(inputPath);
+                retriever.setDataSource(mInputPath);
                 String hasImage = retriever.extractMetadata(
                         MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
                 if (!"yes".equals(hasImage)) {
                     throw new IllegalArgumentException("no bitmap found!");
                 }
-                width = Integer.parseInt(retriever.extractMetadata(
-                        MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH));
-                height = Integer.parseInt(retriever.extractMetadata(
-                        MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT));
-                maxNumImages = Math.min(maxNumImages, Integer.parseInt(retriever.extractMetadata(
+                mMaxNumImages = Math.min(mMaxNumImages, Integer.parseInt(retriever.extractMetadata(
                         MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
-                if (!numImagesSetExplicitly) {
-                    numImages = maxNumImages;
+                if (!mNumImagesSetExplicitly) {
+                    mNumImages = mMaxNumImages;
                 }
-                bitmaps = new Bitmap[maxNumImages];
-                for (int i = 0; i < bitmaps.length; i++) {
-                    bitmaps[i] = retriever.getImageAtIndex(i);
+                mBitmaps = new Bitmap[mMaxNumImages];
+                for (int i = 0; i < mBitmaps.length; i++) {
+                    mBitmaps[i] = retriever.getImageAtIndex(i);
                 }
+                mWidth = mBitmaps[0].getWidth();
+                mHeight = mBitmaps[0].getHeight();
                 retriever.release();
             }
 
             private void cleanupStaleOutputs() {
-                File outputFile = new File(outputPath);
+                File outputFile = new File(mOutputPath);
                 if (outputFile.exists()) {
                     outputFile.delete();
                 }
@@ -355,58 +368,61 @@
                 cleanupStaleOutputs();
                 loadBitmapInputs();
 
-                return new TestConfig(inputMode, useGrid, useHandler, maxNumImages, numImages,
-                        width, height, quality, inputPath, outputPath, bitmaps);
+                return new TestConfig(mInputMode, mUseGrid, mUseHandler, mMaxNumImages, mNumImages,
+                        mWidth, mHeight, mRotation, mQuality, mInputPath, mOutputPath, mBitmaps);
             }
         }
 
         @Override
         public String toString() {
-            return "TestConfig" +
-                    ": inputMode " + inputMode +
-                    ", useGrid " + useGrid +
-                    ", useHandler " + useHandler +
-                    ", maxNumImages " + maxNumImages +
-                    ", numImages " + numImages +
-                    ", width " + width +
-                    ", height " + height +
-                    ", quality " + quality +
-                    ", inputPath " + inputPath +
-                    ", outputPath " + outputPath;
+            return "TestConfig"
+                    + ": mInputMode " + mInputMode
+                    + ", mUseGrid " + mUseGrid
+                    + ", mUseHandler " + mUseHandler
+                    + ", mMaxNumImages " + mMaxNumImages
+                    + ", mNumImages " + mNumImages
+                    + ", mWidth " + mWidth
+                    + ", mHeight " + mHeight
+                    + ", mRotation " + mRotation
+                    + ", mQuality " + mQuality
+                    + ", mInputPath " + mInputPath
+                    + ", mOutputPath " + mOutputPath;
         }
     }
 
-    private void doTest(TestConfig testConfig) throws Exception {
-        int width = testConfig.width;
-        int height = testConfig.height;
-        int numImages = testConfig.numImages;
+    private void doTest(TestConfig config) throws Exception {
+        int width = config.mWidth;
+        int height = config.mHeight;
+        int numImages = config.mNumImages;
 
         mInputIndex = 0;
         HeifWriter heifWriter = null;
         FileInputStream inputStream = null;
         FileOutputStream outputStream = null;
         try {
-            if (DEBUG) Log.d(TAG, "started: " + testConfig);
+            if (DEBUG) Log.d(TAG, "started: " + config);
 
-            heifWriter = new HeifWriter(testConfig.outputPath, width, height,
-                    testConfig.useGrid,
-                    testConfig.quality,
-                    testConfig.maxNumImages,
-                    testConfig.maxNumImages - 1,
-                    testConfig.inputMode,
-                    testConfig.useHandler ? mHandler : null);
+            heifWriter = new HeifWriter.Builder(
+                    config.mOutputPath, width, height, config.mInputMode)
+                    .setRotation(config.mRotation)
+                    .setGridEnabled(config.mUseGrid)
+                    .setMaxImages(config.mMaxNumImages)
+                    .setQuality(config.mQuality)
+                    .setPrimaryIndex(config.mMaxNumImages - 1)
+                    .setHandler(config.mUseHandler ? mHandler : null)
+                    .build();
 
-            if (testConfig.inputMode == INPUT_MODE_SURFACE) {
+            if (config.mInputMode == INPUT_MODE_SURFACE) {
                 mInputEglSurface = new EglWindowSurface(heifWriter.getInputSurface());
             }
 
             heifWriter.start();
 
-            if (testConfig.inputMode == INPUT_MODE_BUFFER) {
+            if (config.mInputMode == INPUT_MODE_BUFFER) {
                 byte[] data = new byte[width * height * 3 / 2];
 
-                if (testConfig.inputPath != null) {
-                    inputStream = new FileInputStream(testConfig.inputPath);
+                if (config.mInputPath != null) {
+                    inputStream = new FileInputStream(config.mInputPath);
                 }
 
                 if (DUMP_YUV_INPUT) {
@@ -424,7 +440,7 @@
                     }
                     heifWriter.addYuvBuffer(ImageFormat.YUV_420_888, data);
                 }
-            } else if (testConfig.inputMode == INPUT_MODE_SURFACE) {
+            } else if (config.mInputMode == INPUT_MODE_SURFACE) {
                 // The input surface is a surface texture using single buffer mode, draws will be
                 // blocked until onFrameAvailable is done with the buffer, which is dependant on
                 // how fast MediaCodec processes them, which is further dependent on how fast the
@@ -436,8 +452,8 @@
                 }
                 heifWriter.setInputEndOfStreamTimestamp(
                         1000 * computePresentationTime(numImages - 1));
-            } else if (testConfig.inputMode == INPUT_MODE_BITMAP) {
-                Bitmap[] bitmaps = testConfig.bitmaps;
+            } else if (config.mInputMode == INPUT_MODE_BITMAP) {
+                Bitmap[] bitmaps = config.mBitmaps;
                 for (int i = 0; i < Math.min(bitmaps.length, numImages); i++) {
                     if (DEBUG) Log.d(TAG, "addBitmap: " + i);
                     heifWriter.addBitmap(bitmaps[i]);
@@ -446,8 +462,8 @@
             }
 
             heifWriter.stop(3000);
-            verifyResult(testConfig.outputPath, width, height, testConfig.useGrid,
-                    Math.min(numImages, testConfig.maxNumImages));
+            verifyResult(config.mOutputPath, width, height, config.mRotation, config.mUseGrid,
+                    Math.min(numImages, config.mMaxNumImages));
             if (DEBUG) Log.d(TAG, "finished: PASS");
         } finally {
             try {
@@ -530,7 +546,7 @@
     }
 
     private void verifyResult(
-            String filename, int width, int height, boolean useGrid, int numImages)
+            String filename, int width, int height, int rotation, boolean useGrid, int numImages)
             throws Exception {
         MediaMetadataRetriever retriever = new MediaMetadataRetriever();
         retriever.setDataSource(filename);
@@ -540,23 +556,30 @@
         }
         assertEquals("Wrong image count", numImages,
                 Integer.parseInt(retriever.extractMetadata(
-                MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
+                    MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
         assertEquals("Wrong width", width,
                 Integer.parseInt(retriever.extractMetadata(
-                MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH)));
+                    MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH)));
         assertEquals("Wrong height", height,
                 Integer.parseInt(retriever.extractMetadata(
-                MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT)));
+                    MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT)));
+        assertEquals("Wrong rotation", rotation,
+                Integer.parseInt(retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION)));
         retriever.release();
 
         if (useGrid) {
             MediaExtractor extractor = new MediaExtractor();
             extractor.setDataSource(filename);
             MediaFormat format = extractor.getTrackFormat(0);
-            int gridWidth = format.getInteger(MediaFormat.KEY_GRID_WIDTH);
-            int gridHeight = format.getInteger(MediaFormat.KEY_GRID_HEIGHT);
-            assertEquals("Wrong grid width", 512, gridWidth);
-            assertEquals("Wrong grid height", 512, gridHeight);
+            int gridWidth = format.getInteger(MediaFormat.KEY_TILE_WIDTH);
+            int gridHeight = format.getInteger(MediaFormat.KEY_TILE_HEIGHT);
+            int gridRows = format.getInteger(MediaFormat.KEY_GRID_ROWS);
+            int gridCols = format.getInteger(MediaFormat.KEY_GRID_COLUMNS);
+            assertTrue("Wrong grid width or cols",
+                    ((width + gridWidth - 1) / gridWidth) == gridCols);
+            assertTrue("Wrong grid height or rows",
+                    ((height + gridHeight - 1) / gridHeight) == gridRows);
             extractor.release();
         }
     }
diff --git a/heifwriter/src/main/java/androidx/heifwriter/EglWindowSurface.java b/heifwriter/src/main/java/androidx/heifwriter/EglWindowSurface.java
index 9b5df45..35d34d4 100644
--- a/heifwriter/src/main/java/androidx/heifwriter/EglWindowSurface.java
+++ b/heifwriter/src/main/java/androidx/heifwriter/EglWindowSurface.java
@@ -25,6 +25,8 @@
 import android.util.Log;
 import android.view.Surface;
 
+import java.util.Objects;
+
 /**
  * Holds state associated with a Surface used for MediaCodec encoder input.
  * <p>
@@ -63,7 +65,7 @@
      */
     private void eglSetup() {
         mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
-        if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
+        if (Objects.equals(mEGLDisplay, EGL14.EGL_NO_DISPLAY)) {
             throw new RuntimeException("unable to get EGL14 display");
         }
         int[] version = new int[2];
@@ -130,7 +132,7 @@
     }
 
     private void releaseEGLSurface() {
-        if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
+        if (!Objects.equals(mEGLDisplay, EGL14.EGL_NO_DISPLAY)) {
             EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface);
             mEGLSurface = EGL14.EGL_NO_SURFACE;
         }
@@ -141,7 +143,7 @@
      * Surface that was passed to our constructor.
      */
     public void release() {
-        if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
+        if (!Objects.equals(mEGLDisplay, EGL14.EGL_NO_DISPLAY)) {
             EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface);
             EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);
             EGL14.eglReleaseThread();
diff --git a/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java b/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
index 85aa925..be5425a 100644
--- a/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
+++ b/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
@@ -17,14 +17,9 @@
 package androidx.heifwriter;
 
 import android.graphics.Bitmap;
+import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.graphics.SurfaceTexture;
-import android.opengl.GLES20;
-import android.opengl.GLUtils;
-import android.os.Looper;
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import android.media.Image;
 import android.media.MediaCodec;
 import android.media.MediaCodec.BufferInfo;
@@ -32,13 +27,19 @@
 import android.media.MediaCodecInfo;
 import android.media.MediaCodecInfo.CodecCapabilities;
 import android.media.MediaFormat;
+import android.opengl.GLES20;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.Looper;
 import android.os.Process;
 import android.util.Log;
 import android.util.Range;
 import android.view.Surface;
 
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -179,25 +180,29 @@
             throw new IllegalArgumentException("invalid encoder inputs");
         }
 
-        mEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_HEVC);
+        // Disable grid if the image is too small
+        useGrid &= (width > GRID_WIDTH || height > GRID_HEIGHT);
 
-        mWidth = width;
-        mHeight = height;
-
-        if (useGrid) {
-            mGridWidth = GRID_WIDTH;
-            mGridHeight = GRID_HEIGHT;
-            mGridRows = (height + GRID_HEIGHT - 1) / GRID_HEIGHT;
-            mGridCols = (width + GRID_WIDTH - 1) / GRID_WIDTH;
-        } else {
-            mGridWidth = mWidth;
-            mGridHeight = mHeight;
-            mGridRows = 1;
-            mGridCols = 1;
+        boolean useHeicEncoder = false;
+        MediaCodecInfo.CodecCapabilities caps = null;
+        try {
+            mEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
+            caps = mEncoder.getCodecInfo().getCapabilitiesForType(
+                    MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
+            // If the HEIC encoder can't support the size, fall back to HEVC encoder.
+            if (!caps.getVideoCapabilities().isSizeSupported(width, height)) {
+                mEncoder.release();
+                mEncoder = null;
+                throw new Exception();
+            }
+            useHeicEncoder = true;
+        } catch (Exception e) {
+            mEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_HEVC);
+            caps = mEncoder.getCodecInfo().getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_HEVC);
+            // Always enable grid if the size is too large for the HEVC encoder
+            useGrid |= !caps.getVideoCapabilities().isSizeSupported(width, height);
         }
 
-        mNumTiles = mGridRows * mGridCols;
-
         mInputMode = inputMode;
 
         mCallback = cb;
@@ -217,18 +222,59 @@
         int colorFormat = useSurfaceInternally ? CodecCapabilities.COLOR_FormatSurface :
                 CodecCapabilities.COLOR_FormatYUV420Flexible;
 
-        // TODO: determine how to set bitrate and framerate, or use constant quality
-        MediaFormat codecFormat = MediaFormat.createVideoFormat(
-                MediaFormat.MIMETYPE_VIDEO_HEVC, mGridWidth, mGridHeight);
+        mWidth = width;
+        mHeight = height;
+
+        int gridWidth, gridHeight, gridRows, gridCols;
+
+        if (useGrid) {
+            gridWidth = GRID_WIDTH;
+            gridHeight = GRID_HEIGHT;
+            gridRows = (height + GRID_HEIGHT - 1) / GRID_HEIGHT;
+            gridCols = (width + GRID_WIDTH - 1) / GRID_WIDTH;
+        } else {
+            gridWidth = mWidth;
+            gridHeight = mHeight;
+            gridRows = 1;
+            gridCols = 1;
+        }
+
+        MediaFormat codecFormat;
+        if (useHeicEncoder) {
+            codecFormat = MediaFormat.createVideoFormat(
+                    MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC, mWidth, mHeight);
+        } else {
+            codecFormat = MediaFormat.createVideoFormat(
+                    MediaFormat.MIMETYPE_VIDEO_HEVC, gridWidth, gridHeight);
+        }
+
+        if (useGrid) {
+            codecFormat.setInteger(MediaFormat.KEY_TILE_WIDTH, gridWidth);
+            codecFormat.setInteger(MediaFormat.KEY_TILE_HEIGHT, gridHeight);
+            codecFormat.setInteger(MediaFormat.KEY_GRID_COLUMNS, gridCols);
+            codecFormat.setInteger(MediaFormat.KEY_GRID_ROWS, gridRows);
+        }
+
+        if (useHeicEncoder) {
+            mGridWidth = width;
+            mGridHeight = height;
+            mGridRows = 1;
+            mGridCols = 1;
+        } else {
+            mGridWidth = gridWidth;
+            mGridHeight = gridHeight;
+            mGridRows = gridRows;
+            mGridCols = gridCols;
+        }
+        mNumTiles = mGridRows * mGridCols;
+
         codecFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 0);
         codecFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
-
-        MediaCodecInfo.CodecCapabilities caps =
-                mEncoder.getCodecInfo().getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_HEVC);
-        MediaCodecInfo.EncoderCapabilities encoderCaps = caps.getEncoderCapabilities();
-
         codecFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mNumTiles);
         codecFormat.setInteger(MediaFormat.KEY_CAPTURE_RATE, mNumTiles * 30);
+
+        MediaCodecInfo.EncoderCapabilities encoderCaps = caps.getEncoderCapabilities();
+
         if (encoderCaps.isBitrateModeSupported(
                 MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) {
             Log.d(TAG, "Setting bitrate mode to constant quality");
@@ -240,14 +286,14 @@
                             (qualityRange.getUpper() - qualityRange.getLower()) * quality / 100.0));
         } else {
             if (encoderCaps.isBitrateModeSupported(
-                    MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR)) {
-                Log.d(TAG, "Setting bitrate mode to variable bitrate");
-                codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
-                        MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
-            } else { // assume CBR
+                    MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR)) {
                 Log.d(TAG, "Setting bitrate mode to constant bitrate");
                 codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
                         MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
+            } else { // assume VBR
+                Log.d(TAG, "Setting bitrate mode to variable bitrate");
+                codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
+                        MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
             }
             // Calculate the bitrate based on image dimension, max compression ratio and quality.
             // Note that we set the frame rate to the number of tiles, so the bitrate would be the
@@ -262,34 +308,36 @@
         if (useSurfaceInternally) {
             mEncoderSurface = mEncoder.createInputSurface();
 
-            boolean useGLCopy = (mNumTiles > 1) || (inputMode == INPUT_MODE_BITMAP);
-            mEOSTracker = new SurfaceEOSTracker(useGLCopy);
+            boolean copyTiles = (mNumTiles > 1);
+            mEOSTracker = new SurfaceEOSTracker(copyTiles);
 
-            if (useGLCopy) {
-                mEncoderEglSurface = new EglWindowSurface(mEncoderSurface);
-                mEncoderEglSurface.makeCurrent();
+            if (inputMode == INPUT_MODE_SURFACE) {
+                if (copyTiles) {
+                    mEncoderEglSurface = new EglWindowSurface(mEncoderSurface);
+                    mEncoderEglSurface.makeCurrent();
 
-                mRectBlt = new EglRectBlt(
-                        new Texture2dProgram((inputMode == INPUT_MODE_BITMAP) ?
-                                Texture2dProgram.TEXTURE_2D :
-                                Texture2dProgram.TEXTURE_EXT),
-                        mWidth, mHeight);
+                    mRectBlt = new EglRectBlt(
+                            new Texture2dProgram((inputMode == INPUT_MODE_BITMAP)
+                                    ? Texture2dProgram.TEXTURE_2D
+                                    : Texture2dProgram.TEXTURE_EXT),
+                            mWidth, mHeight);
 
-                mTextureId = mRectBlt.createTextureObject();
+                    mTextureId = mRectBlt.createTextureObject();
 
-                if (inputMode == INPUT_MODE_SURFACE) {
-                    // use single buffer mode to block on input
-                    mInputTexture = new SurfaceTexture(mTextureId, true);
-                    mInputTexture.setOnFrameAvailableListener(this);
-                    mInputTexture.setDefaultBufferSize(mWidth, mHeight);
-                    mInputSurface = new Surface(mInputTexture);
+                    if (inputMode == INPUT_MODE_SURFACE) {
+                        // use single buffer mode to block on input
+                        mInputTexture = new SurfaceTexture(mTextureId, true);
+                        mInputTexture.setOnFrameAvailableListener(this);
+                        mInputTexture.setDefaultBufferSize(mWidth, mHeight);
+                        mInputSurface = new Surface(mInputTexture);
+                    }
+
+                    // make uncurrent since onFrameAvailable could be called on arbituray thread.
+                    // making the context current on a different thread will cause error.
+                    mEncoderEglSurface.makeUnCurrent();
+                } else {
+                    mInputSurface = mEncoderSurface;
                 }
-
-                // make uncurrent since the onFrameAvailable could be called on arbituray thread.
-                // making the context current on a different thread will cause error.
-                mEncoderEglSurface.makeUnCurrent();
-            } else {
-                mInputSurface = mEncoderSurface;
             }
         } else {
             for (int i = 0; i < INPUT_BUFFER_POOL_SIZE; i++) {
@@ -321,7 +369,20 @@
                     computePresentationTime(mInputIndex + mNumTiles - 1));
 
             if (takeFrame) {
-                copyTilesGL(mTmpMatrix);
+                // Copies from surface texture to encoder inputs using GL.
+                GLES20.glViewport(0, 0, mGridWidth, mGridHeight);
+
+                for (int row = 0; row < mGridRows; row++) {
+                    for (int col = 0; col < mGridCols; col++) {
+                        int left = col * mGridWidth;
+                        int top = row * mGridHeight;
+                        mSrcRect.set(left, top, left + mGridWidth, top + mGridHeight);
+                        mRectBlt.copyRect(mTextureId, mTmpMatrix, mSrcRect);
+                        mEncoderEglSurface.setPresentationTime(
+                                1000 * computePresentationTime(mInputIndex++));
+                        mEncoderEglSurface.swapBuffers();
+                    }
+                }
             }
 
             surfaceTexture.releaseTexImage();
@@ -407,21 +468,16 @@
         if (!takeFrame) return;
 
         synchronized (this) {
-            if (mEncoderEglSurface == null) {
-                return;
+            for (int row = 0; row < mGridRows; row++) {
+                for (int col = 0; col < mGridCols; col++) {
+                    int left = col * mGridWidth;
+                    int top = row * mGridHeight;
+                    mSrcRect.set(left, top, left + mGridWidth, top + mGridHeight);
+                    Canvas canvas = mEncoderSurface.lockCanvas(null);
+                    canvas.drawBitmap(bitmap, mSrcRect, mDstRect, null);
+                    mEncoderSurface.unlockCanvasAndPost(canvas);
+                }
             }
-
-            mEncoderEglSurface.makeCurrent();
-
-            // load the bitmap to texture
-            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId);
-            GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
-
-            copyTilesGL(Texture2dProgram.V_FLIP_MATRIX);
-
-            // make uncurrent since the onFrameAvailable could be called on arbituray thread.
-            // making the context current on a different thread will cause error.
-            mEncoderEglSurface.makeUnCurrent();
         }
     }
 
@@ -594,28 +650,6 @@
     }
 
     /**
-     * Copies from source frame to encoder inputs using GL. The source could be either
-     * client's input surface, or the input bitmap loaded to texture.
-     *
-     * @param texMatrix The texture matrix to use. See the shader program in
-     * {@link Texture2dProgram} as well as {@link SurfaceTexture} for more details.
-     */
-    private void copyTilesGL(float[] texMatrix) {
-        GLES20.glViewport(0, 0, mGridWidth, mGridHeight);
-
-        for (int row = 0; row < mGridRows; row++) {
-            for (int col = 0; col < mGridCols; col++) {
-                int left = col * mGridWidth;
-                int top = row * mGridHeight;
-                mSrcRect.set(left, top, left + mGridWidth, top + mGridHeight);
-                mRectBlt.copyRect(mTextureId, texMatrix, mSrcRect);
-                mEncoderEglSurface.setPresentationTime(1000 * computePresentationTime(mInputIndex++));
-                mEncoderEglSurface.swapBuffers();
-            }
-        }
-    }
-
-    /**
      * Routine to release all resources. Must be run on the same looper that
      * handles the MediaCodec callbacks.
      */
@@ -677,7 +711,7 @@
     private class SurfaceEOSTracker {
         private static final boolean DEBUG_EOS = false;
 
-        final boolean mUseGLCopy;
+        final boolean mCopyTiles;
         long mInputEOSTimeNs = -1;
         long mLastInputTimeNs = -1;
         long mEncoderEOSTimeUs = -1;
@@ -685,14 +719,14 @@
         long mLastOutputTimeUs = -1;
         boolean mSignaled;
 
-        SurfaceEOSTracker(boolean useGLCopy) {
-            mUseGLCopy = useGLCopy;
+        SurfaceEOSTracker(boolean copyTiles) {
+            mCopyTiles = copyTiles;
         }
 
         synchronized void updateInputEOSTime(long timestampNs) {
             if (DEBUG_EOS) Log.d(TAG, "updateInputEOSTime: " + timestampNs);
 
-            if (mUseGLCopy) {
+            if (mCopyTiles) {
                 if (mInputEOSTimeNs < 0) {
                     mInputEOSTimeNs = timestampNs;
                 }
@@ -773,15 +807,18 @@
 
             if (DEBUG) Log.d(TAG, "onOutputFormatChanged: " + format);
 
-            format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
-            format.setInteger(MediaFormat.KEY_WIDTH, mWidth);
-            format.setInteger(MediaFormat.KEY_HEIGHT, mHeight);
+            if (!MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC.equals(
+                    format.getString(MediaFormat.KEY_MIME))) {
+                format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
+                format.setInteger(MediaFormat.KEY_WIDTH, mWidth);
+                format.setInteger(MediaFormat.KEY_HEIGHT, mHeight);
 
-            if (mNumTiles > 1) {
-                format.setInteger(MediaFormat.KEY_GRID_WIDTH, mGridWidth);
-                format.setInteger(MediaFormat.KEY_GRID_HEIGHT, mGridHeight);
-                format.setInteger(MediaFormat.KEY_GRID_ROWS, mGridRows);
-                format.setInteger(MediaFormat.KEY_GRID_COLS, mGridCols);
+                if (mNumTiles > 1) {
+                    format.setInteger(MediaFormat.KEY_TILE_WIDTH, mGridWidth);
+                    format.setInteger(MediaFormat.KEY_TILE_HEIGHT, mGridHeight);
+                    format.setInteger(MediaFormat.KEY_GRID_ROWS, mGridRows);
+                    format.setInteger(MediaFormat.KEY_GRID_COLUMNS, mGridCols);
+                }
             }
 
             mCallback.onOutputFormatChanged(HeifEncoder.this, format);
@@ -800,8 +837,12 @@
         public void onOutputBufferAvailable(MediaCodec codec, int index, BufferInfo info) {
             if (codec != mEncoder || mOutputEOS) return;
 
-            if (DEBUG) Log.d(TAG, "onInputBufferAvailable: " + index + ", time "
-                    + info.presentationTimeUs + ", size " + info.size + ", flags " + info.flags);
+            if (DEBUG) {
+                Log.d(TAG, "onOutputBufferAvailable: " + index
+                        + ", time " + info.presentationTimeUs
+                        + ", size " + info.size
+                        + ", flags " + info.flags);
+            }
 
             if ((info.size > 0) && ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0)) {
                 ByteBuffer outputBuffer = codec.getOutputBuffer(index);
diff --git a/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java b/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
index be7dffb..bc657c4 100644
--- a/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
+++ b/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
@@ -16,6 +16,8 @@
 
 package androidx.heifwriter;
 
+import static android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_HEIF;
+
 import android.annotation.SuppressLint;
 import android.graphics.Bitmap;
 import android.media.MediaCodec;
@@ -25,11 +27,12 @@
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Process;
+import android.util.Log;
+import android.view.Surface;
+
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import android.util.Log;
-import android.view.Surface;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -38,8 +41,6 @@
 import java.nio.ByteBuffer;
 import java.util.concurrent.TimeoutException;
 
-import static android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_HEIF;
-
 /**
  * This class writes one or more still images (of the same dimensions) into
  * a heif file.
@@ -79,7 +80,8 @@
     private final HandlerThread mHandlerThread;
     private final Handler mHandler;
     private int mNumTiles;
-    private final int mNumImages;
+    private final int mRotation;
+    private final int mMaxImages;
     private final int mPrimaryIndex;
     private final ResultWaiter mResultWaiter = new ResultWaiter();
 
@@ -119,96 +121,200 @@
     public @interface InputMode {}
 
     /**
-     * Construct a heif writer that writes to a file specified by its path.
-     *
-     * @param path Path of the file to be written.
-     * @param width Width of the image.
-     * @param height Height of the image.
-     * @param useGrid Whether to encode image into tiles. If enabled, the tile size will be
-     *                automatically chosen.
-     * @param quality A number between 0 and 100 (inclusive), with 100 indicating the best quality
-     *                supported by this implementation (which often results in larger file size).
-     * @param numImages Max number of images to write. Frames exceeding this number will not be
-     *                  written to file. The writing can be stopped earlier before this number of
-     *                  images are written by {@link #stop(long)}, except for the input mode of
-     *                  {@link #INPUT_MODE_SURFACE}, where the EOS timestamp must be specified (via
-     *                 {@link #setInputEndOfStreamTimestamp(long)} and reached.
-     * @param primaryIndex Index of the image that should be marked as primary, must be within range
-     *                     [0, numImages - 1] inclusive.
-     * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
-     *                  {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
-     * @param handler If not null, client will receive all callbacks on the handler's looper.
-     *                Otherwise, client will receive callbacks on a looper created by the writer.
-     *
-     * @throws IOException if failed to construct MediaMuxer or HeifEncoder.
+     * Builder class for constructing a HeifWriter object from specified parameters.
      */
-    @SuppressLint("WrongConstant")
-    public HeifWriter(@NonNull String path,
-                      int width, int height, boolean useGrid,
-                      int quality, int numImages, int primaryIndex,
-                      @InputMode int inputMode,
-                      @Nullable Handler handler) throws IOException {
-        this(width, height, useGrid, quality, numImages, primaryIndex, inputMode, handler,
-                new MediaMuxer(path, MUXER_OUTPUT_HEIF));
+    public static final class Builder {
+        private final String mPath;
+        private final FileDescriptor mFd;
+        private final int mWidth;
+        private final int mHeight;
+        private final @InputMode int mInputMode;
+        private boolean mGridEnabled = true;
+        private int mQuality = 100;
+        private int mMaxImages = 1;
+        private int mPrimaryIndex = 0;
+        private int mRotation = 0;
+        private Handler mHandler;
+
+        /**
+         * Construct a Builder with output specified by its path.
+         *
+         * @param path Path of the file to be written.
+         * @param width Width of the image.
+         * @param height Height of the image.
+         * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
+         *                  {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
+         */
+        public Builder(@NonNull String path,
+                       int width, int height, @InputMode int inputMode) {
+            this(path, null, width, height, inputMode);
+        }
+
+        /**
+         * Construct a Builder with output specified by its file descriptor.
+         *
+         * @param fd File descriptor of the file to be written.
+         * @param width Width of the image.
+         * @param height Height of the image.
+         * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
+         *                  {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
+         */
+        public Builder(@NonNull FileDescriptor fd,
+                       int width, int height, @InputMode int inputMode) {
+            this(null, fd, width, height, inputMode);
+        }
+
+        private Builder(String path, FileDescriptor fd,
+                        int width, int height, @InputMode int inputMode) {
+            if (width <= 0 || height <= 0) {
+                throw new IllegalArgumentException("Invalid image size: " + width + "x" + height);
+            }
+            mPath = path;
+            mFd = fd;
+            mWidth = width;
+            mHeight = height;
+            mInputMode = inputMode;
+        }
+
+        /**
+         * Set the image rotation in degrees.
+         *
+         * @param rotation Rotation angle (clockwise) of the image, must be 0, 90, 180 or 270.
+         *                 Default is 0.
+         * @return this Builder object.
+         */
+        public Builder setRotation(int rotation) {
+            if (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270) {
+                throw new IllegalArgumentException("Invalid rotation angle: " + rotation);
+            }
+            mRotation = rotation;
+            return this;
+        }
+
+        /**
+         * Set whether to enable grid option.
+         *
+         * @param gridEnabled Whether to enable grid option. If enabled, the tile size will be
+         *                    automatically chosen. Default is to enable.
+         * @return this Builder object.
+         */
+        public Builder setGridEnabled(boolean gridEnabled) {
+            mGridEnabled = gridEnabled;
+            return this;
+        }
+
+        /**
+         * Set the quality for encoding images.
+         *
+         * @param quality A number between 0 and 100 (inclusive), with 100 indicating the best
+         *                quality supported by this implementation. Default is 100.
+         * @return this Builder object.
+         */
+        public Builder setQuality(int quality) {
+            if (quality < 0 || quality > 100) {
+                throw new IllegalArgumentException("Invalid quality: " + quality);
+            }
+            mQuality = quality;
+            return this;
+        }
+
+        /**
+         * Set the maximum number of images to write.
+         *
+         * @param maxImages Max number of images to write. Frames exceeding this number will not be
+         *                  written to file. The writing can be stopped earlier before this number
+         *                  of images are written by {@link #stop(long)}, except for the input mode
+         *                  of {@link #INPUT_MODE_SURFACE}, where the EOS timestamp must be
+         *                  specified (via {@link #setInputEndOfStreamTimestamp(long)} and reached.
+         *                  Default is 1.
+         * @return this Builder object.
+         */
+        public Builder setMaxImages(int maxImages) {
+            if (maxImages <= 0) {
+                throw new IllegalArgumentException("Invalid maxImage: " + maxImages);
+            }
+            mMaxImages = maxImages;
+            return this;
+        }
+
+        /**
+         * Set the primary image index.
+         *
+         * @param primaryIndex Index of the image that should be marked as primary, must be within
+         *                     range [0, maxImages - 1] inclusive. Default is 0.
+         * @return this Builder object.
+         */
+        public Builder setPrimaryIndex(int primaryIndex) {
+            if (primaryIndex < 0) {
+                throw new IllegalArgumentException("Invalid primaryIndex: " + primaryIndex);
+            }
+            mPrimaryIndex = primaryIndex;
+            return this;
+        }
+
+        /**
+         * Provide a handler for the HeifWriter to use.
+         *
+         * @param handler If not null, client will receive all callbacks on the handler's looper.
+         *                Otherwise, client will receive callbacks on a looper created by the
+         *                writer. Default is null.
+         * @return this Builder object.
+         */
+        public Builder setHandler(@Nullable Handler handler) {
+            mHandler = handler;
+            return this;
+        }
+
+        /**
+         * Build a HeifWriter object.
+         *
+         * @return a HeifWriter object built according to the specifications.
+         * @throws IOException if failed to create the writer, possibly due to failure to create
+         *                     {@link android.media.MediaMuxer} or {@link android.media.MediaCodec}.
+         */
+        public HeifWriter build() throws IOException {
+            return new HeifWriter(mPath, mFd, mWidth, mHeight, mRotation, mGridEnabled, mQuality,
+                    mMaxImages, mPrimaryIndex, mInputMode, mHandler);
+        }
     }
 
-    /**
-     * Construct a heif writer that writes to a file specified by file descriptor.
-     *
-     * @param fd File descriptor of the file to be written.
-     * @param width Width of the image.
-     * @param height Height of the image.
-     * @param useGrid Whether to encode image into tiles. If enabled, the tile size will be
-     *                automatically chosen.
-     * @param quality A number between 0 and 100 (inclusive), with 100 indicating the best quality
-     *                supported by this implementation (which often results in larger file size).
-     * @param numImages Max number of images to write. Frames exceeding this number will not be
-     *                  written to file. The writing can be stopped earlier before this number of
-     *                  images are written by {@link #stop(long)}, except for the input mode of
-     *                  {@link #INPUT_MODE_SURFACE}, where the EOS timestamp must be specified (via
-     *                 {@link #setInputEndOfStreamTimestamp(long)} and reached.
-     * @param primaryIndex Index of the image that should be marked as primary, must be within range
-     *                     [0, numImages - 1] inclusive.
-     * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
-     *                  {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
-     * @param handler If not null, client will receive all callbacks on the handler's looper.
-     *                Otherwise, client will receive callbacks on a looper created by the writer.
-     *
-     * @throws IOException if failed to construct MediaMuxer or HeifEncoder.
-     */
     @SuppressLint("WrongConstant")
-    public HeifWriter(@NonNull FileDescriptor fd,
-                      int width, int height, boolean useGrid,
-                      int quality, int numImages, int primaryIndex,
-                      @InputMode int inputMode,
-                      @Nullable Handler handler) throws IOException {
-        this(width, height, useGrid, quality, numImages, primaryIndex, inputMode, handler,
-                new MediaMuxer(fd, MUXER_OUTPUT_HEIF));
-    }
-
-    private HeifWriter(int width, int height, boolean useGrid,
-                       int quality, int numImages, int primaryIndex,
+    private HeifWriter(@NonNull String path,
+                       @NonNull FileDescriptor fd,
+                       int width,
+                       int height,
+                       int rotation,
+                       boolean gridEnabled,
+                       int quality,
+                       int maxImages,
+                       int primaryIndex,
                        @InputMode int inputMode,
-                       @Nullable Handler handler,
-                       @NonNull MediaMuxer muxer) throws IOException {
-        if (numImages <= 0 || primaryIndex < 0 || primaryIndex >= numImages) {
+                       @Nullable Handler handler) throws IOException {
+        if (primaryIndex >= maxImages) {
             throw new IllegalArgumentException(
-                    "Invalid numImages (" + numImages + ") or primaryIndex (" + primaryIndex + ")");
+                    "Invalid maxImages (" + maxImages + ") or primaryIndex (" + primaryIndex + ")");
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "width: " + width
+                    + ", height: " + height
+                    + ", rotation: " + rotation
+                    + ", gridEnabled: " + gridEnabled
+                    + ", quality: " + quality
+                    + ", maxImages: " + maxImages
+                    + ", primaryIndex: " + primaryIndex
+                    + ", inputMode: " + inputMode);
         }
 
         MediaFormat format = MediaFormat.createVideoFormat(
                 MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC, width, height);
 
-        if (DEBUG) {
-            Log.d(TAG, "format: " + format + ", inputMode: " + inputMode +
-                    ", numImage: " + numImages + ", primaryIndex: " + primaryIndex);
-        }
-
         // set to 1 initially, and wait for output format to know for sure
         mNumTiles = 1;
 
+        mRotation = rotation;
         mInputMode = inputMode;
-        mNumImages = numImages;
+        mMaxImages = maxImages;
         mPrimaryIndex = primaryIndex;
 
         Looper looper = (handler != null) ? handler.getLooper() : null;
@@ -222,9 +328,10 @@
         }
         mHandler = new Handler(looper);
 
-        mMuxer = muxer;
+        mMuxer = (path != null) ? new MediaMuxer(path, MUXER_OUTPUT_HEIF)
+                                : new MediaMuxer(fd, MUXER_OUTPUT_HEIF);
 
-        mHeifEncoder = new HeifEncoder(width, height, useGrid, quality,
+        mHeifEncoder = new HeifEncoder(width, height, gridEnabled, quality,
                 mInputMode, mHandler, new HeifCallback());
     }
 
@@ -400,19 +507,23 @@
 
             try {
                 int gridRows = format.getInteger(MediaFormat.KEY_GRID_ROWS);
-                int gridCols = format.getInteger(MediaFormat.KEY_GRID_COLS);
+                int gridCols = format.getInteger(MediaFormat.KEY_GRID_COLUMNS);
                 mNumTiles = gridRows * gridCols;
             } catch (NullPointerException | ClassCastException  e) {
                 mNumTiles = 1;
             }
 
-            // add mNumImages image tracks of the same format
-            mTrackIndexArray = new int[mNumImages];
+            // add mMaxImages image tracks of the same format
+            mTrackIndexArray = new int[mMaxImages];
+
+            // set rotation angle
+            if (mRotation > 0) {
+                Log.d(TAG, "setting rotation: " + mRotation);
+                mMuxer.setOrientationHint(mRotation);
+            }
             for (int i = 0; i < mTrackIndexArray.length; i++) {
                 // mark primary
-                if (i == mPrimaryIndex) {
-                    format.setInteger(MediaFormat.KEY_IS_DEFAULT, 1);
-                }
+                format.setInteger(MediaFormat.KEY_IS_DEFAULT, (i == mPrimaryIndex) ? 1 : 0);
                 mTrackIndexArray[i] = mMuxer.addTrack(format);
             }
             mMuxer.start();
@@ -436,7 +547,7 @@
                 return;
             }
 
-            if (mOutputIndex < mNumImages * mNumTiles) {
+            if (mOutputIndex < mMaxImages * mNumTiles) {
                 MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
                 info.set(byteBuffer.position(), byteBuffer.remaining(), 0, 0);
                 mMuxer.writeSampleData(
@@ -446,7 +557,7 @@
             mOutputIndex++;
 
             // post EOS if reached max number of images allowed.
-            if (mOutputIndex == mNumImages * mNumTiles) {
+            if (mOutputIndex == mMaxImages * mNumTiles) {
                 stopAndNotify(null);
             }
         }
diff --git a/jetifier/jetifier/processor/src/main/kotlin/com/android/tools/build/jetifier/processor/transform/bytecode/CoreRemapperImpl.kt b/jetifier/jetifier/processor/src/main/kotlin/com/android/tools/build/jetifier/processor/transform/bytecode/CoreRemapperImpl.kt
index 7019d78..e3766b6 100644
--- a/jetifier/jetifier/processor/src/main/kotlin/com/android/tools/build/jetifier/processor/transform/bytecode/CoreRemapperImpl.kt
+++ b/jetifier/jetifier/processor/src/main/kotlin/com/android/tools/build/jetifier/processor/transform/bytecode/CoreRemapperImpl.kt
@@ -66,11 +66,11 @@
             return value
         }
 
-        val result = context.config.typesMap.mapType(type)
-        if (result != null) {
-            changesDone = changesDone || result != type
-            Log.i(TAG, "Map string: '%s' -> '%s'", type, result)
-            return result.toDotNotation()
+        val mappedType = context.config.typesMap.mapType(type)
+        if (mappedType != null) {
+            changesDone = changesDone || mappedType != type
+            Log.i(TAG, "Map string: '%s' -> '%s'", type, mappedType)
+            return mappedType.toDotNotation()
         }
 
         // We might be working with an internal type or field reference, e.g.
@@ -86,10 +86,10 @@
 
         // Try rewrite rules
         if (context.useFallbackIfTypeIsMissing) {
-            val result = context.config.rulesMap.rewriteType(type)
-            if (result != null) {
-                Log.i(TAG, "Map string: '%s' -> '%s' via fallback", value, result)
-                return result.toDotNotation()
+            val rewrittenType = context.config.rulesMap.rewriteType(type)
+            if (rewrittenType != null) {
+                Log.i(TAG, "Map string: '%s' -> '%s' via fallback", value, rewrittenType)
+                return rewrittenType.toDotNotation()
             }
         }
 
diff --git a/jetifier/jetifier/source-transformer/rewritePackageNames.py b/jetifier/jetifier/source-transformer/rewritePackageNames.py
index 16c99cc..2d82b86 100755
--- a/jetifier/jetifier/source-transformer/rewritePackageNames.py
+++ b/jetifier/jetifier/source-transformer/rewritePackageNames.py
@@ -95,7 +95,7 @@
   rewriterTextBuilder = StringBuilder()
   rewriteRules = executionConfig.jetifierConfig.getTypesMap()
   for rule in rewriteRules:
-    rewriterTextBuilder.add("s|").add(rule.fromName).add("|").add(rule.toName).add("|g\n")
+    rewriterTextBuilder.add("s|").add(rule.fromName.replace(".", "\.")).add("|").add(rule.toName).add("|g\n")
   for rule in HARDCODED_RULES_REVERSE:
     rewriterTextBuilder.add(rule)
   scriptPath = "/tmp/jetifier-sed-script.txt"
diff --git a/leanback/src/main/res/values-as/strings.xml b/leanback/src/main/res/values-as/strings.xml
new file mode 100644
index 0000000..eb3b2b0
--- /dev/null
+++ b/leanback/src/main/res/values-as/strings.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="lb_navigation_menu_contentDescription" msgid="8126335323963415494">"নেভিগেশ্বন মেনু"</string>
+    <string name="orb_search_action" msgid="7534843523462177008">"সন্ধান সম্পৰ্কীয় কাৰ্য"</string>
+    <string name="lb_search_bar_hint" msgid="4819380969103509861">"সন্ধান"</string>
+    <string name="lb_search_bar_hint_speech" msgid="2795474673510974502">"সন্ধান কৰিবলৈ কথা কওক"</string>
+    <string name="lb_search_bar_hint_with_title" msgid="7453744869467668159">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> সন্ধান কৰক"</string>
+    <string name="lb_search_bar_hint_with_title_speech" msgid="5851694095153624617">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> সন্ধান কৰিবলৈ কথা কওক"</string>
+    <string name="lb_control_display_fast_forward_multiplier" msgid="2721825378927619928">"%1$dX"</string>
+    <string name="lb_control_display_rewind_multiplier" msgid="6173753802428649303">"%1$dX"</string>
+    <string name="lb_playback_controls_play" msgid="1590369760862605402">"প্লে কৰক"</string>
+    <string name="lb_playback_controls_pause" msgid="1769131316742618433">"পজ কৰক"</string>
+    <string name="lb_playback_controls_fast_forward" msgid="8966769845721269304">"ফাষ্ট ফৰৱাৰ্ড"</string>
+    <string name="lb_playback_controls_fast_forward_multiplier" msgid="801276177839339511">"ফাষ্ট ফৰৱার্ড কৰক %1$dX"</string>
+    <string name="lb_playback_controls_rewind" msgid="1412664391757869774">"ৰিৱাইণ্ড কৰক"</string>
+    <string name="lb_playback_controls_rewind_multiplier" msgid="8651612807713092781">"ৰিৱাইণ্ড কৰক %1$dX"</string>
+    <string name="lb_playback_controls_skip_next" msgid="4877009494447817003">"পৰৱৰ্তীটোলৈ এৰি যাওক"</string>
+    <string name="lb_playback_controls_skip_previous" msgid="3147124289285911980">"আগৰটোলৈ এৰি যাওক"</string>
+    <string name="lb_playback_controls_more_actions" msgid="2827883329510404797">"অধিক কাৰ্য"</string>
+    <string name="lb_playback_controls_thumb_up" msgid="8332816524260995892">"থাম্ব আপ বাছনি নাইকিয়া কৰক"</string>
+    <string name="lb_playback_controls_thumb_up_outline" msgid="1038344559734334272">"থাম্ব আপ বাছনি কৰক"</string>
+    <string name="lb_playback_controls_thumb_down" msgid="5075744418630733006">"থাম্ব ডাউন বাছনি নাইকিয়া কৰক"</string>
+    <string name="lb_playback_controls_thumb_down_outline" msgid="2847309435442474470">"থাম্ব ডাউন বাছনি কৰক"</string>
+    <string name="lb_playback_controls_repeat_none" msgid="5812341701962930499">"একো পুনৰাই প্লে নকৰিব"</string>
+    <string name="lb_playback_controls_repeat_all" msgid="5164826436271322261">"সকলো পুনৰাই প্লে কৰক"</string>
+    <string name="lb_playback_controls_repeat_one" msgid="7675097479246139440">"এটা পুনৰাই প্লে কৰক"</string>
+    <string name="lb_playback_controls_shuffle_enable" msgid="7809089255981448519">"সান-মিহলি সক্ষম কৰক"</string>
+    <string name="lb_playback_controls_shuffle_disable" msgid="8182435535948303910">"সান-মিহলি অক্ষম কৰক"</string>
+    <string name="lb_playback_controls_high_quality_enable" msgid="1862669142355962638">"উচ্চ মানৰ প্লেবেক সক্ষম কৰক"</string>
+    <string name="lb_playback_controls_high_quality_disable" msgid="3000046054608531995">"উচ্চ মান অক্ষম কৰক"</string>
+    <string name="lb_playback_controls_closed_captioning_enable" msgid="3934392140182327163">"ছাব-টাইটেল সক্ষম কৰক"</string>
+    <string name="lb_playback_controls_closed_captioning_disable" msgid="5508271941331836786">"ছাব-টাইটেল অক্ষম কৰক"</string>
+    <string name="lb_playback_controls_picture_in_picture" msgid="8800305194045609275">"চিত্ৰৰ ভিতৰত চিত্ৰ ম\'ড আৰম্ভ কৰক"</string>
+    <string name="lb_playback_time_separator" msgid="6549544638083578695">"/"</string>
+    <string name="lb_playback_controls_shown" msgid="7794717158616536936">"মিডিয়াৰ নিয়ন্ত্ৰণসমূহ দেখুওৱা হ\'ল"</string>
+    <string name="lb_playback_controls_hidden" msgid="619396299825306757">"মিডিয়াৰ নিয়ন্ত্ৰণসমূহ লুকুৱাই ৰখা হৈছে, দেখুওৱাবলৈ ডি-পেডত টিপক"</string>
+    <string name="lb_guidedaction_finish_title" msgid="7747913934287176843">"সমাপ্ত"</string>
+    <string name="lb_guidedaction_continue_title" msgid="1122271825827282965">"অব্যাহত ৰাখক"</string>
+    <string name="lb_media_player_error" msgid="8748646000835486516">"MediaPlayer ত্ৰুটি ক\'ড %1$d, অতিৰিক্ত %2$d"</string>
+    <string name="lb_onboarding_get_started" msgid="7674487829030291492">"আৰম্ভ কৰক"</string>
+    <string name="lb_onboarding_accessibility_next" msgid="4213611627196077555">"পৰৱৰ্তী"</string>
+</resources>
diff --git a/leanback/src/main/res/values-be/strings.xml b/leanback/src/main/res/values-be/strings.xml
index 2828004..a4038d6 100644
--- a/leanback/src/main/res/values-be/strings.xml
+++ b/leanback/src/main/res/values-be/strings.xml
@@ -21,8 +21,8 @@
     <string name="orb_search_action" msgid="7534843523462177008">"Пошук"</string>
     <string name="lb_search_bar_hint" msgid="4819380969103509861">"Пошук"</string>
     <string name="lb_search_bar_hint_speech" msgid="2795474673510974502">"Прамоўце пошукавы запыт"</string>
-    <string name="lb_search_bar_hint_with_title" msgid="7453744869467668159">"Шукаць тут <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
-    <string name="lb_search_bar_hint_with_title_speech" msgid="5851694095153624617">"Прамоўце пошукавы запыт тут <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+    <string name="lb_search_bar_hint_with_title" msgid="7453744869467668159">"Шукаць тут: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+    <string name="lb_search_bar_hint_with_title_speech" msgid="5851694095153624617">"Прамоўце запыт для пошуку тут: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
     <string name="lb_control_display_fast_forward_multiplier" msgid="2721825378927619928">"%1$dX"</string>
     <string name="lb_control_display_rewind_multiplier" msgid="6173753802428649303">"%1$dX"</string>
     <string name="lb_playback_controls_play" msgid="1590369760862605402">"Прайграць"</string>
@@ -53,7 +53,7 @@
     <string name="lb_playback_controls_hidden" msgid="619396299825306757">"Элементы кіравання мультымедыя схаваны. Каб паказаць іх, націсніце d-pad"</string>
     <string name="lb_guidedaction_finish_title" msgid="7747913934287176843">"Завяршыць"</string>
     <string name="lb_guidedaction_continue_title" msgid="1122271825827282965">"Працягнуць"</string>
-    <string name="lb_media_player_error" msgid="8748646000835486516">"Код памылкі MediaPlayer %1$d дадаткова %2$d"</string>
+    <string name="lb_media_player_error" msgid="8748646000835486516">"Код памылкі MediaPlayer: %1$d (дадатковы: %2$d)"</string>
     <string name="lb_onboarding_get_started" msgid="7674487829030291492">"ПАЧАЦЬ"</string>
     <string name="lb_onboarding_accessibility_next" msgid="4213611627196077555">"Далей"</string>
 </resources>
diff --git a/leanback/src/main/res/values-es/strings.xml b/leanback/src/main/res/values-es/strings.xml
index dc33a6b..53e6797 100644
--- a/leanback/src/main/res/values-es/strings.xml
+++ b/leanback/src/main/res/values-es/strings.xml
@@ -22,7 +22,7 @@
     <string name="lb_search_bar_hint" msgid="4819380969103509861">"Haz una búsqueda"</string>
     <string name="lb_search_bar_hint_speech" msgid="2795474673510974502">"Habla para buscar"</string>
     <string name="lb_search_bar_hint_with_title" msgid="7453744869467668159">"Busca <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
-    <string name="lb_search_bar_hint_with_title_speech" msgid="5851694095153624617">"Busca <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> por voz"</string>
+    <string name="lb_search_bar_hint_with_title_speech" msgid="5851694095153624617">"Habla para buscar <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
     <string name="lb_control_display_fast_forward_multiplier" msgid="2721825378927619928">"%1$dx"</string>
     <string name="lb_control_display_rewind_multiplier" msgid="6173753802428649303">"%1$dx"</string>
     <string name="lb_playback_controls_play" msgid="1590369760862605402">"Reproducir"</string>
@@ -47,7 +47,7 @@
     <string name="lb_playback_controls_high_quality_disable" msgid="3000046054608531995">"Inhabilitar alta calidad"</string>
     <string name="lb_playback_controls_closed_captioning_enable" msgid="3934392140182327163">"Habilitar subtítulos"</string>
     <string name="lb_playback_controls_closed_captioning_disable" msgid="5508271941331836786">"Inhabilitar subtítulos"</string>
-    <string name="lb_playback_controls_picture_in_picture" msgid="8800305194045609275">"Activar modo pantalla en pantalla"</string>
+    <string name="lb_playback_controls_picture_in_picture" msgid="8800305194045609275">"Activar modo imagen en imagen"</string>
     <string name="lb_playback_time_separator" msgid="6549544638083578695">"/"</string>
     <string name="lb_playback_controls_shown" msgid="7794717158616536936">"Controles multimedia mostrados"</string>
     <string name="lb_playback_controls_hidden" msgid="619396299825306757">"Controles multimedia ocultos (pulsa la cruceta para mostrarlos)"</string>
diff --git a/leanback/src/main/res/values-eu/strings.xml b/leanback/src/main/res/values-eu/strings.xml
index 6b7226f..72c433f 100644
--- a/leanback/src/main/res/values-eu/strings.xml
+++ b/leanback/src/main/res/values-eu/strings.xml
@@ -49,8 +49,8 @@
     <string name="lb_playback_controls_closed_captioning_disable" msgid="5508271941331836786">"Desgaitu azpitituluak"</string>
     <string name="lb_playback_controls_picture_in_picture" msgid="8800305194045609275">"Aktibatu \"Pantaila txiki gainjarri\" modua"</string>
     <string name="lb_playback_time_separator" msgid="6549544638083578695">"/"</string>
-    <string name="lb_playback_controls_shown" msgid="7794717158616536936">"Multimedia kontrolatzeko aukerak ikusgai"</string>
-    <string name="lb_playback_controls_hidden" msgid="619396299825306757">"Ezkutatuta daude multimedia-edukia kontrolatzeko aukerak. Erakusteko, sakatu nabigazio-gurutzea."</string>
+    <string name="lb_playback_controls_shown" msgid="7794717158616536936">"Multimedia-edukia kontrolatzeko aukerak ikusgai"</string>
+    <string name="lb_playback_controls_hidden" msgid="619396299825306757">"Ezkutatuta daude multimedia-edukia kontrolatzeko aukerak. Haiek erakusteko, sakatu nabigazio-gurutzea."</string>
     <string name="lb_guidedaction_finish_title" msgid="7747913934287176843">"Amaitu"</string>
     <string name="lb_guidedaction_continue_title" msgid="1122271825827282965">"Egin aurrera"</string>
     <string name="lb_media_player_error" msgid="8748646000835486516">"MediaPlayer errore-kodea: %1$d (%2$d gehigarria)"</string>
diff --git a/leanback/src/main/res/values-fr-rCA/strings.xml b/leanback/src/main/res/values-fr-rCA/strings.xml
index de3fde5..ad38e94 100644
--- a/leanback/src/main/res/values-fr-rCA/strings.xml
+++ b/leanback/src/main/res/values-fr-rCA/strings.xml
@@ -32,7 +32,7 @@
     <string name="lb_playback_controls_rewind" msgid="1412664391757869774">"Retour arrière"</string>
     <string name="lb_playback_controls_rewind_multiplier" msgid="8651612807713092781">"Retour rapide à %1$dX"</string>
     <string name="lb_playback_controls_skip_next" msgid="4877009494447817003">"Passer à l\'élément suivant"</string>
-    <string name="lb_playback_controls_skip_previous" msgid="3147124289285911980">"Passer à l\'élément précédent"</string>
+    <string name="lb_playback_controls_skip_previous" msgid="3147124289285911980">"Retourner à l\'élément précédent"</string>
     <string name="lb_playback_controls_more_actions" msgid="2827883329510404797">"Autres actions"</string>
     <string name="lb_playback_controls_thumb_up" msgid="8332816524260995892">"Désélectionner la mention « J\'aime »"</string>
     <string name="lb_playback_controls_thumb_up_outline" msgid="1038344559734334272">"Sélectionner la mention « J\'aime »"</string>
diff --git a/leanback/src/main/res/values-ja/strings.xml b/leanback/src/main/res/values-ja/strings.xml
index 22980f1..96f27e2 100644
--- a/leanback/src/main/res/values-ja/strings.xml
+++ b/leanback/src/main/res/values-ja/strings.xml
@@ -43,8 +43,8 @@
     <string name="lb_playback_controls_repeat_one" msgid="7675097479246139440">"1 曲をリピート"</string>
     <string name="lb_playback_controls_shuffle_enable" msgid="7809089255981448519">"シャッフルを有効にする"</string>
     <string name="lb_playback_controls_shuffle_disable" msgid="8182435535948303910">"シャッフルを無効にする"</string>
-    <string name="lb_playback_controls_high_quality_enable" msgid="1862669142355962638">"高品質を有効にする"</string>
-    <string name="lb_playback_controls_high_quality_disable" msgid="3000046054608531995">"高品質を無効にする"</string>
+    <string name="lb_playback_controls_high_quality_enable" msgid="1862669142355962638">"高画質を有効にする"</string>
+    <string name="lb_playback_controls_high_quality_disable" msgid="3000046054608531995">"高画質を無効にする"</string>
     <string name="lb_playback_controls_closed_captioning_enable" msgid="3934392140182327163">"クローズド キャプションを有効にする"</string>
     <string name="lb_playback_controls_closed_captioning_disable" msgid="5508271941331836786">"クローズド キャプションを無効にする"</string>
     <string name="lb_playback_controls_picture_in_picture" msgid="8800305194045609275">"ピクチャー イン ピクチャー モードに移動"</string>
diff --git a/leanback/src/main/res/values-ko/strings.xml b/leanback/src/main/res/values-ko/strings.xml
index 9eacb53..b1587b9 100644
--- a/leanback/src/main/res/values-ko/strings.xml
+++ b/leanback/src/main/res/values-ko/strings.xml
@@ -40,7 +40,7 @@
     <string name="lb_playback_controls_thumb_down_outline" msgid="2847309435442474470">"싫어요 선택"</string>
     <string name="lb_playback_controls_repeat_none" msgid="5812341701962930499">"반복 안함"</string>
     <string name="lb_playback_controls_repeat_all" msgid="5164826436271322261">"전체 반복"</string>
-    <string name="lb_playback_controls_repeat_one" msgid="7675097479246139440">"한 항목 반복"</string>
+    <string name="lb_playback_controls_repeat_one" msgid="7675097479246139440">"한 개 반복"</string>
     <string name="lb_playback_controls_shuffle_enable" msgid="7809089255981448519">"셔플 사용 설정"</string>
     <string name="lb_playback_controls_shuffle_disable" msgid="8182435535948303910">"셔플 사용 중지"</string>
     <string name="lb_playback_controls_high_quality_enable" msgid="1862669142355962638">"고품질 사용 설정"</string>
diff --git a/leanback/src/main/res/values-mr/strings.xml b/leanback/src/main/res/values-mr/strings.xml
index b01e273..07b4e14 100644
--- a/leanback/src/main/res/values-mr/strings.xml
+++ b/leanback/src/main/res/values-mr/strings.xml
@@ -29,7 +29,7 @@
     <!-- String.format failed for translation -->
     <!-- no translation found for lb_control_display_rewind_multiplier (6173753802428649303) -->
     <skip />
-    <string name="lb_playback_controls_play" msgid="1590369760862605402">"खेळा"</string>
+    <string name="lb_playback_controls_play" msgid="1590369760862605402">"प्ले"</string>
     <string name="lb_playback_controls_pause" msgid="1769131316742618433">"विराम द्या"</string>
     <string name="lb_playback_controls_fast_forward" msgid="8966769845721269304">"पुढे ढकला"</string>
     <string name="lb_playback_controls_fast_forward_multiplier" msgid="801276177839339511">"फास्ट फॉरवर्ड %1$d"</string>
@@ -38,10 +38,10 @@
     <string name="lb_playback_controls_skip_next" msgid="4877009494447817003">"पुढील वगळा"</string>
     <string name="lb_playback_controls_skip_previous" msgid="3147124289285911980">"मागील वगळा"</string>
     <string name="lb_playback_controls_more_actions" msgid="2827883329510404797">"आणखी क्रिया"</string>
-    <string name="lb_playback_controls_thumb_up" msgid="8332816524260995892">"वर अंगठा निवड रद्द करा"</string>
-    <string name="lb_playback_controls_thumb_up_outline" msgid="1038344559734334272">"वर अंगठा निवडा"</string>
-    <string name="lb_playback_controls_thumb_down" msgid="5075744418630733006">"खाली अंगठा निवड रद्द करा"</string>
-    <string name="lb_playback_controls_thumb_down_outline" msgid="2847309435442474470">"खाली अंगठा निवडा"</string>
+    <string name="lb_playback_controls_thumb_up" msgid="8332816524260995892">"थंब अप निवड रद्द करा"</string>
+    <string name="lb_playback_controls_thumb_up_outline" msgid="1038344559734334272">"थंब अप निवडा"</string>
+    <string name="lb_playback_controls_thumb_down" msgid="5075744418630733006">"थंब डाउन निवड रद्द करा"</string>
+    <string name="lb_playback_controls_thumb_down_outline" msgid="2847309435442474470">"थंब डाउन निवडा"</string>
     <string name="lb_playback_controls_repeat_none" msgid="5812341701962930499">"काहीही रिपीट करू नका"</string>
     <string name="lb_playback_controls_repeat_all" msgid="5164826436271322261">"सर्व रिपीट करा"</string>
     <string name="lb_playback_controls_repeat_one" msgid="7675097479246139440">"एक रिपीट करा"</string>
diff --git a/leanback/src/main/res/values-or/strings.xml b/leanback/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000..ce6d1ea
--- /dev/null
+++ b/leanback/src/main/res/values-or/strings.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="lb_navigation_menu_contentDescription" msgid="8126335323963415494">"ନେଭିଗେଶନ୍ ମେନୁ"</string>
+    <string name="orb_search_action" msgid="7534843523462177008">"ଖୋଜିବା କାମ"</string>
+    <string name="lb_search_bar_hint" msgid="4819380969103509861">"ସର୍ଚ୍ଚ କରନ୍ତୁ"</string>
+    <string name="lb_search_bar_hint_speech" msgid="2795474673510974502">"ଖୋଜିବା ପାଇଁ କୁହନ୍ତୁ"</string>
+    <string name="lb_search_bar_hint_with_title" msgid="7453744869467668159">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> ଖୋଜନ୍ତୁ"</string>
+    <string name="lb_search_bar_hint_with_title_speech" msgid="5851694095153624617">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> ଖୋଜିବା ପାଇଁ କୁହନ୍ତୁ"</string>
+    <string name="lb_control_display_fast_forward_multiplier" msgid="2721825378927619928">"%1$dX"</string>
+    <string name="lb_control_display_rewind_multiplier" msgid="6173753802428649303">"%1$dX"</string>
+    <string name="lb_playback_controls_play" msgid="1590369760862605402">"ଚଲାନ୍ତୁ"</string>
+    <string name="lb_playback_controls_pause" msgid="1769131316742618433">"ପଜ୍‍ କରନ୍ତୁ"</string>
+    <string name="lb_playback_controls_fast_forward" msgid="8966769845721269304">"ଫାଷ୍ଟ ଫର୍‌ୱାର୍ଡ"</string>
+    <string name="lb_playback_controls_fast_forward_multiplier" msgid="801276177839339511">"%1$dX ବେଗରେ ଫାଷ୍ଟ ଫରୱାର୍ଡ"</string>
+    <string name="lb_playback_controls_rewind" msgid="1412664391757869774">"ରିୱାଇଣ୍ଡ"</string>
+    <string name="lb_playback_controls_rewind_multiplier" msgid="8651612807713092781">"%1$dX ବେଗରେ ରିୱାଇଣ୍ଡ କରନ୍ତୁ"</string>
+    <string name="lb_playback_controls_skip_next" msgid="4877009494447817003">"ପରବର୍ତ୍ତୀକୁ ଯାଆନ୍ତୁ"</string>
+    <string name="lb_playback_controls_skip_previous" msgid="3147124289285911980">"ପୂର୍ବଟିକୁ ଛାଡ଼ିଦିଅନ୍ତୁ"</string>
+    <string name="lb_playback_controls_more_actions" msgid="2827883329510404797">"ଅଧିକ ଗତିବିଧି"</string>
+    <string name="lb_playback_controls_thumb_up" msgid="8332816524260995892">"ପସନ୍ଦକୁ ଚୟନ କରନ୍ତୁ ନାହିଁ"</string>
+    <string name="lb_playback_controls_thumb_up_outline" msgid="1038344559734334272">"ପସନ୍ଦକୁ ଚୟନ କରନ୍ତୁ"</string>
+    <string name="lb_playback_controls_thumb_down" msgid="5075744418630733006">"ପସନ୍ଦହୀନକୁ ଚୟନ କରନ୍ତୁ ନାହିଁ"</string>
+    <string name="lb_playback_controls_thumb_down_outline" msgid="2847309435442474470">"ପସନ୍ଦହୀନକୁ ଚୟନ କରନ୍ତୁ"</string>
+    <string name="lb_playback_controls_repeat_none" msgid="5812341701962930499">"କୌଣସିଟି ଦୋହରାନ୍ତୁ ନାହିଁ"</string>
+    <string name="lb_playback_controls_repeat_all" msgid="5164826436271322261">"ସବୁଗୁଡ଼ିକୁ ଦୋହରାନ୍ତୁ"</string>
+    <string name="lb_playback_controls_repeat_one" msgid="7675097479246139440">"ଗୋଟିଏ ଦୋହରାନ୍ତୁ"</string>
+    <string name="lb_playback_controls_shuffle_enable" msgid="7809089255981448519">"ଅଦଳବଦଳକୁ ସକ୍ଷମ କରନ୍ତୁ"</string>
+    <string name="lb_playback_controls_shuffle_disable" msgid="8182435535948303910">"ଅଦଳବଦଳକୁ ଅକ୍ଷମ କରନ୍ତୁ"</string>
+    <string name="lb_playback_controls_high_quality_enable" msgid="1862669142355962638">"ଉଚ୍ଚ କ୍ୱାଲିଟୀକୁ ସକ୍ଷମ କରନ୍ତୁ"</string>
+    <string name="lb_playback_controls_high_quality_disable" msgid="3000046054608531995">"ଉଚ୍ଚ କ୍ୱାଲିଟୀକୁ ଅକ୍ଷମ କରନ୍ତୁ"</string>
+    <string name="lb_playback_controls_closed_captioning_enable" msgid="3934392140182327163">"କ୍ଲୋଜଡ୍‍ କ୍ୟାପ୍ସନିଙ୍ଗକୁ ସକ୍ଷମ କରନ୍ତୁ"</string>
+    <string name="lb_playback_controls_closed_captioning_disable" msgid="5508271941331836786">"କ୍ଲୋଜଡ୍‍ କ୍ୟାପ୍ସନିଙ୍ଗକୁ ଅକ୍ଷମ କରନ୍ତୁ"</string>
+    <string name="lb_playback_controls_picture_in_picture" msgid="8800305194045609275">"ଛବି ଭିତରେ ଛବି ମୋଡ୍‌ରେ ପ୍ରବେଶ କରନ୍ତୁ"</string>
+    <string name="lb_playback_time_separator" msgid="6549544638083578695">"/"</string>
+    <string name="lb_playback_controls_shown" msgid="7794717158616536936">"ମିଡିଆ ନିୟନ୍ତ୍ରଣ ଦେଖାଯାଇଛି"</string>
+    <string name="lb_playback_controls_hidden" msgid="619396299825306757">"ମିଡିଆ ନିୟନ୍ତ୍ରଣ ଲୁଚିଯାଇଛି, ଦେଖାଇବାକୁ ଡି-ପ୍ୟାଡ୍‍ ଦବାନ୍ତୁ"</string>
+    <string name="lb_guidedaction_finish_title" msgid="7747913934287176843">"ସମାପ୍ତ କରନ୍ତୁ"</string>
+    <string name="lb_guidedaction_continue_title" msgid="1122271825827282965">"ଜାରି ରଖନ୍ତୁ"</string>
+    <string name="lb_media_player_error" msgid="8748646000835486516">"MediaPlayer ତ୍ରୁଟି କୋଡ୍‍ %1$d ଅତିରିକ୍ତ %2$d"</string>
+    <string name="lb_onboarding_get_started" msgid="7674487829030291492">"ଆରମ୍ଭ କରନ୍ତୁ"</string>
+    <string name="lb_onboarding_accessibility_next" msgid="4213611627196077555">"ପରବର୍ତ୍ତୀ"</string>
+</resources>
diff --git a/leanback/src/main/res/values-uz/strings.xml b/leanback/src/main/res/values-uz/strings.xml
index 50a86af..95e0b67 100644
--- a/leanback/src/main/res/values-uz/strings.xml
+++ b/leanback/src/main/res/values-uz/strings.xml
@@ -21,8 +21,8 @@
     <string name="orb_search_action" msgid="7534843523462177008">"Qidiruv amali"</string>
     <string name="lb_search_bar_hint" msgid="4819380969103509861">"Qidiruv"</string>
     <string name="lb_search_bar_hint_speech" msgid="2795474673510974502">"Qidirish uchun gapiring"</string>
-    <string name="lb_search_bar_hint_with_title" msgid="7453744869467668159">"Qidirish: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
-    <string name="lb_search_bar_hint_with_title_speech" msgid="5851694095153624617">"Qidirish uchun ayting: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+    <string name="lb_search_bar_hint_with_title" msgid="7453744869467668159">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> ichidan qidirish"</string>
+    <string name="lb_search_bar_hint_with_title_speech" msgid="5851694095153624617">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> ichidan qidirish uchun gapiring"</string>
     <string name="lb_control_display_fast_forward_multiplier" msgid="2721825378927619928">"%1$dX"</string>
     <string name="lb_control_display_rewind_multiplier" msgid="6173753802428649303">"%1$dX"</string>
     <string name="lb_playback_controls_play" msgid="1590369760862605402">"Ijro"</string>
@@ -39,14 +39,14 @@
     <string name="lb_playback_controls_thumb_down" msgid="5075744418630733006">"Salbiy baho tanlovini bekor qilish"</string>
     <string name="lb_playback_controls_thumb_down_outline" msgid="2847309435442474470">"Salbiy bahoni tanlash"</string>
     <string name="lb_playback_controls_repeat_none" msgid="5812341701962930499">"Takrorlamaslik"</string>
-    <string name="lb_playback_controls_repeat_all" msgid="5164826436271322261">"Barchasini takrorlash"</string>
+    <string name="lb_playback_controls_repeat_all" msgid="5164826436271322261">"Hammasini takrorlash"</string>
     <string name="lb_playback_controls_repeat_one" msgid="7675097479246139440">"Bir marta takrorlash"</string>
     <string name="lb_playback_controls_shuffle_enable" msgid="7809089255981448519">"Aralashtirish funksiyasini yoqish"</string>
-    <string name="lb_playback_controls_shuffle_disable" msgid="8182435535948303910">"Aralashtirish funksiyasini o‘chirish"</string>
+    <string name="lb_playback_controls_shuffle_disable" msgid="8182435535948303910">"Aralashtirmaslik"</string>
     <string name="lb_playback_controls_high_quality_enable" msgid="1862669142355962638">"Yuqori sifatni yoqish"</string>
-    <string name="lb_playback_controls_high_quality_disable" msgid="3000046054608531995">"Yuqori sifatni o‘chirib qo‘yish"</string>
+    <string name="lb_playback_controls_high_quality_disable" msgid="3000046054608531995">"Yuqori sifatda ijro qilmaslik"</string>
     <string name="lb_playback_controls_closed_captioning_enable" msgid="3934392140182327163">"Taglavhalarni yoqish"</string>
-    <string name="lb_playback_controls_closed_captioning_disable" msgid="5508271941331836786">"Taglavhalarni o‘chirib qo‘yish"</string>
+    <string name="lb_playback_controls_closed_captioning_disable" msgid="5508271941331836786">"Taglavhalarni chiqarmaslik"</string>
     <string name="lb_playback_controls_picture_in_picture" msgid="8800305194045609275">"Tasvir ustida tasvir rejimiga kirish"</string>
     <string name="lb_playback_time_separator" msgid="6549544638083578695">"/"</string>
     <string name="lb_playback_controls_shown" msgid="7794717158616536936">"Boshqaruv elementlari ochiq"</string>
diff --git a/leanback/src/main/res/values-zh-rTW/strings.xml b/leanback/src/main/res/values-zh-rTW/strings.xml
index bcaf11e..9aea4d2 100644
--- a/leanback/src/main/res/values-zh-rTW/strings.xml
+++ b/leanback/src/main/res/values-zh-rTW/strings.xml
@@ -34,10 +34,10 @@
     <string name="lb_playback_controls_skip_next" msgid="4877009494447817003">"跳至下一個項目"</string>
     <string name="lb_playback_controls_skip_previous" msgid="3147124289285911980">"跳至上一個項目"</string>
     <string name="lb_playback_controls_more_actions" msgid="2827883329510404797">"更多動作"</string>
-    <string name="lb_playback_controls_thumb_up" msgid="8332816524260995892">"取消選取喜歡"</string>
-    <string name="lb_playback_controls_thumb_up_outline" msgid="1038344559734334272">"選取喜歡"</string>
-    <string name="lb_playback_controls_thumb_down" msgid="5075744418630733006">"取消選取不喜歡"</string>
-    <string name="lb_playback_controls_thumb_down_outline" msgid="2847309435442474470">"選取不喜歡"</string>
+    <string name="lb_playback_controls_thumb_up" msgid="8332816524260995892">"取消選取「喜歡」"</string>
+    <string name="lb_playback_controls_thumb_up_outline" msgid="1038344559734334272">"選取「喜歡」"</string>
+    <string name="lb_playback_controls_thumb_down" msgid="5075744418630733006">"取消選取「不喜歡」"</string>
+    <string name="lb_playback_controls_thumb_down_outline" msgid="2847309435442474470">"選取「不喜歡」"</string>
     <string name="lb_playback_controls_repeat_none" msgid="5812341701962930499">"不重複播放"</string>
     <string name="lb_playback_controls_repeat_all" msgid="5164826436271322261">"重複播放所有項目"</string>
     <string name="lb_playback_controls_repeat_one" msgid="7675097479246139440">"重複播放單一項目"</string>
diff --git a/lifecycle/reactivestreams/build.gradle b/lifecycle/reactivestreams/build.gradle
index 34390b7..649c526 100644
--- a/lifecycle/reactivestreams/build.gradle
+++ b/lifecycle/reactivestreams/build.gradle
@@ -35,9 +35,7 @@
 
     testImplementation(JUNIT)
     testImplementation(RX_JAVA)
-    testImplementation(TEST_RUNNER)
-
-    androidTestImplementation(SUPPORT_APPCOMPAT, libs.support_exclude_config)
+    testImplementation(project(":arch:core-testing"))
 }
 
 supportLibrary {
@@ -48,4 +46,4 @@
     inceptionYear = "2017"
     description = "Android Lifecycle Reactivestreams"
     url = SupportLibraryExtension.ARCHITECTURE_URL
-}
\ No newline at end of file
+}
diff --git a/lifecycle/reactivestreams/ktx/OWNERS b/lifecycle/reactivestreams/ktx/OWNERS
new file mode 100644
index 0000000..e450f4c
--- /dev/null
+++ b/lifecycle/reactivestreams/ktx/OWNERS
@@ -0,0 +1 @@
+jakew@google.com
diff --git a/lifecycle/reactivestreams/ktx/build.gradle b/lifecycle/reactivestreams/ktx/build.gradle
new file mode 100644
index 0000000..2604a8e
--- /dev/null
+++ b/lifecycle/reactivestreams/ktx/build.gradle
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SupportLibraryExtension
+
+plugins {
+  id("SupportAndroidLibraryPlugin")
+  id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+  api(project(":lifecycle:lifecycle-reactivestreams"))
+  api(KOTLIN_STDLIB)
+
+  testImplementation(JUNIT)
+  testImplementation(RX_JAVA)
+  testImplementation(TRUTH)
+  testImplementation(project(":arch:core-testing"))
+}
+
+supportLibrary {
+  name = "Android Lifecycle ReactiveStreams KTX"
+  publish = true
+  mavenVersion = LibraryVersions.LIFECYCLES_EXT
+  mavenGroup = LibraryGroups.LIFECYCLE
+  inceptionYear = "2018"
+  description = "Kotlin extensions for Lifecycle ReactiveStreams"
+  url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/lifecycle/reactivestreams/ktx/src/main/AndroidManifest.xml b/lifecycle/reactivestreams/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d2c150c
--- /dev/null
+++ b/lifecycle/reactivestreams/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<!--
+  ~ Copyright (C) 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest package="androidx.lifecycle.reactivestreams.ktx"/>
diff --git a/lifecycle/reactivestreams/ktx/src/main/java/androidx/lifecycle/LiveDataReactiveSteams.kt b/lifecycle/reactivestreams/ktx/src/main/java/androidx/lifecycle/LiveDataReactiveSteams.kt
new file mode 100644
index 0000000..e4a95e7
--- /dev/null
+++ b/lifecycle/reactivestreams/ktx/src/main/java/androidx/lifecycle/LiveDataReactiveSteams.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.lifecycle
+
+import org.reactivestreams.Publisher
+
+/**
+ * Adapts the given [LiveData] stream to a ReactiveStreams [Publisher].
+ *
+ * @see LiveDataReactiveStreams.toPublisher
+ */
+inline fun <T> LiveData<T>.toPublisher(lifecycle: LifecycleOwner): Publisher<T> =
+    LiveDataReactiveStreams.toPublisher(lifecycle, this)
+
+/**
+ * Creates an observable [LiveData] stream from a ReactiveStreams [Publisher].
+ *
+ * @see LiveDataReactiveStreams.fromPublisher
+ */
+inline fun <T> Publisher<T>.toLiveData(): LiveData<T> =
+    LiveDataReactiveStreams.fromPublisher(this)
diff --git a/lifecycle/reactivestreams/ktx/src/test/java/androidx/lifecycle/LiveDataReactiveStreamsTest.kt b/lifecycle/reactivestreams/ktx/src/test/java/androidx/lifecycle/LiveDataReactiveStreamsTest.kt
new file mode 100644
index 0000000..ad3bdb2
--- /dev/null
+++ b/lifecycle/reactivestreams/ktx/src/test/java/androidx/lifecycle/LiveDataReactiveStreamsTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+package androidx.lifecycle
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.google.common.truth.Truth.assertThat
+import io.reactivex.processors.PublishProcessor
+import io.reactivex.processors.ReplayProcessor
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class LiveDataReactiveStreamsTest {
+    @get:Rule val rule = InstantTaskExecutorRule()
+
+    private lateinit var lifecycleOwner: LifecycleOwner
+
+    @Before fun init() {
+        lifecycleOwner = object : LifecycleOwner {
+            internal var registry = LifecycleRegistry(this)
+
+            init {
+                registry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
+            }
+
+            override fun getLifecycle(): Lifecycle {
+                return registry
+            }
+        }
+    }
+
+    @Test fun convertsFromPublisher() {
+        val processor = PublishProcessor.create<String>()
+        val liveData = processor.toLiveData()
+
+        val output = mutableListOf<String?>()
+        liveData.observe(lifecycleOwner, Observer { output.add(it) })
+
+        processor.onNext("foo")
+        processor.onNext("bar")
+        processor.onNext("baz")
+
+        assertThat(output).containsExactly("foo", "bar", "baz")
+    }
+
+    @Test fun convertsToPublisherWithSyncData() {
+        val liveData = MutableLiveData<String>()
+        liveData.value = "foo"
+
+        val outputProcessor = ReplayProcessor.create<String>()
+        liveData.toPublisher(lifecycleOwner).subscribe(outputProcessor)
+
+        liveData.value = "bar"
+        liveData.value = "baz"
+
+        assertThat(outputProcessor.values).asList().containsExactly("foo", "bar", "baz")
+    }
+}
diff --git a/lifecycle/reactivestreams/src/androidTest/AndroidManifest.xml b/lifecycle/reactivestreams/src/androidTest/AndroidManifest.xml
deleted file mode 100644
index 304fd93..0000000
--- a/lifecycle/reactivestreams/src/androidTest/AndroidManifest.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2017 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="androidx.lifecycle.reactivestreams.test">
-    <uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
-
-    <application>
-        <activity android:name="androidx.lifecycle.viewmodeltest.ViewModelActivity"
-                  android:theme="@style/Base.Theme.AppCompat">
-        </activity>
-    </application>
-
-</manifest>
diff --git a/lifecycle/reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.java b/lifecycle/reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.java
index 4af5a09..711e71e 100644
--- a/lifecycle/reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.java
+++ b/lifecycle/reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.java
@@ -165,7 +165,7 @@
     }
 
     /**
-     * Creates an Observable {@link LiveData} stream from a ReactiveStreams publisher.
+     * Creates an observable {@link LiveData} stream from a ReactiveStreams {@link Publisher}}.
      *
      * <p>
      * When the LiveData becomes active, it subscribes to the emissions from the Publisher.
diff --git a/lifecycle/reactivestreams/src/test/java/androidx/lifecycle/LiveDataReactiveStreamsTest.java b/lifecycle/reactivestreams/src/test/java/androidx/lifecycle/LiveDataReactiveStreamsTest.java
index 6311d94..ed792d2 100644
--- a/lifecycle/reactivestreams/src/test/java/androidx/lifecycle/LiveDataReactiveStreamsTest.java
+++ b/lifecycle/reactivestreams/src/test/java/androidx/lifecycle/LiveDataReactiveStreamsTest.java
@@ -21,15 +21,13 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 
-import android.support.test.filters.SmallTest;
-
 import androidx.annotation.Nullable;
-import androidx.arch.core.executor.ArchTaskExecutor;
-import androidx.arch.core.executor.TaskExecutor;
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
 
-import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 import org.reactivestreams.Subscriber;
 import org.reactivestreams.Subscription;
 
@@ -47,8 +45,9 @@
 import io.reactivex.schedulers.TestScheduler;
 import io.reactivex.subjects.AsyncSubject;
 
-@SmallTest
 public class LiveDataReactiveStreamsTest {
+    @Rule public final TestRule instantTaskExecutorRule = new InstantTaskExecutorRule();
+
     private LifecycleOwner mLifecycleOwner;
 
     private final List<String> mLiveDataOutput = new ArrayList<>();
@@ -62,7 +61,6 @@
     private final ReplayProcessor<String> mOutputProcessor = ReplayProcessor.create();
 
     private static final TestScheduler sBackgroundScheduler = new TestScheduler();
-    private Thread mTestThread;
 
     @Before
     public void init() {
@@ -77,31 +75,6 @@
                 return mRegistry;
             }
         };
-        mTestThread = Thread.currentThread();
-        ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
-
-            @Override
-            public void executeOnDiskIO(Runnable runnable) {
-                throw new IllegalStateException();
-            }
-
-            @Override
-            public void postToMainThread(Runnable runnable) {
-                // Wrong implementation, but it is fine for test
-                runnable.run();
-            }
-
-            @Override
-            public boolean isMainThread() {
-                return Thread.currentThread() == mTestThread;
-            }
-
-        });
-    }
-
-    @After
-    public void removeExecutorDelegate() {
-        ArchTaskExecutor.getInstance().setDelegate(null);
     }
 
     @Test
diff --git a/lifecycle/viewmodel/ktx/build.gradle b/lifecycle/viewmodel/ktx/build.gradle
new file mode 100644
index 0000000..a963911
--- /dev/null
+++ b/lifecycle/viewmodel/ktx/build.gradle
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+    id("org.jetbrains.kotlin.android")
+}
+
+android {
+    buildTypes {
+        debug {
+            testCoverageEnabled = false // Breaks Kotlin compiler.
+        }
+    }
+}
+
+dependencies {
+    api(project(":lifecycle:lifecycle-viewmodel"))
+    api(KOTLIN_STDLIB)
+
+    testImplementation(JUNIT)
+    testImplementation(TEST_RUNNER)
+}
+
+supportLibrary {
+    name = "Android Lifecycle ViewModel Kotlin Extensions"
+    publish = true
+    mavenVersion = LibraryVersions.LIFECYCLES_VIEWMODEL
+    mavenGroup = LibraryGroups.LIFECYCLE
+    inceptionYear = "2018"
+    description = "Kotlin extensions for 'viewmodel' artifact"
+}
diff --git a/lifecycle/viewmodel/ktx/src/main/AndroidManifest.xml b/lifecycle/viewmodel/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..75b1d20
--- /dev/null
+++ b/lifecycle/viewmodel/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest package="androidx.lifecycle.viewmodel.ktx"/>
diff --git a/lifecycle/viewmodel/ktx/src/main/java/androidx/lifecycle/ViewModelProvider.kt b/lifecycle/viewmodel/ktx/src/main/java/androidx/lifecycle/ViewModelProvider.kt
new file mode 100644
index 0000000..bc42e1b
--- /dev/null
+++ b/lifecycle/viewmodel/ktx/src/main/java/androidx/lifecycle/ViewModelProvider.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.lifecycle
+
+import androidx.annotation.MainThread
+
+/**
+ * Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
+ * an activity), associated with this `ViewModelProvider`.
+ *
+ * @see ViewModelProvider.get(Class)
+ */
+@MainThread
+inline fun <reified VM : ViewModel> ViewModelProvider.get() = get(VM::class.java)
diff --git a/lifecycle/viewmodel/ktx/src/test/java/androidx/lifecycle/ViewModelProviderTest.kt b/lifecycle/viewmodel/ktx/src/test/java/androidx/lifecycle/ViewModelProviderTest.kt
new file mode 100644
index 0000000..e8e76ce
--- /dev/null
+++ b/lifecycle/viewmodel/ktx/src/test/java/androidx/lifecycle/ViewModelProviderTest.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.lifecycle
+
+import android.support.test.filters.SmallTest
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+@SmallTest
+class ViewModelProviderTest {
+    class TestViewModel : ViewModel()
+
+    @Test
+    fun providerReifiedGet() {
+        val factory = object : ViewModelProvider.Factory {
+            override fun <T : ViewModel> create(modelClass: Class<T>) = modelClass.newInstance()
+        }
+        val provider = ViewModelProvider(ViewModelStore(), factory)
+
+        val viewModel = provider.get<TestViewModel>()
+        assertNotNull(viewModel)
+    }
+}
diff --git a/media-widget/api/current.txt b/media-widget/api/current.txt
new file mode 100644
index 0000000..d81423b
--- /dev/null
+++ b/media-widget/api/current.txt
@@ -0,0 +1,39 @@
+package androidx.media.widget {
+
+  public class MediaControlView2 extends android.view.ViewGroup {
+    ctor public MediaControlView2(android.content.Context);
+    ctor public MediaControlView2(android.content.Context, android.util.AttributeSet);
+    ctor public MediaControlView2(android.content.Context, android.util.AttributeSet, int);
+    method public void onMeasure(int, int);
+    method public void requestPlayButtonFocus();
+    method public void setOnFullScreenListener(androidx.media.widget.MediaControlView2.OnFullScreenListener);
+  }
+
+  public static abstract interface MediaControlView2.OnFullScreenListener {
+    method public abstract void onFullScreen(android.view.View, boolean);
+  }
+
+  public class VideoView2 extends android.view.ViewGroup {
+    ctor public VideoView2(android.content.Context);
+    ctor public VideoView2(android.content.Context, android.util.AttributeSet);
+    ctor public VideoView2(android.content.Context, android.util.AttributeSet, int);
+    method public androidx.media.widget.MediaControlView2 getMediaControlView2();
+    method public float getSpeed();
+    method public int getViewType();
+    method public boolean isSubtitleEnabled();
+    method public void onAttachedToWindow();
+    method public void onDetachedFromWindow();
+    method public void onMeasure(int, int);
+    method public void setAudioAttributes(android.media.AudioAttributes);
+    method public void setAudioFocusRequest(int);
+    method public void setMediaControlView2(androidx.media.widget.MediaControlView2, long);
+    method public void setSpeed(float);
+    method public void setSubtitleEnabled(boolean);
+    method public void setVideoUri(android.net.Uri, java.util.Map<java.lang.String, java.lang.String>);
+    method public void setViewType(int);
+    field public static final int VIEW_TYPE_SURFACEVIEW = 0; // 0x0
+    field public static final int VIEW_TYPE_TEXTUREVIEW = 1; // 0x1
+  }
+
+}
+
diff --git a/media-widget/build.gradle b/media-widget/build.gradle
index de3f9d9..e6ff65d 100644
--- a/media-widget/build.gradle
+++ b/media-widget/build.gradle
@@ -37,10 +37,13 @@
 }
 
 supportLibrary {
-    name = "Android Media Support Library"
+    name = "Android Support Media Widget"
     publish = true
     mavenVersion = LibraryVersions.SUPPORT_LIBRARY
     mavenGroup = LibraryGroups.MEDIA
     inceptionYear = "2011"
-    description = "Android Media Support Library"
+    description = "Android Support Media Widget"
+    minSdkVersion = 19
+    failOnDeprecationWarnings = false
+    failOnDeprecationWarnings = false
 }
diff --git a/media-widget/src/androidTest/AndroidManifest.xml b/media-widget/src/androidTest/AndroidManifest.xml
index dd6e9e0..5304a48 100644
--- a/media-widget/src/androidTest/AndroidManifest.xml
+++ b/media-widget/src/androidTest/AndroidManifest.xml
@@ -1,25 +1,26 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright 2018 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
+  Copyright 2018 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="androidx.media.widget.test">
     <uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
 
     <application>
         <activity android:name="androidx.media.widget.VideoView2TestActivity"
+            android:theme="@style/Theme.AppCompat"
             android:configChanges="keyboardHidden|orientation|screenSize"
             android:label="VideoView2TestActivity">
             <intent-filter>
diff --git a/media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java
index 2876e47..da38bd2 100644
--- a/media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java
+++ b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java
@@ -61,7 +61,7 @@
 /**
  * Test {@link VideoView2}.
  */
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) // TODO: KITKAT
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.P) // TODO: KITKAT
 @LargeTest
 @RunWith(AndroidJUnit4.class)
 public class VideoView2Test {
@@ -107,10 +107,17 @@
             @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);
+                if (Build.VERSION.SDK_INT >= 27) {
+                    mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+                    mActivity.setTurnScreenOn(true);
+                    mActivity.setShowWhenLocked(true);
+                    mKeyguardManager.requestDismissKeyguard(mActivity, null);
+                } else {
+                    mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                            | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+                            | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+                            | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+                }
             }
         });
         mInstrumentation.waitForIdleSync();
diff --git a/media-widget/src/androidTest/java/androidx/media/widget/VideoView2TestActivity.java b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2TestActivity.java
index d6a3ebc..912d336 100644
--- a/media-widget/src/androidTest/java/androidx/media/widget/VideoView2TestActivity.java
+++ b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2TestActivity.java
@@ -16,15 +16,15 @@
 
 package androidx.media.widget;
 
-import android.app.Activity;
 import android.os.Bundle;
 
+import androidx.fragment.app.FragmentActivity;
 import androidx.media.widget.test.R;
 
 /**
  * A minimal application for {@link VideoView2} test.
  */
-public class VideoView2TestActivity extends Activity {
+public class VideoView2TestActivity extends FragmentActivity {
     /**
      * Called with the activity is first created.
      */
diff --git a/media-widget/src/main/java/androidx/media/widget/BaseLayout.java b/media-widget/src/main/java/androidx/media/widget/BaseLayout.java
index 0b6988a..982513a 100644
--- a/media-widget/src/main/java/androidx/media/widget/BaseLayout.java
+++ b/media-widget/src/main/java/androidx/media/widget/BaseLayout.java
@@ -39,7 +39,7 @@
     }
 
     BaseLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
-        this(context, attrs, 0);
+        super(context, attrs);
     }
 
     BaseLayout(@NonNull Context context, @Nullable AttributeSet attrs,
diff --git a/media-widget/src/main/java/androidx/media/widget/MediaControlView2.java b/media-widget/src/main/java/androidx/media/widget/MediaControlView2.java
index 89fa946..a8371f2 100644
--- a/media-widget/src/main/java/androidx/media/widget/MediaControlView2.java
+++ b/media-widget/src/main/java/androidx/media/widget/MediaControlView2.java
@@ -27,6 +27,7 @@
 import android.support.v4.media.session.PlaybackStateCompat;
 import android.util.AttributeSet;
 import android.view.Gravity;
+import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
@@ -51,6 +52,8 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.media.SessionToken2;
+import androidx.mediarouter.app.MediaRouteButton;
+import androidx.mediarouter.media.MediaRouteSelector;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -60,41 +63,34 @@
 import java.util.List;
 import java.util.Locale;
 
-// import androidx.mediarouter.app.MediaRouteButton;
-// import androidx.mediarouter.media.MediaRouter;
-// import androidx.mediarouter.media.MediaRouteSelector;
-
 /**
- * @hide
- * A View that contains the controls for MediaPlayer2.
- * It provides a wide range of UI including buttons such as "Play/Pause", "Rewind", "Fast Forward",
- * "Subtitle", "Full Screen", and it is also possible to add multiple custom buttons.
+ * A View that contains the controls for {@link android.media.MediaPlayer}.
+ * It provides a wide range of buttons that serve the following functions: play/pause,
+ * rewind/fast-forward, skip to next/previous, select subtitle track, enter/exit full screen mode,
+ * adjust video quality, select audio track, mute/unmute, and adjust playback speed.
  *
  * <p>
  * <em> MediaControlView2 can be initialized in two different ways: </em>
- * 1) When VideoView2 is initialized, it automatically initializes a MediaControlView2 instance and
- * adds it to the view.
- * 2) Initialize MediaControlView2 programmatically and add it to a ViewGroup instance.
+ * 1) When initializing {@link VideoView2} a default MediaControlView2 is created.
+ * 2) Initialize MediaControlView2 programmatically and add it to a {@link ViewGroup} instance.
  *
  * In the first option, VideoView2 automatically connects MediaControlView2 to MediaController,
- * which is necessary to communicate with MediaSession2. In the second option, however, the
- * developer needs to manually retrieve a MediaController instance and set it to MediaControlView2
- * by calling setController(MediaController controller).
+ * which is necessary to communicate with MediaSession. In the second option, however, the
+ * developer needs to manually retrieve a MediaController instance from MediaSession and set it to
+ * MediaControlView2.
  *
  * <p>
  * There is no separate method that handles the show/hide behavior for MediaControlView2. Instead,
- * one can directly change the visibility of this view by calling View.setVisibility(int). The
- * values supported are View.VISIBLE and View.GONE.
- * In addition, the following customization is supported:
- * Set focus to the play/pause button by calling requestPlayButtonFocus().
+ * one can directly change the visibility of this view by calling {@link View#setVisibility(int)}.
+ * The values supported are View.VISIBLE and View.GONE.
  *
  * <p>
- * It is also possible to add custom buttons with custom icons and actions inside MediaControlView2.
- * Those buttons will be shown when the overflow button is clicked.
- * See VideoView2#setCustomActions for more details on how to add.
+ * In addition, the following customizations are supported:
+ * 1) Set focus to the play/pause button by calling requestPlayButtonFocus().
+ * 2) Set full screen mode
+ *
  */
 @RequiresApi(21) // TODO correct minSdk API use incompatibilities and remove before release.
-@RestrictTo(LIBRARY_GROUP)
 public class MediaControlView2 extends BaseLayout {
     /**
      * @hide
@@ -185,10 +181,6 @@
 
     private static final String TAG = "MediaControlView2";
 
-    static final String ARGUMENT_KEY_FULLSCREEN = "fullScreen";
-
-    // TODO: Make these constants public api to support custom video view.
-    // TODO: Combine these constants into one regarding TrackInfo.
     static final String KEY_VIDEO_TRACK_COUNT = "VideoTrackCount";
     static final String KEY_AUDIO_TRACK_COUNT = "AudioTrackCount";
     static final String KEY_SUBTITLE_TRACK_COUNT = "SubtitleTrackCount";
@@ -196,8 +188,6 @@
     static final String KEY_SELECTED_AUDIO_INDEX = "SelectedAudioIndex";
     static final String KEY_SELECTED_SUBTITLE_INDEX = "SelectedSubtitleIndex";
     static final String EVENT_UPDATE_TRACK_STATUS = "UpdateTrackStatus";
-
-    // TODO: Remove this once integrating with MediaSession2 & MediaMetadata2
     static final String KEY_STATE_IS_ADVERTISEMENT = "MediaTypeAdvertisement";
     static final String EVENT_UPDATE_MEDIA_TYPE_STATUS = "UpdateMediaTypeStatus";
 
@@ -205,8 +195,6 @@
     static final String COMMAND_SHOW_SUBTITLE = "showSubtitle";
     // String for sending command to hide subtitle to MediaSession.
     static final String COMMAND_HIDE_SUBTITLE = "hideSubtitle";
-    // TODO: remove once the implementation is revised
-    public static final String COMMAND_SET_FULLSCREEN = "setFullscreen";
     // String for sending command to select audio track to MediaSession.
     static final String COMMAND_SELECT_AUDIO_TRACK = "SelectTrack";
     // String for sending command to set playback speed to MediaSession.
@@ -230,7 +218,6 @@
 
     private static final int SIZE_TYPE_EMBEDDED = 0;
     private static final int SIZE_TYPE_FULL = 1;
-    // TODO: add support for Minimal size type.
     private static final int SIZE_TYPE_MINIMAL = 2;
 
     private static final int MAX_PROGRESS = 1000;
@@ -246,6 +233,7 @@
     private MediaControllerCompat.TransportControls mControls;
     private PlaybackStateCompat mPlaybackState;
     private MediaMetadataCompat mMetadata;
+    private OnFullScreenListener mOnFullScreenListener;
     private int mDuration;
     private int mPrevState;
     private int mPrevWidth;
@@ -261,8 +249,7 @@
     private int mSelectedSpeedIndex;
     private int mEmbeddedSettingsItemWidth;
     private int mFullSettingsItemWidth;
-    private int mEmbeddedSettingsItemHeight;
-    private int mFullSettingsItemHeight;
+    private int mSettingsItemHeight;
     private int mSettingsWindowMargin;
     private int mMediaType;
     private int mSizeType;
@@ -284,9 +271,8 @@
     private TextView mTitleView;
     private View mAdExternalLink;
     private ImageButton mBackButton;
-    // TODO (b/77158231) revive
-    // private MediaRouteButton mRouteButton;
-    // private MediaRouteSelector mRouteSelector;
+    private MediaRouteButton mRouteButton;
+    private MediaRouteSelector mRouteSelector;
 
     // Relating to Center View
     private ViewGroup mCenterView;
@@ -354,20 +340,9 @@
 
     public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs,
             int defStyleAttr) {
-        this(context, attrs, defStyleAttr, 0);
-    }
+        super(context, attrs, defStyleAttr);
 
-    public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs,
-            int defStyleAttr, int defStyleRes) {
-//        super((instance, superProvider, privateProvider) ->
-//                ApiLoader.getProvider().createMediaControlView2(
-//                        (MediaControlView2) instance, superProvider, privateProvider,
-//                        attrs, defStyleAttr, defStyleRes),
-//                context, attrs, defStyleAttr, defStyleRes);
-//        mProvider.initialize(attrs, defStyleAttr, defStyleRes);
-        super(context, attrs, defStyleAttr, defStyleRes);
-
-        mResources = getContext().getResources();
+        mResources = context.getResources();
         // Inflate MediaControlView2 from XML
         mRoot = makeControllerView();
         addView(mRoot);
@@ -375,9 +350,10 @@
 
     /**
      * Sets MediaSession2 token to control corresponding MediaSession2.
+     * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     public void setMediaSessionToken(SessionToken2 token) {
-        //mProvider.setMediaSessionToken_impl(token);
     }
 
     /**
@@ -385,17 +361,20 @@
      * @param l The callback that will be run
      */
     public void setOnFullScreenListener(OnFullScreenListener l) {
-        //mProvider.setOnFullScreenListener_impl(l);
+        mOnFullScreenListener = l;
     }
 
     /**
+     * Sets MediaController instance to MediaControlView2, which makes it possible to send and
+     * receive data between MediaControlView2 and VideoView2. This method does not need to be called
+     * when MediaControlView2 is initialized with VideoView2.
      * @hide TODO: remove once the implementation is revised
      */
     @RestrictTo(LIBRARY_GROUP)
     public void setController(MediaControllerCompat controller) {
         mController = controller;
         if (controller != null) {
-            mControls = controller.getTransportControls();
+            mControls = mController.getTransportControls();
             // Set mMetadata and mPlaybackState to existing MediaSession variables since they may
             // be called before the callback is called
             mPlaybackState = mController.getPlaybackState();
@@ -429,8 +408,6 @@
      */
     @RestrictTo(LIBRARY_GROUP)
     public void setButtonVisibility(@Button int button, /*@Visibility*/ int visibility) {
-        // TODO: add member variables for Fast-Forward/Prvious/Rewind buttons to save visibility in
-        // order to prevent being overriden inside updateLayout().
         switch (button) {
             case MediaControlView2.BUTTON_PLAY_PAUSE:
                 if (mPlayPauseButton != null && canPause()) {
@@ -517,7 +494,6 @@
         return false;
     }
 
-    // TODO: Should this function be removed?
     @Override
     public boolean onTrackballEvent(MotionEvent ev) {
         return false;
@@ -546,17 +522,13 @@
             manager.getDefaultDisplay().getSize(screenSize);
             int screenWidth = screenSize.x;
             int screenHeight = screenSize.y;
-            int fullIconSize = mResources.getDimensionPixelSize(R.dimen.mcv2_full_icon_size);
-            int embeddedIconSize = mResources.getDimensionPixelSize(
-                    R.dimen.mcv2_embedded_icon_size);
-            int marginSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_margin);
+            int iconSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_size);
 
-            // TODO: add support for Advertisement Mode.
             if (mMediaType == MEDIA_TYPE_DEFAULT) {
                 // Max number of icons inside BottomBarRightView for Music mode is 4.
                 int maxIconCount = 4;
-                updateLayout(maxIconCount, fullIconSize, embeddedIconSize, marginSize, currWidth,
-                        currHeight, screenWidth, screenHeight);
+                updateLayout(maxIconCount, iconSize, currWidth, currHeight, screenWidth,
+                        screenHeight);
 
             } else if (mMediaType == MEDIA_TYPE_MUSIC) {
                 if (mNeedUXUpdate) {
@@ -575,13 +547,12 @@
 
                 // Max number of icons inside BottomBarRightView for Music mode is 3.
                 int maxIconCount = 3;
-                updateLayout(maxIconCount, fullIconSize, embeddedIconSize, marginSize, currWidth,
-                        currHeight, screenWidth, screenHeight);
+                updateLayout(maxIconCount, iconSize, currWidth, currHeight, screenWidth,
+                        screenHeight);
             }
             mPrevWidth = currWidth;
             mPrevHeight = currHeight;
         }
-        // TODO: move this to a different location.
         // Update title bar parameters in order to avoid overlap between title view and the right
         // side of the title bar.
         updateTitleBarLayout();
@@ -591,7 +562,6 @@
     public void setEnabled(boolean enabled) {
         super.setEnabled(enabled);
 
-        // TODO: Merge the below code with disableUnsupportedButtons().
         if (mPlayPauseButton != null) {
             mPlayPauseButton.setEnabled(enabled);
         }
@@ -626,19 +596,16 @@
         }
     }
 
-    // TODO (b/77158231) revive once androidx.mediarouter.* packagaes are available.
-    /*
     void setRouteSelector(MediaRouteSelector selector) {
         mRouteSelector = selector;
         if (mRouteSelector != null && !mRouteSelector.isEmpty()) {
-            mRouteButton.setRouteSelector(selector, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+            mRouteButton.setRouteSelector(selector);
             mRouteButton.setVisibility(View.VISIBLE);
         } else {
             mRouteButton.setRouteSelector(MediaRouteSelector.EMPTY);
             mRouteButton.setVisibility(View.GONE);
         }
     }
-    */
 
     ///////////////////////////////////////////////////
     // Protected or private methods
@@ -698,14 +665,12 @@
      *
      * @return The controller view.
      */
-    // TODO: This was "protected". Determine if it should be protected in MCV2.
     private ViewGroup makeControllerView() {
         ViewGroup root = (ViewGroup) inflateLayout(getContext(), R.layout.media_controller);
         initControllerView(root);
         return root;
     }
 
-    // TODO(b/76444971) make sure this is compatible with ApiHelper's one in updatable.
     private View inflateLayout(Context context, int resId) {
         LayoutInflater inflater = (LayoutInflater) context
                 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
@@ -723,8 +688,7 @@
             mBackButton.setOnClickListener(mBackListener);
             mBackButton.setVisibility(View.GONE);
         }
-        // TODO (b/77158231) revive
-        // mRouteButton = v.findViewById(R.id.cast);
+        mRouteButton = v.findViewById(R.id.cast);
 
         // Relating to Center View
         mCenterView = v.findViewById(R.id.center_view);
@@ -735,7 +699,7 @@
         mMinimalExtraView = (LinearLayout) v.findViewById(R.id.minimal_extra_view);
         LinearLayout.LayoutParams params =
                 (LinearLayout.LayoutParams) mMinimalExtraView.getLayoutParams();
-        int iconSize = mResources.getDimensionPixelSize(R.dimen.mcv2_embedded_icon_size);
+        int iconSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_size);
         int marginSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_margin);
         params.setMargins(0, (iconSize + marginSize * 2) * (-1), 0, 0);
         mMinimalExtraView.setLayoutParams(params);
@@ -778,7 +742,6 @@
         mFullScreenButton = v.findViewById(R.id.fullscreen);
         if (mFullScreenButton != null) {
             mFullScreenButton.setOnClickListener(mFullScreenListener);
-            // TODO: Show Fullscreen button when only it is possible.
         }
         mOverflowButtonRight = v.findViewById(R.id.overflow_right);
         if (mOverflowButtonRight != null) {
@@ -815,10 +778,8 @@
         mEmbeddedSettingsItemWidth = mResources.getDimensionPixelSize(
                 R.dimen.mcv2_embedded_settings_width);
         mFullSettingsItemWidth = mResources.getDimensionPixelSize(R.dimen.mcv2_full_settings_width);
-        mEmbeddedSettingsItemHeight = mResources.getDimensionPixelSize(
-                R.dimen.mcv2_embedded_settings_height);
-        mFullSettingsItemHeight = mResources.getDimensionPixelSize(
-                R.dimen.mcv2_full_settings_height);
+        mSettingsItemHeight = mResources.getDimensionPixelSize(
+                R.dimen.mcv2_settings_height);
         mSettingsWindowMargin = (-1) * mResources.getDimensionPixelSize(
                 R.dimen.mcv2_settings_offset);
         mSettingsWindow = new PopupWindow(mSettingsListView, mEmbeddedSettingsItemWidth,
@@ -840,13 +801,6 @@
             if (mFfwdButton != null && !canSeekForward()) {
                 mFfwdButton.setEnabled(false);
             }
-            // TODO What we really should do is add a canSeek to the MediaPlayerControl interface;
-            // this scheme can break the case when applications want to allow seek through the
-            // progress bar but disable forward/backward buttons.
-            //
-            // However, currently the flags SEEK_BACKWARD_AVAILABLE, SEEK_FORWARD_AVAILABLE,
-            // and SEEK_AVAILABLE are all (un)set together; as such the aforementioned issue
-            // shouldn't arise in existing applications.
             if (mProgress != null && !canSeekBackward() && !canSeekForward()) {
                 mProgress.setEnabled(false);
             }
@@ -1078,7 +1032,11 @@
     private final OnClickListener mBackListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
-            // TODO: implement
+            View parent = (View) getParent();
+            if (parent != null) {
+                parent.onKeyDown(KeyEvent.KEYCODE_BACK,
+                        new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK));
+            }
         }
     };
 
@@ -1105,8 +1063,11 @@
     private final OnClickListener mFullScreenListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
+            if (mOnFullScreenListener == null) {
+                return;
+            }
+
             final boolean isEnteringFullScreen = !mIsFullScreen;
-            // TODO: Re-arrange the button layouts according to the UX.
             if (isEnteringFullScreen) {
                 mFullScreenButton.setImageDrawable(
                         mResources.getDrawable(R.drawable.ic_fullscreen_exit, null));
@@ -1114,11 +1075,9 @@
                 mFullScreenButton.setImageDrawable(
                         mResources.getDrawable(R.drawable.ic_fullscreen, null));
             }
-            Bundle args = new Bundle();
-            args.putBoolean(ARGUMENT_KEY_FULLSCREEN, isEnteringFullScreen);
-            mController.sendCommand(COMMAND_SET_FULLSCREEN, args, null);
-
             mIsFullScreen = isEnteringFullScreen;
+            mOnFullScreenListener.onFullScreen(MediaControlView2.this,
+                    mIsFullScreen);
         }
     };
 
@@ -1183,7 +1142,6 @@
                         mSubSettingsAdapter.setCheckPosition(mSelectedSpeedIndex);
                         mSettingsMode = SETTINGS_MODE_PLAYBACK_SPEED;
                     } else if (position == SETTINGS_MODE_HELP) {
-                        // TODO: implement this.
                         mSettingsWindow.dismiss();
                         return;
                     }
@@ -1214,7 +1172,6 @@
                     mSettingsWindow.dismiss();
                     break;
                 case SETTINGS_MODE_HELP:
-                    // TODO: implement this.
                     break;
                 case SETTINGS_MODE_SUBTITLE_TRACK:
                     if (position != mSelectedSubtitleTrackIndex) {
@@ -1240,7 +1197,6 @@
                     mSettingsWindow.dismiss();
                     break;
                 case SETTINGS_MODE_VIDEO_QUALITY:
-                    // TODO: add support for video quality
                     mSelectedVideoQualityIndex = position;
                     mSettingsWindow.dismiss();
                     break;
@@ -1350,21 +1306,14 @@
         }
     }
 
-    private void updateLayout(int maxIconCount, int fullIconSize, int embeddedIconSize,
-             int marginSize, int currWidth, int currHeight, int screenWidth, int screenHeight) {
-        int fullBottomBarRightWidthMax = fullIconSize * maxIconCount
-                + marginSize * (maxIconCount * 2);
-        int embeddedBottomBarRightWidthMax = embeddedIconSize * maxIconCount
-                + marginSize * (maxIconCount * 2);
+    private void updateLayout(int maxIconCount, int iconSize, int currWidth,
+             int currHeight, int screenWidth, int screenHeight) {
+        int bottomBarRightWidthMax = iconSize * maxIconCount;
         int fullWidth = mTransportControls.getWidth() + mTimeView.getWidth()
-                + fullBottomBarRightWidthMax;
-        int embeddedWidth = mTimeView.getWidth() + embeddedBottomBarRightWidthMax;
+                + bottomBarRightWidthMax;
+        int embeddedWidth = mTimeView.getWidth() + bottomBarRightWidthMax;
         int screenMaxLength = Math.max(screenWidth, screenHeight);
 
-        if (fullWidth > screenMaxLength) {
-            // TODO: screen may be smaller than the length needed for Full size.
-        }
-
         boolean isFullSize = (mMediaType == MEDIA_TYPE_DEFAULT) ? (currWidth == screenMaxLength) :
                 (currWidth == screenWidth && currHeight == screenHeight);
 
@@ -1403,6 +1352,11 @@
                 // Relating to Title Bar
                 mTitleBar.setVisibility(View.VISIBLE);
                 mBackButton.setVisibility(View.GONE);
+                mTitleView.setPadding(
+                        mResources.getDimensionPixelSize(R.dimen.mcv2_embedded_icon_padding),
+                        mTitleView.getPaddingTop(),
+                        mTitleView.getPaddingRight(),
+                        mTitleView.getPaddingBottom());
 
                 // Relating to Full Screen Button
                 mMinimalExtraView.setVisibility(View.GONE);
@@ -1431,6 +1385,11 @@
                 // Relating to Title Bar
                 mTitleBar.setVisibility(View.VISIBLE);
                 mBackButton.setVisibility(View.VISIBLE);
+                mTitleView.setPadding(
+                        0,
+                        mTitleView.getPaddingTop(),
+                        mTitleView.getPaddingRight(),
+                        mTitleView.getPaddingBottom());
 
                 // Relating to Full Screen Button
                 mMinimalExtraView.setVisibility(View.GONE);
@@ -1523,7 +1482,6 @@
                 mRewButton.setVisibility(View.GONE);
             }
         }
-        // TODO: Add support for Next and Previous buttons
         mNextButton = v.findViewById(R.id.next);
         if (mNextButton != null) {
             mNextButton.setOnClickListener(mNextListener);
@@ -1590,15 +1548,12 @@
         mSettingsWindow.setWidth(itemWidth);
 
         // Calculate height of window and show
-        int itemHeight = (mSizeType == SIZE_TYPE_EMBEDDED)
-                ? mEmbeddedSettingsItemHeight : mFullSettingsItemHeight;
-        int totalHeight = adapter.getCount() * itemHeight;
+        int totalHeight = adapter.getCount() * mSettingsItemHeight;
         mSettingsWindow.dismiss();
         mSettingsWindow.showAsDropDown(this, mSettingsWindowMargin,
                 mSettingsWindowMargin - totalHeight, Gravity.BOTTOM | Gravity.RIGHT);
     }
 
-    @RequiresApi(26) // TODO correct minSdk API use incompatibilities and remove before release.
     private class MediaControllerCallback extends MediaControllerCompat.Callback {
         @Override
         public void onPlaybackStateChanged(PlaybackStateCompat state) {
@@ -1670,16 +1625,12 @@
                 for (final PlaybackStateCompat.CustomAction action : customActions) {
                     ImageButton button = new ImageButton(getContext(),
                             null /* AttributeSet */, 0 /* Style */);
-                    // TODO: Apply R.style.BottomBarButton to this button using library context.
                     // Refer Constructor with argument (int defStyleRes) of View.java
                     button.setImageResource(action.getIcon());
-                    button.setTooltipText(action.getName());
                     final String actionString = action.getAction().toString();
                     button.setOnClickListener(new OnClickListener() {
                         @Override
                         public void onClick(View v) {
-                            // TODO: Currently, we are just sending extras that came from session.
-                            // Is it the right behavior?
                             mControls.sendCustomAction(actionString, action.getExtras());
                             setVisibility(View.VISIBLE);
                         }
@@ -1708,7 +1659,6 @@
                     mAudioTrackCount = extras.getInt(KEY_AUDIO_TRACK_COUNT);
                     mAudioTrackList = new ArrayList<String>();
                     if (mAudioTrackCount > 0) {
-                        // TODO: add more text about track info.
                         for (int i = 0; i < mAudioTrackCount; i++) {
                             String track = mResources.getString(
                                     R.string.MediaControlView2_audio_track_number_text, i + 1);
@@ -1787,14 +1737,12 @@
         @Override
         public long getItemId(int position) {
             // Auto-generated method stub--does not have any purpose here
-            // TODO: implement this.
             return 0;
         }
 
         @Override
         public Object getItem(int position) {
             // Auto-generated method stub--does not have any purpose here
-            // TODO: implement this.
             return null;
         }
 
@@ -1838,7 +1786,6 @@
         }
     }
 
-    // TODO: extend this class from SettingsAdapter
     private class SubSettingsAdapter extends BaseAdapter {
         private List<String> mTexts;
         private int mCheckPosition;
@@ -1865,14 +1812,12 @@
         @Override
         public long getItemId(int position) {
             // Auto-generated method stub--does not have any purpose here
-            // TODO: implement this.
             return 0;
         }
 
         @Override
         public Object getItem(int position) {
             // Auto-generated method stub--does not have any purpose here
-            // TODO: implement this.
             return null;
         }
 
diff --git a/media-widget/src/main/java/androidx/media/widget/RoutePlayer.java b/media-widget/src/main/java/androidx/media/widget/RoutePlayer.java
new file mode 100644
index 0000000..00ac36a
--- /dev/null
+++ b/media-widget/src/main/java/androidx/media/widget/RoutePlayer.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.media.session.MediaSession;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.mediarouter.media.MediaItemStatus;
+import androidx.mediarouter.media.MediaRouter;
+import androidx.mediarouter.media.MediaSessionStatus;
+import androidx.mediarouter.media.RemotePlaybackClient;
+import androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback;
+import androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback;
+import androidx.mediarouter.media.RemotePlaybackClient.StatusCallback;
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+public class RoutePlayer extends MediaSession.Callback {
+    public static final long PLAYBACK_ACTIONS = PlaybackStateCompat.ACTION_PAUSE
+            | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_SEEK_TO
+            | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND;
+
+    private RemotePlaybackClient mClient;
+    private String mSessionId;
+    private String mItemId;
+    private PlayerEventCallback mCallback;
+
+    private StatusCallback mStatusCallback = new StatusCallback() {
+        @Override
+        public void onItemStatusChanged(Bundle data,
+                                        String sessionId, MediaSessionStatus sessionStatus,
+                                        String itemId, MediaItemStatus itemStatus) {
+            updateSessionStatus(sessionId, sessionStatus);
+            updateItemStatus(itemId, itemStatus);
+        }
+    };
+
+    public RoutePlayer(Context context, MediaRouter.RouteInfo route) {
+        mClient = new RemotePlaybackClient(context, route);
+        mClient.setStatusCallback(mStatusCallback);
+        if (mClient.isSessionManagementSupported()) {
+            mClient.startSession(null, new SessionActionCallback() {
+                @Override
+                public void onResult(Bundle data,
+                                     String sessionId, MediaSessionStatus sessionStatus) {
+                    updateSessionStatus(sessionId, sessionStatus);
+                }
+            });
+        }
+    }
+
+    @Override
+    public void onPlay() {
+        if (mClient.isSessionManagementSupported()) {
+            mClient.resume(null, new SessionActionCallback() {
+                @Override
+                public void onResult(Bundle data,
+                                     String sessionId, MediaSessionStatus sessionStatus) {
+                    updateSessionStatus(sessionId, sessionStatus);
+                }
+            });
+        }
+    }
+
+    @Override
+    public void onPause() {
+        if (mClient.isSessionManagementSupported()) {
+            mClient.pause(null, new SessionActionCallback() {
+                @Override
+                public void onResult(Bundle data,
+                                     String sessionId, MediaSessionStatus sessionStatus) {
+                    updateSessionStatus(sessionId, sessionStatus);
+                }
+            });
+        }
+    }
+
+    @Override
+    public void onSeekTo(long pos) {
+        if (mClient.isSessionManagementSupported()) {
+            mClient.seek(mItemId, pos, null, new ItemActionCallback() {
+                @Override
+                public void onResult(Bundle data,
+                                     String sessionId, MediaSessionStatus sessionStatus,
+                                     String itemId, MediaItemStatus itemStatus) {
+                    updateSessionStatus(sessionId, sessionStatus);
+                    updateItemStatus(itemId, itemStatus);
+                }
+            });
+        }
+    }
+
+    @Override
+    public void onStop() {
+        if (mClient.isSessionManagementSupported()) {
+            mClient.stop(null, new SessionActionCallback() {
+                @Override
+                public void onResult(Bundle data,
+                                     String sessionId, MediaSessionStatus sessionStatus) {
+                    updateSessionStatus(sessionId, sessionStatus);
+                }
+            });
+        }
+    }
+
+    /**
+     * Sets a callback to be notified of events for this player.
+     * @param callback the callback to receive the events.
+     */
+    public void setPlayerEventCallback(PlayerEventCallback callback) {
+        mCallback = callback;
+    }
+
+    // b/77556429
+//    public void openVideo(DataSourceDesc dsd) {
+//        mClient.play(dsd.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() {
+//            @Override
+//            public void onResult(Bundle data,
+//                                 String sessionId, MediaSessionStatus sessionStatus,
+//                                 String itemId, MediaItemStatus itemStatus) {
+//                updateSessionStatus(sessionId, sessionStatus);
+//                updateItemStatus(itemId, itemStatus);
+//                playInternal(dsd.getUri());
+//            }
+//        });
+//    }
+
+    /**
+     * Opens the video based on the given uri and updates the media session and item statuses.
+     * @param uri link to the video
+     */
+    public void openVideo(Uri uri) {
+        mClient.play(uri, "video/mp4", null, 0, null, new ItemActionCallback() {
+            @Override
+            public void onResult(Bundle data,
+                                 String sessionId, MediaSessionStatus sessionStatus,
+                                 String itemId, MediaItemStatus itemStatus) {
+                updateSessionStatus(sessionId, sessionStatus);
+                updateItemStatus(itemId, itemStatus);
+            }
+        });
+    }
+
+    /**
+     * Releases the {@link RemotePlaybackClient} and {@link PlayerEventCallback} instances.
+     */
+    public void release() {
+        if (mClient != null) {
+            mClient.release();
+            mClient = null;
+        }
+        if (mCallback != null) {
+            mCallback = null;
+        }
+    }
+
+    private void playInternal(Uri uri) {
+        mClient.play(uri, "video/mp4", null, 0, null, new ItemActionCallback() {
+            @Override
+            public void onResult(Bundle data,
+                                 String sessionId, MediaSessionStatus sessionStatus,
+                                 String itemId, MediaItemStatus itemStatus) {
+                updateSessionStatus(sessionId, sessionStatus);
+                updateItemStatus(itemId, itemStatus);
+            }
+        });
+    }
+
+    private void updateSessionStatus(String sessionId, MediaSessionStatus sessionStatus) {
+        mSessionId = sessionId;
+    }
+
+    private void updateItemStatus(String itemId, MediaItemStatus itemStatus) {
+        mItemId = itemId;
+        if (itemStatus == null || mCallback == null) return;
+        mCallback.onPlayerStateChanged(itemStatus);
+    }
+
+    /**
+     * A callback class to receive notifications for events on the route player.
+     */
+    public abstract static class PlayerEventCallback {
+        /**
+         * Override to handle changes in playback state.
+         *
+         * @param itemStatus The new MediaItemStatus of the RoutePlayer
+         */
+        public void onPlayerStateChanged(MediaItemStatus itemStatus) { }
+    }
+}
diff --git a/media-widget/src/main/java/androidx/media/widget/SubtitleView.java b/media-widget/src/main/java/androidx/media/widget/SubtitleView.java
new file mode 100644
index 0000000..1faff2f
--- /dev/null
+++ b/media-widget/src/main/java/androidx/media/widget/SubtitleView.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.os.Looper;
+import android.util.AttributeSet;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.media.subtitle.SubtitleController.Anchor;
+import androidx.media.subtitle.SubtitleTrack.RenderingWidget;
+
+@RequiresApi(21)
+class SubtitleView extends BaseLayout implements Anchor {
+    private static final String TAG = "SubtitleView";
+
+    private RenderingWidget mSubtitleWidget;
+    private RenderingWidget.OnChangedListener mSubtitlesChangedListener;
+
+    SubtitleView(Context context) {
+        this(context, null);
+    }
+
+    SubtitleView(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    SubtitleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    SubtitleView(
+            Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public void setSubtitleWidget(RenderingWidget subtitleWidget) {
+        if (mSubtitleWidget == subtitleWidget) {
+            return;
+        }
+
+        final boolean attachedToWindow = isAttachedToWindow();
+        if (mSubtitleWidget != null) {
+            if (attachedToWindow) {
+                mSubtitleWidget.onDetachedFromWindow();
+            }
+
+            mSubtitleWidget.setOnChangedListener(null);
+        }
+        mSubtitleWidget = subtitleWidget;
+
+        if (subtitleWidget != null) {
+            if (mSubtitlesChangedListener == null) {
+                mSubtitlesChangedListener = new RenderingWidget.OnChangedListener() {
+                    @Override
+                    public void onChanged(RenderingWidget renderingWidget) {
+                        invalidate();
+                    }
+                };
+            }
+
+            setWillNotDraw(false);
+            subtitleWidget.setOnChangedListener(mSubtitlesChangedListener);
+
+            if (attachedToWindow) {
+                subtitleWidget.onAttachedToWindow();
+                requestLayout();
+            }
+        } else {
+            setWillNotDraw(true);
+        }
+
+        invalidate();
+    }
+
+    @Override
+    public Looper getSubtitleLooper() {
+        return Looper.getMainLooper();
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        if (mSubtitleWidget != null) {
+            mSubtitleWidget.onAttachedToWindow();
+        }
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        if (mSubtitleWidget != null) {
+            mSubtitleWidget.onDetachedFromWindow();
+        }
+    }
+
+    @Override
+    public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+
+        if (mSubtitleWidget != null) {
+            final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+            final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+            mSubtitleWidget.setSize(width, height);
+        }
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        super.draw(canvas);
+
+        if (mSubtitleWidget != null) {
+            final int saveCount = canvas.save();
+            canvas.translate(getPaddingLeft(), getPaddingTop());
+            mSubtitleWidget.draw(canvas);
+            canvas.restoreToCount(saveCount);
+        }
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return SubtitleView.class.getName();
+    }
+}
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoView2.java b/media-widget/src/main/java/androidx/media/widget/VideoView2.java
index a8ea450..a851370 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoView2.java
+++ b/media-widget/src/main/java/androidx/media/widget/VideoView2.java
@@ -19,38 +19,17 @@
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
 import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Point;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
 import android.media.AudioAttributes;
-import android.media.AudioFocusRequest;
 import android.media.AudioManager;
-import android.media.MediaMetadataRetriever;
 import android.media.MediaPlayer;
-import android.media.PlaybackParams;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.ResultReceiver;
-import android.support.v4.media.MediaMetadataCompat;
 import android.support.v4.media.session.MediaControllerCompat;
-import android.support.v4.media.session.MediaControllerCompat.PlaybackInfo;
-import android.support.v4.media.session.MediaSessionCompat;
 import android.support.v4.media.session.PlaybackStateCompat;
 import android.util.AttributeSet;
-import android.util.DisplayMetrics;
 import android.util.Log;
-import android.util.Pair;
-import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityManager;
-import android.widget.ImageView;
-import android.widget.TextView;
 import android.widget.VideoView;
 
 import androidx.annotation.IntDef;
@@ -59,25 +38,21 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
+import androidx.media.AudioAttributesCompat;
 import androidx.media.DataSourceDesc;
 import androidx.media.MediaItem2;
 import androidx.media.MediaMetadata2;
 import androidx.media.SessionToken2;
-import androidx.palette.graphics.Palette;
 
-import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.Executor;
 
-// TODO: Replace MediaSession wtih MediaSession2 once MediaSession2 is submitted.
 /**
- * @hide
- * Displays a video file.  VideoView2 class is a View class which is wrapping {@link MediaPlayer}
- * so that developers can easily implement a video rendering application.
+ * Displays a video file.  VideoView2 class is a ViewGroup class which is wrapping
+ * {@link MediaPlayer} so that developers can easily implement a video rendering application.
  *
  * <p>
  * <em> Data sources that VideoView2 supports : </em>
@@ -98,20 +73,12 @@
  * VideoView2 covers and inherits the most of
  * VideoView's functionalities. The main differences are
  * <ul>
- * <li> VideoView2 inherits FrameLayout and renders videos using SurfaceView and TextureView
+ * <li> VideoView2 inherits ViewGroup and renders videos using SurfaceView and TextureView
  * selectively while VideoView inherits SurfaceView class.
  * <li> VideoView2 is integrated with MediaControlView2 and a default MediaControlView2 instance is
- * attached to VideoView2 by default. If a developer does not want to use the default
- * MediaControlView2, needs to set enableControlView attribute to false. For instance,
- * <pre>
- * &lt;VideoView2
- *     android:id="@+id/video_view"
- *     xmlns:widget="http://schemas.android.com/apk/com.android.media.update"
- *     widget:enableControlView="false" /&gt;
- * </pre>
- * If a developer wants to attach a customed MediaControlView2, then set enableControlView attribute
- * to false and assign the customed media control widget using {@link #setMediaControlView2}.
- * <li> VideoView2 is integrated with MediaPlayer while VideoView is integrated with MediaPlayer.
+ * attached to VideoView2 by default.
+ * <li> If a developer wants to attach a customed MediaControlView2,
+ * assign the customed media control widget using {@link #setMediaControlView2}.
  * <li> VideoView2 is integrated with MediaSession and so it responses with media key events.
  * A VideoView2 keeps a MediaSession instance internally and connects it to a corresponding
  * MediaControlView2 instance.
@@ -134,8 +101,7 @@
  * {@link android.app.Activity#onRestoreInstanceState}.
  */
 @RequiresApi(21) // TODO correct minSdk API use incompatibilities and remove before release.
-@RestrictTo(LIBRARY_GROUP)
-public class VideoView2 extends BaseLayout implements VideoViewInterface.SurfaceListener {
+public class VideoView2 extends BaseLayout {
     /** @hide */
     @RestrictTo(LIBRARY_GROUP)
     @IntDef({
@@ -160,171 +126,10 @@
     public static final int VIEW_TYPE_TEXTUREVIEW = 1;
 
     private static final String TAG = "VideoView2";
-    private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
-    private static final long DEFAULT_SHOW_CONTROLLER_INTERVAL_MS = 2000;
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final boolean USE_MP2 = Log.isLoggable("VV2MP2", Log.DEBUG);
 
-    private static final int STATE_ERROR = -1;
-    private static final int STATE_IDLE = 0;
-    private static final int STATE_PREPARING = 1;
-    private static final int STATE_PREPARED = 2;
-    private static final int STATE_PLAYING = 3;
-    private static final int STATE_PAUSED = 4;
-    private static final int STATE_PLAYBACK_COMPLETED = 5;
-
-    private static final int INVALID_TRACK_INDEX = -1;
-    private static final float INVALID_SPEED = 0f;
-
-    private static final int SIZE_TYPE_EMBEDDED = 0;
-    private static final int SIZE_TYPE_FULL = 1;
-    // TODO: add support for Minimal size type.
-    private static final int SIZE_TYPE_MINIMAL = 2;
-
-    private AccessibilityManager mAccessibilityManager;
-    private AudioManager mAudioManager;
-    private AudioAttributes mAudioAttributes;
-    private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain
-    private boolean mAudioFocused = false;
-
-    private Pair<Executor, OnCustomActionListener> mCustomActionListenerRecord;
-    private OnViewTypeChangedListener mViewTypeChangedListener;
-    private OnFullScreenRequestListener mFullScreenRequestListener;
-
-    private VideoViewInterface mCurrentView;
-    private VideoTextureView mTextureView;
-    private VideoSurfaceView mSurfaceView;
-
-    private MediaPlayer mMediaPlayer;
-    private DataSourceDesc mDsd;
-    private MediaControlView2 mMediaControlView;
-    private MediaSessionCompat mMediaSession;
-    private MediaControllerCompat mMediaController;
-    private MediaMetadata2 mMediaMetadata;
-    private MediaMetadataRetriever mRetriever;
-    private boolean mNeedUpdateMediaType;
-    private Bundle mMediaTypeData;
-    private String mTitle;
-
-    // TODO: move music view inside SurfaceView/TextureView or implement VideoViewInterface.
-    private WindowManager mManager;
-    private Resources mResources;
-    private View mMusicView;
-    private Drawable mMusicAlbumDrawable;
-    private String mMusicTitleText;
-    private String mMusicArtistText;
-    private boolean mIsMusicMediaType;
-    private int mPrevWidth;
-    private int mPrevHeight;
-    private int mDominantColor;
-    private int mSizeType;
-
-    private PlaybackStateCompat.Builder mStateBuilder;
-    private List<PlaybackStateCompat.CustomAction> mCustomActionList;
-
-    private int mTargetState = STATE_IDLE;
-    private int mCurrentState = STATE_IDLE;
-    private int mCurrentBufferPercentage;
-    private long mSeekWhenPrepared;  // recording the seek position while preparing
-
-    private int mVideoWidth;
-    private int mVideoHeight;
-
-    private ArrayList<Integer> mVideoTrackIndices;
-    private ArrayList<Integer> mAudioTrackIndices;
-    // private ArrayList<Pair<Integer, SubtitleTrack>> mSubtitleTrackIndices;
-    // private SubtitleController mSubtitleController;
-
-    // selected video/audio/subtitle track index as MediaPlayer returns
-    private int mSelectedVideoTrackIndex;
-    private int mSelectedAudioTrackIndex;
-    private int mSelectedSubtitleTrackIndex;
-
-    // private SubtitleView mSubtitleView;
-    private boolean mSubtitleEnabled;
-
-    private float mSpeed;
-    // TODO: Remove mFallbackSpeed when integration with MediaPlayer's new setPlaybackParams().
-    // Refer: https://docs.google.com/document/d/1nzAfns6i2hJ3RkaUre3QMT6wsDedJ5ONLiA_OOBFFX8/edit
-    private float mFallbackSpeed;  // keep the original speed before 'pause' is called.
-    private float mVolumeLevelFloat;
-    private int mVolumeLevel;
-
-    private long mShowControllerIntervalMs;
-
-    // private MediaRouter mMediaRouter;
-    // private MediaRouteSelector mRouteSelector;
-    // private MediaRouter.RouteInfo mRoute;
-    // private RoutePlayer mRoutePlayer;
-
-    // TODO (b/77158231)
-    /*
-    private final MediaRouter.Callback mRouterCallback = new MediaRouter.Callback() {
-        @Override
-        public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
-            if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
-                // Stop local playback (if necessary)
-                resetPlayer();
-                mRoute = route;
-                mRoutePlayer = new RoutePlayer(getContext(), route);
-                mRoutePlayer.setPlayerEventCallback(new RoutePlayer.PlayerEventCallback() {
-                    @Override
-                    public void onPlayerStateChanged(MediaItemStatus itemStatus) {
-                        PlaybackStateCompat.Builder psBuilder = new PlaybackStateCompat.Builder();
-                        psBuilder.setActions(RoutePlayer.PLAYBACK_ACTIONS);
-                        long position = itemStatus.getContentPosition();
-                        switch (itemStatus.getPlaybackState()) {
-                            case MediaItemStatus.PLAYBACK_STATE_PENDING:
-                                psBuilder.setState(PlaybackStateCompat.STATE_NONE, position, 0);
-                                mCurrentState = STATE_IDLE;
-                                break;
-                            case MediaItemStatus.PLAYBACK_STATE_PLAYING:
-                                psBuilder.setState(PlaybackStateCompat.STATE_PLAYING, position, 1);
-                                mCurrentState = STATE_PLAYING;
-                                break;
-                            case MediaItemStatus.PLAYBACK_STATE_PAUSED:
-                                psBuilder.setState(PlaybackStateCompat.STATE_PAUSED, position, 0);
-                                mCurrentState = STATE_PAUSED;
-                                break;
-                            case MediaItemStatus.PLAYBACK_STATE_BUFFERING:
-                                psBuilder.setState(
-                                        PlaybackStateCompat.STATE_BUFFERING, position, 0);
-                                mCurrentState = STATE_PAUSED;
-                                break;
-                            case MediaItemStatus.PLAYBACK_STATE_FINISHED:
-                                psBuilder.setState(PlaybackStateCompat.STATE_STOPPED, position, 0);
-                                mCurrentState = STATE_PLAYBACK_COMPLETED;
-                                break;
-                        }
-
-                        PlaybackStateCompat pbState = psBuilder.build();
-                        mMediaSession.setPlaybackState(pbState);
-
-                        MediaMetadataCompat.Builder mmBuilder = new MediaMetadataCompat.Builder();
-                        mmBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION,
-                                itemStatus.getContentDuration());
-                        mMediaSession.setMetadata(mmBuilder.build());
-                    }
-                });
-                // Start remote playback (if necessary)
-                mRoutePlayer.openVideo(mDsd);
-            }
-        }
-
-        @Override
-        public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route, int reason) {
-            if (mRoute != null && mRoutePlayer != null) {
-                mRoutePlayer.release();
-                mRoutePlayer = null;
-            }
-            if (mRoute == route) {
-                mRoute = null;
-            }
-            if (reason != MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
-                // TODO: Resume local playback  (if necessary)
-                openVideo(mDsd);
-            }
-        }
-    };
-    */
+    private VideoView2Impl mImpl;
 
     public VideoView2(@NonNull Context context) {
         this(context, null);
@@ -335,86 +140,20 @@
     }
 
     public VideoView2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
-        this(context, attrs, defStyleAttr, 0);
-    }
-
-    public VideoView2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-
-        mVideoWidth = 0;
-        mVideoHeight = 0;
-        mSpeed = 1.0f;
-        mFallbackSpeed = mSpeed;
-        mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
-        // TODO: add attributes to get this value.
-        mShowControllerIntervalMs = DEFAULT_SHOW_CONTROLLER_INTERVAL_MS;
-
-        mAccessibilityManager = (AccessibilityManager) context.getSystemService(
-                Context.ACCESSIBILITY_SERVICE);
-
-        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
-        mAudioAttributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA)
-                .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE).build();
-        setFocusable(true);
-        setFocusableInTouchMode(true);
-        requestFocus();
-
-        // TODO: try to keep a single child at a time rather than always having both.
-        mTextureView = new VideoTextureView(getContext());
-        mSurfaceView = new VideoSurfaceView(getContext());
-        LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
-                LayoutParams.MATCH_PARENT);
-        mTextureView.setLayoutParams(params);
-        mSurfaceView.setLayoutParams(params);
-        mTextureView.setSurfaceListener(this);
-        mSurfaceView.setSurfaceListener(this);
-
-        addView(mTextureView);
-        addView(mSurfaceView);
-
-        // mSubtitleView = new SubtitleView(getContext());
-        // mSubtitleView.setLayoutParams(params);
-        // mSubtitleView.setBackgroundColor(0);
-        // addView(mSubtitleView);
-
-        boolean enableControlView = (attrs == null) || attrs.getAttributeBooleanValue(
-                "http://schemas.android.com/apk/res/android",
-                "enableControlView", true);
-        if (enableControlView) {
-            mMediaControlView = new MediaControlView2(getContext());
+        super(context, attrs, defStyleAttr);
+        if (android.os.Build.VERSION.SDK_INT >= 28) {
+            if (USE_MP2) {
+                Log.d(TAG, "Create VideoView2ImplBase");
+                mImpl = new VideoView2ImplBase();
+            } else {
+                Log.d(TAG, "Create VideoView2ImplApi28WithMp1");
+                mImpl = new VideoView2ImplApi28WithMp1();
+            }
+        } else {
+            Log.d(TAG, "Create VideoView2ImplBaseWithMp1");
+            mImpl = new VideoView2ImplBaseWithMp1();
         }
-
-        mSubtitleEnabled = (attrs == null) || attrs.getAttributeBooleanValue(
-                "http://schemas.android.com/apk/res/android",
-                "enableSubtitle", false);
-
-        // TODO: Choose TextureView when SurfaceView cannot be created.
-        // Choose surface view by default
-        int viewType = (attrs == null) ? VideoView2.VIEW_TYPE_SURFACEVIEW
-                : attrs.getAttributeIntValue(
-                "http://schemas.android.com/apk/res/android",
-                "viewType", VideoView2.VIEW_TYPE_SURFACEVIEW);
-        if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) {
-            Log.d(TAG, "viewType attribute is surfaceView.");
-            mTextureView.setVisibility(View.GONE);
-            mSurfaceView.setVisibility(View.VISIBLE);
-            mCurrentView = mSurfaceView;
-        } else if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
-            Log.d(TAG, "viewType attribute is textureView.");
-            mTextureView.setVisibility(View.VISIBLE);
-            mSurfaceView.setVisibility(View.GONE);
-            mCurrentView = mTextureView;
-        }
-
-        // TODO (b/77158231)
-        /*
-        MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
-        builder.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
-        builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
-        builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
-        mRouteSelector = builder.build();
-        */
+        mImpl.initialize(this, context, attrs, defStyleAttr);
     }
 
     /**
@@ -425,15 +164,7 @@
      * @param intervalMs a time interval in milliseconds until VideoView2 hides MediaControlView2.
      */
     public void setMediaControlView2(MediaControlView2 mediaControlView, long intervalMs) {
-        mMediaControlView = mediaControlView;
-        mShowControllerIntervalMs = intervalMs;
-        // TODO: Call MediaControlView2.setRouteSelector only when cast availalbe.
-        // TODO (b/77158231)
-        // mMediaControlView.setRouteSelector(mRouteSelector);
-
-        if (isAttachedToWindow()) {
-            attachMediaControlView();
-        }
+        mImpl.setMediaControlView2(mediaControlView, intervalMs);
     }
 
     /**
@@ -441,7 +172,7 @@
      * {@link #setMediaControlView2} method.
      */
     public MediaControlView2 getMediaControlView2() {
-        return mMediaControlView;
+        return mImpl.getMediaControlView2();
     }
 
     /**
@@ -463,7 +194,7 @@
      */
     @RestrictTo(LIBRARY_GROUP)
     public MediaMetadata2 getMediaMetadata() {
-        return mMediaMetadata;
+        return mImpl.getMediaMetadata();
     }
 
     /**
@@ -478,10 +209,7 @@
      */
     @RestrictTo(LIBRARY_GROUP)
     public MediaControllerCompat getMediaController() {
-        if (mMediaSession == null) {
-            throw new IllegalStateException("MediaSession instance is not available.");
-        }
-        return mMediaController;
+        return mImpl.getMediaController();
     }
 
     /**
@@ -494,7 +222,6 @@
      */
     @RestrictTo(LIBRARY_GROUP)
     public SessionToken2 getMediaSessionToken() {
-        //return mProvider.getMediaSessionToken_impl();
         return null;
     }
 
@@ -505,10 +232,7 @@
      * @param enable shows closed caption or subtitles if this value is true, or hides.
      */
     public void setSubtitleEnabled(boolean enable) {
-        if (enable != mSubtitleEnabled) {
-            selectOrDeselectSubtitle(enable);
-        }
-        mSubtitleEnabled = enable;
+        mImpl.setSubtitleEnabled(enable);
     }
 
     /**
@@ -517,7 +241,7 @@
      * has been enabled by {@link #setSubtitleEnabled}.
      */
     public boolean isSubtitleEnabled() {
-        return mSubtitleEnabled;
+        return mImpl.isSubtitleEnabled();
     }
 
     /**
@@ -529,17 +253,18 @@
      * be reset to the normal speed 1.0f.
      * @param speed the playback speed. It should be positive.
      */
-    // TODO: Support this via MediaController2.
     public void setSpeed(float speed) {
-        if (speed <= 0.0f) {
-            Log.e(TAG, "Unsupported speed (" + speed + ") is ignored.");
-            return;
-        }
-        mSpeed = speed;
-        if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
-            applySpeed();
-        }
-        updatePlaybackState();
+        mImpl.setSpeed(speed);
+    }
+
+    /**
+     * Returns playback speed.
+     *
+     * It returns the same value that has been set by {@link #setSpeed}, if it was available value.
+     * If {@link #setSpeed} has not been called before, then the normal speed 1.0f will be returned.
+     */
+    public float getSpeed() {
+        return mImpl.getSpeed();
     }
 
     /**
@@ -558,14 +283,7 @@
      *                  playback.
      */
     public void setAudioFocusRequest(int focusGain) {
-        if (focusGain != AudioManager.AUDIOFOCUS_NONE
-                && focusGain != AudioManager.AUDIOFOCUS_GAIN
-                && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
-                && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
-                && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) {
-            throw new IllegalArgumentException("Illegal audio focus type " + focusGain);
-        }
-        mAudioFocusType = focusGain;
+        mImpl.setAudioFocusRequest(focusGain);
     }
 
     /**
@@ -574,10 +292,19 @@
      * @param attributes non-null <code>AudioAttributes</code>.
      */
     public void setAudioAttributes(@NonNull AudioAttributes attributes) {
-        if (attributes == null) {
-            throw new IllegalArgumentException("Illegal null AudioAttributes");
-        }
-        mAudioAttributes = attributes;
+        mImpl.setAudioAttributes(AudioAttributesCompat.wrap(attributes));
+    }
+
+    /**
+     * Sets the {@link AudioAttributesCompat} to be used during the playback of the video.
+     *
+     * @param attributes non-null <code>AudioAttributesCompat</code>.
+     *
+     * @hide TODO unhide and remove setAudioAttributes with framework attributes
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public void setAudioAttributes(@NonNull AudioAttributesCompat attributes) {
+        mImpl.setAudioAttributes(attributes);
     }
 
     /**
@@ -585,11 +312,11 @@
      *
      * @param path the path of the video.
      *
-     * @hide TODO remove
+     * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
     public void setVideoPath(String path) {
-        setVideoUri(Uri.parse(path));
+        mImpl.setVideoUri(Uri.parse(path));
     }
 
     /**
@@ -597,11 +324,11 @@
      *
      * @param uri the URI of the video.
      *
-     * @hide TODO remove
+     * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
     public void setVideoUri(Uri uri) {
-        setVideoUri(uri, null);
+        mImpl.setVideoUri(uri, null);
     }
 
     /**
@@ -613,13 +340,9 @@
      *                changed with key/value pairs through the headers parameter with
      *                "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
      *                to disallow or allow cross domain redirection.
-     *
-     * @hide TODO remove
      */
-    @RestrictTo(LIBRARY_GROUP)
-    public void setVideoUri(Uri uri, Map<String, String> headers) {
-        mSeekWhenPrepared = 0;
-        openVideo(uri, headers);
+    public void setVideoUri(Uri uri, @Nullable Map<String, String> headers) {
+        mImpl.setVideoUri(uri, headers);
     }
 
     /**
@@ -627,9 +350,11 @@
      * object to VideoView2 is {@link #setDataSource}.
      * @param mediaItem the MediaItem2 to play
      * @see #setDataSource
+     *
+     * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     public void setMediaItem(@NonNull MediaItem2 mediaItem) {
-        //mProvider.setMediaItem_impl(mediaItem);
     }
 
     /**
@@ -640,7 +365,6 @@
      */
     @RestrictTo(LIBRARY_GROUP)
     public void setDataSource(@NonNull DataSourceDesc dataSource) {
-        //mProvider.setDataSource_impl(dataSource);
     }
 
     /**
@@ -653,22 +377,7 @@
      * </ul>
      */
     public void setViewType(@ViewType int viewType) {
-        if (viewType == mCurrentView.getViewType()) {
-            return;
-        }
-        VideoViewInterface targetView;
-        if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
-            Log.d(TAG, "switching to TextureView");
-            targetView = mTextureView;
-        } else if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) {
-            Log.d(TAG, "switching to SurfaceView");
-            targetView = mSurfaceView;
-        } else {
-            throw new IllegalArgumentException("Unknown view type: " + viewType);
-        }
-        ((View) targetView).setVisibility(View.VISIBLE);
-        targetView.takeOver(mCurrentView);
-        requestLayout();
+        mImpl.setViewType(viewType);
     }
 
     /**
@@ -678,7 +387,7 @@
      */
     @ViewType
     public int getViewType() {
-        return mCurrentView.getViewType();
+        return mImpl.getViewType();
     }
 
     /**
@@ -689,17 +398,12 @@
      *                   buttons in {@link MediaControlView2}.
      * @param executor executor to run callbacks on.
      * @param listener A listener to be called when a custom button is clicked.
-     * @hide  TODO remove
+     * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
     public void setCustomActions(List<PlaybackStateCompat.CustomAction> actionList,
             Executor executor, OnCustomActionListener listener) {
-        mCustomActionList = actionList;
-        mCustomActionListenerRecord = new Pair<>(executor, listener);
-
-        // Create a new playback builder in order to clear existing the custom actions.
-        mStateBuilder = null;
-        updatePlaybackState();
+        mImpl.setCustomActions(actionList, executor, listener);
     }
 
     /**
@@ -711,54 +415,19 @@
     @VisibleForTesting
     @RestrictTo(LIBRARY_GROUP)
     public void setOnViewTypeChangedListener(OnViewTypeChangedListener l) {
-        mViewTypeChangedListener = l;
-    }
-
-    /**
-     * Registers a callback to be invoked when the fullscreen mode should be changed.
-     * @param l The callback that will be run
-     * @hide  TODO remove
-     */
-    @RestrictTo(LIBRARY_GROUP)
-    public void setFullScreenRequestListener(OnFullScreenRequestListener l) {
-        mFullScreenRequestListener = l;
+        mImpl.setOnViewTypeChangedListener(l);
     }
 
     @Override
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
-
-        // Create MediaSession
-        mMediaSession = new MediaSessionCompat(getContext(), "VideoView2MediaSession");
-        mMediaSession.setCallback(new MediaSessionCallback());
-        mMediaSession.setActive(true);
-        mMediaController = mMediaSession.getController();
-        // TODO (b/77158231)
-        // mMediaRouter = MediaRouter.getInstance(getContext());
-        // mMediaRouter.setMediaSession(mMediaSession);
-        // mMediaRouter.addCallback(mRouteSelector, mRouterCallback);
-        attachMediaControlView();
-        // TODO: remove this after moving MediaSession creating code inside initializing VideoView2
-        if (mCurrentState == STATE_PREPARED) {
-            extractTracks();
-            extractMetadata();
-            extractAudioMetadata();
-            if (mNeedUpdateMediaType) {
-                mMediaSession.sendSessionEvent(
-                        MediaControlView2.EVENT_UPDATE_MEDIA_TYPE_STATUS,
-                        mMediaTypeData);
-                mNeedUpdateMediaType = false;
-            }
-        }
+        mImpl.onAttachedToWindowImpl();
     }
 
     @Override
     public void onDetachedFromWindow() {
         super.onDetachedFromWindow();
-
-        mMediaSession.release();
-        mMediaSession = null;
-        mMediaController = null;
+        mImpl.onDetachedFromWindowImpl();
     }
 
     @Override
@@ -768,76 +437,25 @@
 
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
-        if (DEBUG) {
-            Log.d(TAG, "onTouchEvent(). mCurrentState=" + mCurrentState
-                    + ", mTargetState=" + mTargetState);
-        }
-        if (ev.getAction() == MotionEvent.ACTION_UP && mMediaControlView != null) {
-            if (!mIsMusicMediaType || mSizeType != SIZE_TYPE_FULL) {
-                toggleMediaControlViewVisibility();
-            }
-        }
-
+        mImpl.onTouchEventImpl(ev);
         return super.onTouchEvent(ev);
     }
 
     @Override
     public boolean onTrackballEvent(MotionEvent ev) {
-        if (ev.getAction() == MotionEvent.ACTION_UP && mMediaControlView != null) {
-            if (!mIsMusicMediaType || mSizeType != SIZE_TYPE_FULL) {
-                toggleMediaControlViewVisibility();
-            }
-        }
-
+        mImpl.onTrackballEventImpl(ev);
         return super.onTrackballEvent(ev);
     }
 
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
-        // TODO: Test touch event handling logic thoroughly and simplify the logic.
         return super.dispatchTouchEvent(ev);
     }
 
     @Override
     public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
-        if (mIsMusicMediaType) {
-            if (mPrevWidth != getMeasuredWidth()
-                    || mPrevHeight != getMeasuredHeight()) {
-                int currWidth = getMeasuredWidth();
-                int currHeight = getMeasuredHeight();
-                Point screenSize = new Point();
-                mManager.getDefaultDisplay().getSize(screenSize);
-                int screenWidth = screenSize.x;
-                int screenHeight = screenSize.y;
-
-                if (currWidth == screenWidth && currHeight == screenHeight) {
-                    int orientation = retrieveOrientation();
-                    if (orientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
-                        inflateMusicView(R.layout.full_landscape_music);
-                    } else {
-                        inflateMusicView(R.layout.full_portrait_music);
-                    }
-
-                    if (mSizeType != SIZE_TYPE_FULL) {
-                        mSizeType = SIZE_TYPE_FULL;
-                        // Remove existing mFadeOut callback
-                        mMediaControlView.removeCallbacks(mFadeOut);
-                        mMediaControlView.setVisibility(View.VISIBLE);
-                    }
-                } else {
-                    if (mSizeType != SIZE_TYPE_EMBEDDED) {
-                        mSizeType = SIZE_TYPE_EMBEDDED;
-                        inflateMusicView(R.layout.embedded_music);
-                        // Add new mFadeOut callback
-                        mMediaControlView.postDelayed(mFadeOut, mShowControllerIntervalMs);
-                    }
-                }
-                mPrevWidth = currWidth;
-                mPrevHeight = currHeight;
-            }
-        }
+        mImpl.onMeasureImpl(widthMeasureSpec, heightMeasureSpec);
     }
 
     /**
@@ -845,7 +463,6 @@
      *
      * @hide
      */
-    @VisibleForTesting
     @RestrictTo(LIBRARY_GROUP)
     public interface OnViewTypeChangedListener {
         /**
@@ -862,19 +479,6 @@
     }
 
     /**
-     * Interface definition of a callback to be invoked to inform the fullscreen mode is changed.
-     * Application should handle the fullscreen mode accordingly.
-     * @hide  TODO remove
-     */
-    @RestrictTo(LIBRARY_GROUP)
-    public interface OnFullScreenRequestListener {
-        /**
-         * Called to indicate a fullscreen mode change.
-         */
-        void onFullScreenRequest(View view, boolean fullScreen);
-    }
-
-    /**
      * Interface definition of a callback to be invoked to inform that a custom action is performed.
      * @hide  TODO remove
      */
@@ -889,901 +493,4 @@
          */
         void onCustomAction(String action, Bundle extras);
     }
-
-    ///////////////////////////////////////////////////
-    // Implements VideoViewInterface.SurfaceListener
-    ///////////////////////////////////////////////////
-
-    @Override
-    public void onSurfaceCreated(View view, int width, int height) {
-        if (DEBUG) {
-            Log.d(TAG, "onSurfaceCreated(). mCurrentState=" + mCurrentState
-                    + ", mTargetState=" + mTargetState + ", width/height: " + width + "/" + height
-                    + ", " + view.toString());
-        }
-        if (needToStart()) {
-            mMediaController.getTransportControls().play();
-        }
-    }
-
-    @Override
-    public void onSurfaceDestroyed(View view) {
-        if (DEBUG) {
-            Log.d(TAG, "onSurfaceDestroyed(). mCurrentState=" + mCurrentState
-                    + ", mTargetState=" + mTargetState + ", " + view.toString());
-        }
-    }
-
-    @Override
-    public void onSurfaceChanged(View view, int width, int height) {
-        // TODO: Do we need to call requestLayout here?
-        if (DEBUG) {
-            Log.d(TAG, "onSurfaceChanged(). width/height: " + width + "/" + height
-                    + ", " + view.toString());
-        }
-    }
-
-    @Override
-    public void onSurfaceTakeOverDone(VideoViewInterface view) {
-        if (DEBUG) {
-            Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view);
-        }
-        mCurrentView = view;
-        if (mViewTypeChangedListener != null) {
-            mViewTypeChangedListener.onViewTypeChanged(this, view.getViewType());
-        }
-        if (needToStart()) {
-            mMediaController.getTransportControls().play();
-        }
-    }
-
-    ///////////////////////////////////////////////////
-    // Protected or private methods
-    ///////////////////////////////////////////////////
-
-    private void attachMediaControlView() {
-        // Get MediaController from MediaSession and set it inside MediaControlView
-        mMediaControlView.setController(mMediaSession.getController());
-
-        LayoutParams params =
-                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
-        addView(mMediaControlView, params);
-    }
-
-    private boolean isInPlaybackState() {
-        // TODO (b/77158231)
-        // return (mMediaPlayer != null || mRoutePlayer != null)
-        return (mMediaPlayer != null)
-                && mCurrentState != STATE_ERROR
-                && mCurrentState != STATE_IDLE
-                && mCurrentState != STATE_PREPARING;
-    }
-
-    private boolean needToStart() {
-        // TODO (b/77158231)
-        // return (mMediaPlayer != null || mRoutePlayer != null)
-        return (mMediaPlayer != null)
-                && isAudioGranted()
-                && isWaitingPlayback();
-    }
-
-    private boolean isWaitingPlayback() {
-        return mCurrentState != STATE_PLAYING && mTargetState == STATE_PLAYING;
-    }
-
-    private boolean isAudioGranted() {
-        return mAudioFocused || mAudioFocusType == AudioManager.AUDIOFOCUS_NONE;
-    }
-
-    AudioManager.OnAudioFocusChangeListener mAudioFocusListener =
-            new AudioManager.OnAudioFocusChangeListener() {
-        @Override
-        public void onAudioFocusChange(int focusChange) {
-            switch (focusChange) {
-                case AudioManager.AUDIOFOCUS_GAIN:
-                    mAudioFocused = true;
-                    if (needToStart()) {
-                        mMediaController.getTransportControls().play();
-                    }
-                    break;
-                case AudioManager.AUDIOFOCUS_LOSS:
-                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
-                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
-                    // There is no way to distinguish pause() by transient
-                    // audio focus loss and by other explicit actions.
-                    // TODO: If we can distinguish those cases, change the code to resume when it
-                    // gains audio focus again for AUDIOFOCUS_LOSS_TRANSIENT and
-                    // AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
-                    mAudioFocused = false;
-                    if (isInPlaybackState() && mMediaPlayer.isPlaying()) {
-                        mMediaController.getTransportControls().pause();
-                    } else {
-                        mTargetState = STATE_PAUSED;
-                    }
-            }
-        }
-    };
-
-    @SuppressWarnings("deprecation")
-    private void requestAudioFocus(int focusType) {
-        int result;
-        if (android.os.Build.VERSION.SDK_INT >= 26) {
-            AudioFocusRequest focusRequest;
-            focusRequest = new AudioFocusRequest.Builder(focusType)
-                    .setAudioAttributes(mAudioAttributes)
-                    .setOnAudioFocusChangeListener(mAudioFocusListener)
-                    .build();
-            result = mAudioManager.requestAudioFocus(focusRequest);
-        } else {
-            result = mAudioManager.requestAudioFocus(mAudioFocusListener,
-                    AudioManager.STREAM_MUSIC,
-                    focusType);
-        }
-        if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
-            mAudioFocused = false;
-        } else if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
-            mAudioFocused = true;
-        } else if (result == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
-            mAudioFocused = false;
-        }
-    }
-
-    // Creates a MediaPlayer instance and prepare playback.
-    private void openVideo(Uri uri, Map<String, String> headers) {
-        resetPlayer();
-        if (isRemotePlayback()) {
-            // TODO (b/77158231)
-            // mRoutePlayer.openVideo(dsd);
-            return;
-        }
-
-        try {
-            Log.d(TAG, "openVideo(): creating new MediaPlayer instance.");
-            mMediaPlayer = new MediaPlayer();
-            mSurfaceView.setMediaPlayer(mMediaPlayer);
-            mTextureView.setMediaPlayer(mMediaPlayer);
-            mCurrentView.assignSurfaceToMediaPlayer(mMediaPlayer);
-
-            final Context context = getContext();
-            // TODO: Add timely firing logic for more accurate sync between CC and video frame
-            // mSubtitleController = new SubtitleController(context);
-            // mSubtitleController.registerRenderer(new ClosedCaptionRenderer(context));
-            // mSubtitleController.setAnchor((SubtitleController.Anchor) mSubtitleView);
-
-            mMediaPlayer.setOnPreparedListener(mPreparedListener);
-            mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
-            mMediaPlayer.setOnCompletionListener(mCompletionListener);
-            mMediaPlayer.setOnSeekCompleteListener(mSeekCompleteListener);
-            mMediaPlayer.setOnErrorListener(mErrorListener);
-            mMediaPlayer.setOnInfoListener(mInfoListener);
-            mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
-
-            mCurrentBufferPercentage = -1;
-            mMediaPlayer.setDataSource(getContext(), uri, headers);
-            mMediaPlayer.setAudioAttributes(mAudioAttributes);
-            // mMediaPlayer.setOnSubtitleDataListener(mSubtitleListener);
-            // we don't set the target state here either, but preserve the
-            // target state that was there before.
-            mCurrentState = STATE_PREPARING;
-            mMediaPlayer.prepareAsync();
-
-            // Save file name as title since the file may not have a title Metadata.
-            mTitle = uri.getPath();
-            String scheme = uri.getScheme();
-            if (scheme != null && scheme.equals("file")) {
-                mTitle = uri.getLastPathSegment();
-            }
-            mRetriever = new MediaMetadataRetriever();
-            mRetriever.setDataSource(getContext(), uri);
-
-            if (DEBUG) {
-                Log.d(TAG, "openVideo(). mCurrentState=" + mCurrentState
-                        + ", mTargetState=" + mTargetState);
-            }
-        } catch (IOException | IllegalArgumentException ex) {
-            Log.w(TAG, "Unable to open content: " + uri, ex);
-            mCurrentState = STATE_ERROR;
-            mTargetState = STATE_ERROR;
-            mErrorListener.onError(mMediaPlayer,
-                    MediaPlayer.MEDIA_ERROR_UNKNOWN, MediaPlayer.MEDIA_ERROR_IO);
-        }
-    }
-
-    /*
-     * Reset the media player in any state
-     */
-    @SuppressWarnings("deprecation")
-    private void resetPlayer() {
-        if (mMediaPlayer != null) {
-            mMediaPlayer.reset();
-            mMediaPlayer.release();
-            mMediaPlayer = null;
-            mTextureView.setMediaPlayer(null);
-            mSurfaceView.setMediaPlayer(null);
-            mCurrentState = STATE_IDLE;
-            mTargetState = STATE_IDLE;
-            if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
-                mAudioManager.abandonAudioFocus(null);
-            }
-        }
-        mVideoWidth = 0;
-        mVideoHeight = 0;
-    }
-
-    private void updatePlaybackState() {
-        if (mStateBuilder == null) {
-            /*
-            // Get the capabilities of the player for this stream
-            mMetadata = mMediaPlayer.getMetadata(MediaPlayer.METADATA_ALL,
-                    MediaPlayer.BYPASS_METADATA_FILTER);
-
-            // Add Play action as default
-            long playbackActions = PlaybackStateCompat.ACTION_PLAY;
-            if (mMetadata != null) {
-                if (!mMetadata.has(Metadata.PAUSE_AVAILABLE)
-                        || mMetadata.getBoolean(Metadata.PAUSE_AVAILABLE)) {
-                    playbackActions |= PlaybackStateCompat.ACTION_PAUSE;
-                }
-                if (!mMetadata.has(Metadata.SEEK_BACKWARD_AVAILABLE)
-                        || mMetadata.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE)) {
-                    playbackActions |= PlaybackStateCompat.ACTION_REWIND;
-                }
-                if (!mMetadata.has(Metadata.SEEK_FORWARD_AVAILABLE)
-                        || mMetadata.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE)) {
-                    playbackActions |= PlaybackStateCompat.ACTION_FAST_FORWARD;
-                }
-                if (!mMetadata.has(Metadata.SEEK_AVAILABLE)
-                        || mMetadata.getBoolean(Metadata.SEEK_AVAILABLE)) {
-                    playbackActions |= PlaybackStateCompat.ACTION_SEEK_TO;
-                }
-            } else {
-                playbackActions |= (PlaybackStateCompat.ACTION_PAUSE
-                        | PlaybackStateCompat.ACTION_REWIND
-                        | PlaybackStateCompat.ACTION_FAST_FORWARD
-                        | PlaybackStateCompat.ACTION_SEEK_TO);
-            }
-            */
-            // TODO determine the actionable list based the metadata info.
-            long playbackActions = PlaybackStateCompat.ACTION_PLAY
-                    | PlaybackStateCompat.ACTION_PAUSE
-                    | PlaybackStateCompat.ACTION_REWIND | PlaybackStateCompat.ACTION_FAST_FORWARD
-                    | PlaybackStateCompat.ACTION_SEEK_TO;
-            mStateBuilder = new PlaybackStateCompat.Builder();
-            mStateBuilder.setActions(playbackActions);
-
-            if (mCustomActionList != null) {
-                for (PlaybackStateCompat.CustomAction action : mCustomActionList) {
-                    mStateBuilder.addCustomAction(action);
-                }
-            }
-        }
-        mStateBuilder.setState(getCorrespondingPlaybackState(),
-                mMediaPlayer.getCurrentPosition(), mSpeed);
-        if (mCurrentState != STATE_ERROR
-                && mCurrentState != STATE_IDLE
-                && mCurrentState != STATE_PREPARING) {
-            // TODO: this should be replaced with MediaPlayer2.getBufferedPosition() once it is
-            // implemented.
-            if (mCurrentBufferPercentage == -1) {
-                mStateBuilder.setBufferedPosition(-1);
-            } else {
-                mStateBuilder.setBufferedPosition(
-                        (long) (mCurrentBufferPercentage / 100.0 * mMediaPlayer.getDuration()));
-            }
-        }
-
-        // Set PlaybackState for MediaSession
-        if (mMediaSession != null) {
-            PlaybackStateCompat state = mStateBuilder.build();
-            mMediaSession.setPlaybackState(state);
-        }
-    }
-
-    private int getCorrespondingPlaybackState() {
-        switch (mCurrentState) {
-            case STATE_ERROR:
-                return PlaybackStateCompat.STATE_ERROR;
-            case STATE_IDLE:
-                return PlaybackStateCompat.STATE_NONE;
-            case STATE_PREPARING:
-                return PlaybackStateCompat.STATE_CONNECTING;
-            case STATE_PREPARED:
-                return PlaybackStateCompat.STATE_PAUSED;
-            case STATE_PLAYING:
-                return PlaybackStateCompat.STATE_PLAYING;
-            case STATE_PAUSED:
-                return PlaybackStateCompat.STATE_PAUSED;
-            case STATE_PLAYBACK_COMPLETED:
-                return PlaybackStateCompat.STATE_STOPPED;
-            default:
-                return -1;
-        }
-    }
-
-    private final Runnable mFadeOut = new Runnable() {
-        @Override
-        public void run() {
-            if (mCurrentState == STATE_PLAYING) {
-                mMediaControlView.setVisibility(View.GONE);
-            }
-        }
-    };
-
-    private void showController() {
-        // TODO: Decide what to show when the state is not in playback state
-        if (mMediaControlView == null || !isInPlaybackState()
-                || (mIsMusicMediaType && mSizeType == SIZE_TYPE_FULL)) {
-            return;
-        }
-        mMediaControlView.removeCallbacks(mFadeOut);
-        mMediaControlView.setVisibility(View.VISIBLE);
-        if (mShowControllerIntervalMs != 0
-                && !mAccessibilityManager.isTouchExplorationEnabled()) {
-            mMediaControlView.postDelayed(mFadeOut, mShowControllerIntervalMs);
-        }
-    }
-
-    private void toggleMediaControlViewVisibility() {
-        if (mMediaControlView.getVisibility() == View.VISIBLE) {
-            mMediaControlView.removeCallbacks(mFadeOut);
-            mMediaControlView.setVisibility(View.GONE);
-        } else {
-            showController();
-        }
-    }
-
-    private void applySpeed() {
-        if (android.os.Build.VERSION.SDK_INT < 23) {
-            // TODO: MediaPlayer2 will cover this, or implement with SoundPool.
-            return;
-        }
-        PlaybackParams params = mMediaPlayer.getPlaybackParams().allowDefaults();
-        if (mSpeed != params.getSpeed()) {
-            try {
-                params.setSpeed(mSpeed);
-                mMediaPlayer.setPlaybackParams(params);
-                mFallbackSpeed = mSpeed;
-            } catch (IllegalArgumentException e) {
-                Log.e(TAG, "PlaybackParams has unsupported value: " + e);
-                // TODO: should revise this part after integrating with MP2.
-                // If mSpeed had an illegal value for speed rate, system will determine best
-                // handling (see PlaybackParams.AUDIO_FALLBACK_MODE_DEFAULT).
-                // Note: The pre-MP2 returns 0.0f when it is paused. In this case, VideoView2 will
-                // use mFallbackSpeed instead.
-                float fallbackSpeed = mMediaPlayer.getPlaybackParams().allowDefaults().getSpeed();
-                if (fallbackSpeed > 0.0f) {
-                    mFallbackSpeed = fallbackSpeed;
-                }
-                mSpeed = mFallbackSpeed;
-            }
-        }
-    }
-
-    private boolean isRemotePlayback() {
-        if (mMediaController == null) {
-            return false;
-        }
-        PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo();
-        return playbackInfo != null
-                && playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
-    }
-
-    private void selectOrDeselectSubtitle(boolean select) {
-        if (!isInPlaybackState()) {
-            return;
-        }
-    /*
-        if (select) {
-            if (mSubtitleTrackIndices.size() > 0) {
-                // TODO: make this selection dynamic
-                mSelectedSubtitleTrackIndex = mSubtitleTrackIndices.get(0).first;
-                mSubtitleController.selectTrack(mSubtitleTrackIndices.get(0).second);
-                mMediaPlayer.selectTrack(mSelectedSubtitleTrackIndex);
-                mSubtitleView.setVisibility(View.VISIBLE);
-            }
-        } else {
-            if (mSelectedSubtitleTrackIndex != INVALID_TRACK_INDEX) {
-                mMediaPlayer.deselectTrack(mSelectedSubtitleTrackIndex);
-                mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
-                mSubtitleView.setVisibility(View.GONE);
-            }
-        }
-    */
-    }
-
-    private void extractTracks() {
-        MediaPlayer.TrackInfo[] trackInfos = mMediaPlayer.getTrackInfo();
-        mVideoTrackIndices = new ArrayList<>();
-        mAudioTrackIndices = new ArrayList<>();
-        /*
-        mSubtitleTrackIndices = new ArrayList<>();
-        mSubtitleController.reset();
-        */
-        for (int i = 0; i < trackInfos.length; ++i) {
-            int trackType = trackInfos[i].getTrackType();
-            if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO) {
-                mVideoTrackIndices.add(i);
-            } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) {
-                mAudioTrackIndices.add(i);
-                /*
-            } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE
-                    || trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT) {
-                SubtitleTrack track = mSubtitleController.addTrack(trackInfos[i].getFormat());
-                if (track != null) {
-                    mSubtitleTrackIndices.add(new Pair<>(i, track));
-                }
-                */
-            }
-        }
-        // Select first tracks as default
-        if (mVideoTrackIndices.size() > 0) {
-            mSelectedVideoTrackIndex = 0;
-        }
-        if (mAudioTrackIndices.size() > 0) {
-            mSelectedAudioTrackIndex = 0;
-        }
-        if (mVideoTrackIndices.size() == 0 && mAudioTrackIndices.size() > 0) {
-            mIsMusicMediaType = true;
-        }
-
-        Bundle data = new Bundle();
-        data.putInt(MediaControlView2.KEY_VIDEO_TRACK_COUNT, mVideoTrackIndices.size());
-        data.putInt(MediaControlView2.KEY_AUDIO_TRACK_COUNT, mAudioTrackIndices.size());
-        /*
-        data.putInt(MediaControlView2.KEY_SUBTITLE_TRACK_COUNT, mSubtitleTrackIndices.size());
-        if (mSubtitleTrackIndices.size() > 0) {
-            selectOrDeselectSubtitle(mSubtitleEnabled);
-        }
-        */
-        mMediaSession.sendSessionEvent(MediaControlView2.EVENT_UPDATE_TRACK_STATUS, data);
-    }
-
-    private void extractMetadata() {
-        // Get and set duration and title values as MediaMetadata for MediaControlView2
-        MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
-        String title = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
-        if (title != null) {
-            mTitle = title;
-        }
-        builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
-        builder.putLong(
-                MediaMetadataCompat.METADATA_KEY_DURATION, mMediaPlayer.getDuration());
-
-        if (mMediaSession != null) {
-            mMediaSession.setMetadata(builder.build());
-        }
-    }
-
-    @SuppressWarnings("deprecation")
-    private void extractAudioMetadata() {
-        if (!mIsMusicMediaType) {
-            return;
-        }
-
-        mResources = getResources();
-        mManager = (WindowManager) getContext().getApplicationContext()
-                .getSystemService(Context.WINDOW_SERVICE);
-
-        byte[] album = mRetriever.getEmbeddedPicture();
-        if (album != null) {
-            Bitmap bitmap = BitmapFactory.decodeByteArray(album, 0, album.length);
-            mMusicAlbumDrawable = new BitmapDrawable(bitmap);
-
-            // TODO: replace with visualizer
-            Palette.Builder builder = Palette.from(bitmap);
-            builder.generate(new Palette.PaletteAsyncListener() {
-                @Override
-                public void onGenerated(Palette palette) {
-                    // TODO: add dominant color for default album image.
-                    mDominantColor = palette.getDominantColor(0);
-                    if (mMusicView != null) {
-                        mMusicView.setBackgroundColor(mDominantColor);
-                    }
-                }
-            });
-        } else {
-            mMusicAlbumDrawable = mResources.getDrawable(R.drawable.ic_default_album_image);
-        }
-
-        String title = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
-        if (title != null) {
-            mMusicTitleText = title;
-        } else {
-            mMusicTitleText = mResources.getString(R.string.mcv2_music_title_unknown_text);
-        }
-
-        String artist = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
-        if (artist != null) {
-            mMusicArtistText = artist;
-        } else {
-            mMusicArtistText = mResources.getString(R.string.mcv2_music_artist_unknown_text);
-        }
-
-        // Send title and artist string to MediaControlView2
-        MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
-        builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mMusicTitleText);
-        builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, mMusicArtistText);
-        mMediaSession.setMetadata(builder.build());
-
-        // Display Embedded mode as default
-        removeView(mSurfaceView);
-        removeView(mTextureView);
-        inflateMusicView(R.layout.embedded_music);
-    }
-
-    private int retrieveOrientation() {
-        DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
-        int width = dm.widthPixels;
-        int height = dm.heightPixels;
-
-        return (height > width)
-                ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
-                : ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
-    }
-
-    private void inflateMusicView(int layoutId) {
-        removeView(mMusicView);
-
-        LayoutInflater inflater = (LayoutInflater) getContext()
-                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        View v = inflater.inflate(layoutId, null);
-        v.setBackgroundColor(mDominantColor);
-
-        ImageView albumView = v.findViewById(R.id.album);
-        if (albumView != null) {
-            albumView.setImageDrawable(mMusicAlbumDrawable);
-        }
-
-        TextView titleView = v.findViewById(R.id.title);
-        if (titleView != null) {
-            titleView.setText(mMusicTitleText);
-        }
-
-        TextView artistView = v.findViewById(R.id.artist);
-        if (artistView != null) {
-            artistView.setText(mMusicArtistText);
-        }
-
-        mMusicView = v;
-        addView(mMusicView, 0);
-    }
-
-    /*
-    OnSubtitleDataListener mSubtitleListener =
-            new OnSubtitleDataListener() {
-                @Override
-                public void onSubtitleData(MediaPlayer mp, SubtitleData data) {
-                    if (DEBUG) {
-                        Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex()
-                                + ", getCurrentPosition: " + mp.getCurrentPosition()
-                                + ", getStartTimeUs(): " + data.getStartTimeUs()
-                                + ", diff: "
-                                + (data.getStartTimeUs() / 1000 - mp.getCurrentPosition())
-                                + "ms, getDurationUs(): " + data.getDurationUs());
-
-                    }
-                    final int index = data.getTrackIndex();
-                    if (index != mSelectedSubtitleTrackIndex) {
-                        Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex()
-                                + ", selected track index: " + mSelectedSubtitleTrackIndex);
-                        return;
-                    }
-                    for (Pair<Integer, SubtitleTrack> p : mSubtitleTrackIndices) {
-                        if (p.first == index) {
-                            SubtitleTrack track = p.second;
-                            track.onData(data);
-                        }
-                    }
-                }
-            };
-            */
-
-    MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener =
-            new MediaPlayer.OnVideoSizeChangedListener() {
-                @Override
-                public void onVideoSizeChanged(
-                        MediaPlayer mp, int width, int height) {
-                    if (DEBUG) {
-                        Log.d(TAG, "onVideoSizeChanged(): size: " + width + "/" + height);
-                    }
-                    mVideoWidth = mp.getVideoWidth();
-                    mVideoHeight = mp.getVideoHeight();
-                    if (DEBUG) {
-                        Log.d(TAG, "onVideoSizeChanged(): mVideoSize:" + mVideoWidth + "/"
-                                + mVideoHeight);
-                    }
-                    if (mVideoWidth != 0 && mVideoHeight != 0) {
-                        requestLayout();
-                    }
-                }
-            };
-    MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() {
-        @Override
-        public void onPrepared(MediaPlayer mp) {
-            if (DEBUG) {
-                Log.d(TAG, "OnPreparedListener(). mCurrentState=" + mCurrentState
-                        + ", mTargetState=" + mTargetState);
-            }
-            mCurrentState = STATE_PREPARED;
-            // Create and set playback state for MediaControlView2
-            updatePlaybackState();
-
-            // TODO: change this to send TrackInfos to MediaControlView2
-            // TODO: create MediaSession when initializing VideoView2
-            if (mMediaSession != null) {
-                extractTracks();
-            }
-
-            if (mMediaControlView != null) {
-                mMediaControlView.setEnabled(true);
-            }
-            int videoWidth = mp.getVideoWidth();
-            int videoHeight = mp.getVideoHeight();
-
-            // mSeekWhenPrepared may be changed after seekTo() call
-            long seekToPosition = mSeekWhenPrepared;
-            if (seekToPosition != 0) {
-                mMediaController.getTransportControls().seekTo(seekToPosition);
-            }
-
-            if (videoWidth != 0 && videoHeight != 0) {
-                if (videoWidth != mVideoWidth || videoHeight != mVideoHeight) {
-                    if (DEBUG) {
-                        Log.i(TAG, "OnPreparedListener() : ");
-                        Log.i(TAG, " video size: " + videoWidth + "/" + videoHeight);
-                        Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/"
-                                + getMeasuredHeight());
-                        Log.i(TAG, " viewSize: " + getWidth() + "/" + getHeight());
-                    }
-                    mVideoWidth = videoWidth;
-                    mVideoHeight = videoHeight;
-                    requestLayout();
-                }
-
-                if (needToStart()) {
-                    mMediaController.getTransportControls().play();
-                }
-            } else {
-                // We don't know the video size yet, but should start anyway.
-                // The video size might be reported to us later.
-                if (needToStart()) {
-                    mMediaController.getTransportControls().play();
-                }
-            }
-            // Get and set duration and title values as MediaMetadata for MediaControlView2
-            MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
-
-            // TODO: Get title via other public APIs.
-            /*
-            if (mMetadata != null && mMetadata.has(Metadata.TITLE)) {
-                mTitle = mMetadata.getString(Metadata.TITLE);
-            }
-            */
-            builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
-            builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, mMediaPlayer.getDuration());
-
-            if (mMediaSession != null) {
-                mMediaSession.setMetadata(builder.build());
-
-                // TODO: merge this code with the above code when integrating with
-                // MediaSession2.
-                if (mNeedUpdateMediaType) {
-                    mMediaSession.sendSessionEvent(
-                            MediaControlView2.EVENT_UPDATE_MEDIA_TYPE_STATUS, mMediaTypeData);
-                    mNeedUpdateMediaType = false;
-                }
-            }
-        }
-    };
-
-    MediaPlayer.OnSeekCompleteListener mSeekCompleteListener =
-            new MediaPlayer.OnSeekCompleteListener() {
-                @Override
-                public void onSeekComplete(MediaPlayer mp) {
-                    updatePlaybackState();
-                }
-    };
-
-    MediaPlayer.OnCompletionListener mCompletionListener = new MediaPlayer.OnCompletionListener() {
-        @Override
-        @SuppressWarnings("deprecation")
-        public void onCompletion(MediaPlayer mp) {
-            mCurrentState = STATE_PLAYBACK_COMPLETED;
-            mTargetState = STATE_PLAYBACK_COMPLETED;
-            updatePlaybackState();
-            if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
-                mAudioManager.abandonAudioFocus(null);
-            }
-        }
-    };
-
-    MediaPlayer.OnInfoListener mInfoListener = new MediaPlayer.OnInfoListener() {
-        @Override
-        public boolean onInfo(MediaPlayer mp, int what, int extra) {
-            if (what == MediaPlayer.MEDIA_INFO_METADATA_UPDATE) {
-                extractTracks();
-            }
-            return true;
-        }
-    };
-
-    MediaPlayer.OnErrorListener mErrorListener = new MediaPlayer.OnErrorListener() {
-        @Override
-        public boolean onError(MediaPlayer mp, int frameworkErr, int implErr) {
-            if (DEBUG) {
-                Log.d(TAG, "Error: " + frameworkErr + "," + implErr);
-            }
-            mCurrentState = STATE_ERROR;
-            mTargetState = STATE_ERROR;
-            updatePlaybackState();
-
-            if (mMediaControlView != null) {
-                mMediaControlView.setVisibility(View.GONE);
-            }
-            return true;
-        }
-    };
-
-    MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener =
-            new MediaPlayer.OnBufferingUpdateListener() {
-        @Override
-        public void onBufferingUpdate(MediaPlayer mp, int percent) {
-            mCurrentBufferPercentage = percent;
-            updatePlaybackState();
-        }
-    };
-
-    private class MediaSessionCallback extends MediaSessionCompat.Callback {
-        @Override
-        public void onCommand(String command, Bundle args, ResultReceiver receiver) {
-            if (isRemotePlayback()) {
-                // TODO (b/77158231)
-                // mRoutePlayer.onCommand(command, args, receiver);
-            } else {
-                switch (command) {
-                    case MediaControlView2.COMMAND_SHOW_SUBTITLE:
-                        /*
-                        int subtitleIndex = args.getInt(
-                                MediaControlView2.KEY_SELECTED_SUBTITLE_INDEX,
-                                INVALID_TRACK_INDEX);
-                        if (subtitleIndex != INVALID_TRACK_INDEX) {
-                            int subtitleTrackIndex = mSubtitleTrackIndices.get(subtitleIndex).first;
-                            if (subtitleTrackIndex != mSelectedSubtitleTrackIndex) {
-                                mSelectedSubtitleTrackIndex = subtitleTrackIndex;
-                                setSubtitleEnabled(true);
-                            }
-                        }
-                        */
-                        break;
-                    case MediaControlView2.COMMAND_HIDE_SUBTITLE:
-                        setSubtitleEnabled(false);
-                        break;
-                    case MediaControlView2.COMMAND_SET_FULLSCREEN:
-                        if (mFullScreenRequestListener != null) {
-                            mFullScreenRequestListener.onFullScreenRequest(
-                                    VideoView2.this,
-                                    args.getBoolean(MediaControlView2.ARGUMENT_KEY_FULLSCREEN));
-                        }
-                        break;
-                    case MediaControlView2.COMMAND_SELECT_AUDIO_TRACK:
-                        int audioIndex = args.getInt(MediaControlView2.KEY_SELECTED_AUDIO_INDEX,
-                                INVALID_TRACK_INDEX);
-                        if (audioIndex != INVALID_TRACK_INDEX) {
-                            int audioTrackIndex = mAudioTrackIndices.get(audioIndex);
-                            if (audioTrackIndex != mSelectedAudioTrackIndex) {
-                                mSelectedAudioTrackIndex = audioTrackIndex;
-                                mMediaPlayer.selectTrack(mSelectedAudioTrackIndex);
-                            }
-                        }
-                        break;
-                    case MediaControlView2.COMMAND_SET_PLAYBACK_SPEED:
-                        float speed = args.getFloat(
-                                MediaControlView2.KEY_PLAYBACK_SPEED, INVALID_SPEED);
-                        if (speed != INVALID_SPEED && speed != mSpeed) {
-                            setSpeed(speed);
-                            mSpeed = speed;
-                        }
-                        break;
-                    case MediaControlView2.COMMAND_MUTE:
-                        mVolumeLevel = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
-                        mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, 0);
-                        break;
-                    case MediaControlView2.COMMAND_UNMUTE:
-                        mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeLevel, 0);
-                        break;
-                }
-            }
-            showController();
-        }
-
-        @Override
-        public void onCustomAction(final String action, final Bundle extras) {
-            mCustomActionListenerRecord.first.execute(new Runnable() {
-                @Override
-                public void run() {
-                    mCustomActionListenerRecord.second.onCustomAction(action, extras);
-                }
-            });
-            showController();
-        }
-
-        @Override
-        public void onPlay() {
-            if (!isAudioGranted()) {
-                requestAudioFocus(mAudioFocusType);
-            }
-
-            if ((isInPlaybackState() && mCurrentView.hasAvailableSurface()) || mIsMusicMediaType) {
-                if (isRemotePlayback()) {
-                    // TODO (b/77158231)
-                    // mRoutePlayer.onPlay();
-                } else {
-                    applySpeed();
-                    mMediaPlayer.start();
-                    mCurrentState = STATE_PLAYING;
-                    updatePlaybackState();
-                }
-                mCurrentState = STATE_PLAYING;
-            }
-            mTargetState = STATE_PLAYING;
-            if (DEBUG) {
-                Log.d(TAG, "onPlay(). mCurrentState=" + mCurrentState
-                        + ", mTargetState=" + mTargetState);
-            }
-            showController();
-        }
-
-        @Override
-        public void onPause() {
-            if (isInPlaybackState()) {
-                if (isRemotePlayback()) {
-                    // TODO (b/77158231)
-                    // mRoutePlayer.onPause();
-                    mCurrentState = STATE_PAUSED;
-                } else if (mMediaPlayer.isPlaying()) {
-                    mMediaPlayer.pause();
-                    mCurrentState = STATE_PAUSED;
-                    updatePlaybackState();
-                }
-            }
-            mTargetState = STATE_PAUSED;
-            if (DEBUG) {
-                Log.d(TAG, "onPause(). mCurrentState=" + mCurrentState
-                        + ", mTargetState=" + mTargetState);
-            }
-            showController();
-        }
-
-        @Override
-        public void onSeekTo(long pos) {
-            if (isInPlaybackState()) {
-                if (isRemotePlayback()) {
-                    // TODO (b/77158231)
-                    // mRoutePlayer.onSeekTo(pos);
-                } else {
-                    // TODO Refactor VideoView2 with FooImplBase and FooImplApiXX.
-                    if (android.os.Build.VERSION.SDK_INT < 26) {
-                        mMediaPlayer.seekTo((int) pos);
-                    } else {
-                        mMediaPlayer.seekTo(pos, MediaPlayer.SEEK_PREVIOUS_SYNC);
-                    }
-                    mSeekWhenPrepared = 0;
-                }
-            } else {
-                mSeekWhenPrepared = pos;
-            }
-            showController();
-        }
-
-        @Override
-        public void onStop() {
-            if (isRemotePlayback()) {
-                // TODO (b/77158231)
-                // mRoutePlayer.onStop();
-            } else {
-                resetPlayer();
-            }
-            showController();
-        }
-    }
 }
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoView2Impl.java b/media-widget/src/main/java/androidx/media/widget/VideoView2Impl.java
new file mode 100644
index 0000000..7632490
--- /dev/null
+++ b/media-widget/src/main/java/androidx/media/widget/VideoView2Impl.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.widget;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.media.AudioAttributesCompat;
+import androidx.media.DataSourceDesc;
+import androidx.media.MediaItem2;
+import androidx.media.MediaMetadata2;
+import androidx.media.SessionToken2;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Interface for impl classes.
+ */
+interface VideoView2Impl {
+    void initialize(
+            VideoView2 instance, Context context,
+            @Nullable AttributeSet attrs, int defStyleAttr);
+
+    /**
+     * Sets MediaControlView2 instance. It will replace the previously assigned MediaControlView2
+     * instance if any.
+     *
+     * @param mediaControlView a media control view2 instance.
+     * @param intervalMs a time interval in milliseconds until VideoView2 hides MediaControlView2.
+     */
+    void setMediaControlView2(MediaControlView2 mediaControlView, long intervalMs);
+
+    /**
+     * Returns MediaControlView2 instance which is currently attached to VideoView2 by default or by
+     * {@link #setMediaControlView2} method.
+     */
+    MediaControlView2 getMediaControlView2();
+
+    /**
+     * Sets MediaMetadata2 instance. It will replace the previously assigned MediaMetadata2 instance
+     * if any.
+     *
+     * @param metadata a MediaMetadata2 instance.
+     */
+    void setMediaMetadata(MediaMetadata2 metadata);
+
+    /**
+     * Returns MediaMetadata2 instance which is retrieved from MediaPlayer inside VideoView2 by
+     * default or by {@link #setMediaMetadata} method.
+     */
+    MediaMetadata2 getMediaMetadata();
+
+    /**
+     * Returns MediaController instance which is connected with MediaSession that VideoView2 is
+     * using. This method should be called when VideoView2 is attached to window, or it throws
+     * IllegalStateException, since internal MediaSession instance is not available until
+     * this view is attached to window. Please check {@link View#isAttachedToWindow}
+     * before calling this method.
+     *
+     * @throws IllegalStateException if interal MediaSession is not created yet.
+     */
+    MediaControllerCompat getMediaController();
+
+    /**
+     * Returns {@link SessionToken2} so that developers create their own
+     * {@link androidx.media.MediaController2} instance. This method should be called when
+     * VideoView2 is attached to window, or it throws IllegalStateException.
+     *
+     * @throws IllegalStateException if interal MediaSession is not created yet.
+     */
+    SessionToken2 getMediaSessionToken();
+
+    /**
+     * Shows or hides closed caption or subtitles if there is any.
+     * The first subtitle track will be chosen if there multiple subtitle tracks exist.
+     * Default behavior of VideoView2 is not showing subtitle.
+     * @param enable shows closed caption or subtitles if this value is true, or hides.
+     */
+    void setSubtitleEnabled(boolean enable);
+
+    /**
+     * Returns true if showing subtitle feature is enabled or returns false.
+     * Although there is no subtitle track or closed caption, it can return true, if the feature
+     * has been enabled by {@link #setSubtitleEnabled}.
+     */
+    boolean isSubtitleEnabled();
+
+    /**
+     * Sets playback speed.
+     *
+     * It is expressed as a multiplicative factor, where normal speed is 1.0f. If it is less than
+     * or equal to zero, it will be just ignored and nothing will be changed. If it exceeds the
+     * maximum speed that internal engine supports, system will determine best handling or it will
+     * be reset to the normal speed 1.0f.
+     * @param speed the playback speed. It should be positive.
+     */
+    void setSpeed(float speed);
+
+    /**
+     * Returns playback speed.
+     *
+     * It returns the same value that has been set by {@link #setSpeed}, if it was available value.
+     * If {@link #setSpeed} has not been called before, then the normal speed 1.0f will be returned.
+     */
+    float getSpeed();
+
+    /**
+     * Sets which type of audio focus will be requested during the playback, or configures playback
+     * to not request audio focus. Valid values for focus requests are
+     * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT},
+     * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and
+     * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use
+     * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be
+     * requested when playback starts. You can for instance use this when playing a silent animation
+     * through this class, and you don't want to affect other audio applications playing in the
+     * background.
+     *
+     * @param focusGain the type of audio focus gain that will be requested, or
+     *                  {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during
+     *                  playback.
+     */
+    void setAudioFocusRequest(int focusGain);
+
+    /**
+     * Sets the {@link AudioAttributesCompat} to be used during the playback of the video.
+     *
+     * @param attributes non-null <code>AudioAttributesCompat</code>.
+     */
+    void setAudioAttributes(@NonNull AudioAttributesCompat attributes);
+
+    /**
+     * Sets video path.
+     *
+     * @param path the path of the video.
+     */
+    void setVideoPath(String path);
+
+    /**
+     * Sets video URI.
+     *
+     * @param uri the URI of the video.
+     */
+    void setVideoUri(Uri uri);
+
+    /**
+     * Sets video URI using specific headers.
+     *
+     * @param uri     the URI of the video.
+     * @param headers the headers for the URI request.
+     *                Note that the cross domain redirection is allowed by default, but that can be
+     *                changed with key/value pairs through the headers parameter with
+     *                "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
+     *                to disallow or allow cross domain redirection.
+     */
+    void setVideoUri(Uri uri, @Nullable Map<String, String> headers);
+
+    /**
+     * Sets {@link MediaItem2} object to render using VideoView2. Alternative way to set media
+     * object to VideoView2 is {@link #setDataSource}.
+     * @param mediaItem the MediaItem2 to play
+     * @see #setDataSource
+     */
+    void setMediaItem(@NonNull MediaItem2 mediaItem);
+
+    /**
+     * Sets {@link DataSourceDesc} object to render using VideoView2.
+     * @param dataSource the {@link DataSourceDesc} object to play.
+     * @see #setMediaItem
+     */
+    void setDataSource(@NonNull DataSourceDesc dataSource);
+
+    /**
+     * Selects which view will be used to render video between SurfacView and TextureView.
+     *
+     * @param viewType the view type to render video
+     * <ul>
+     * <li>{@link #VideoView2.VIEW_TYPE_SURFACEVIEW}
+     * <li>{@link #VideoView2.VIEW_TYPE_TEXTUREVIEW}
+     * </ul>
+     */
+    void setViewType(@VideoView2.ViewType int viewType);
+
+    /**
+     * Returns view type.
+     *
+     * @return view type. See {@see setViewType}.
+     */
+    @VideoView2.ViewType
+    int getViewType();
+
+    /**
+     * Sets custom actions which will be shown as custom buttons in {@link MediaControlView2}.
+     *
+     * @param actionList A list of {@link PlaybackStateCompat.CustomAction}. The return value of
+     *                   {@link PlaybackStateCompat.CustomAction#getIcon()} will be used to draw
+     *                   buttons in {@link MediaControlView2}.
+     * @param executor executor to run callbacks on.
+     * @param listener A listener to be called when a custom button is clicked.
+     */
+    void setCustomActions(List<PlaybackStateCompat.CustomAction> actionList,
+            Executor executor, VideoView2.OnCustomActionListener listener);
+
+    /**
+     * Registers a callback to be invoked when a view type change is done.
+     * {@see #setViewType(int)}
+     * @param l The callback that will be run
+     */
+    void setOnViewTypeChangedListener(VideoView2.OnViewTypeChangedListener l);
+
+    void onAttachedToWindowImpl();
+
+    void onDetachedFromWindowImpl();
+
+    void onTouchEventImpl(MotionEvent ev);
+
+    void onTrackballEventImpl(MotionEvent ev);
+
+    void onMeasureImpl(int widthMeasureSpec, int heightMeasureSpec);
+}
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java b/media-widget/src/main/java/androidx/media/widget/impl/VideoSurfaceView.java
similarity index 76%
copy from media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java
copy to media-widget/src/main/java/androidx/media/widget/impl/VideoSurfaceView.java
index d417bd2..5c140c8 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java
+++ b/media-widget/src/main/java/androidx/media/widget/impl/VideoSurfaceView.java
@@ -20,59 +20,44 @@
 
 import android.content.Context;
 import android.graphics.Rect;
-import android.media.MediaPlayer;
-import android.util.AttributeSet;
 import android.util.Log;
+import android.view.Surface;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 import android.view.View;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.media.MediaPlayer2;
 
 @RequiresApi(21)
-class VideoSurfaceView extends SurfaceView implements VideoViewInterface, SurfaceHolder.Callback {
+class VideoSurfaceView extends SurfaceView
+        implements VideoViewInterface, SurfaceHolder.Callback {
     private static final String TAG = "VideoSurfaceView";
-    private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
-    private SurfaceHolder mSurfaceHolder = null;
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    private Surface mSurface = null;
     private SurfaceListener mSurfaceListener = null;
-    private MediaPlayer mMediaPlayer;
+    private MediaPlayer2 mMediaPlayer;
     // A flag to indicate taking over other view should be proceed.
     private boolean mIsTakingOverOldView;
     private VideoViewInterface mOldView;
 
-
     VideoSurfaceView(Context context) {
-        this(context, null);
-    }
-
-    VideoSurfaceView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        getHolder().addCallback(this);
-    }
-
-    @RequiresApi(21)
-    VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr,
-                            int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
+        super(context, null);
         getHolder().addCallback(this);
     }
 
     ////////////////////////////////////////////////////
-    // implements VideoViewInterface
+    // implements VideoViewInterfaceWithMp1
     ////////////////////////////////////////////////////
 
     @Override
-    public boolean assignSurfaceToMediaPlayer(MediaPlayer mp) {
-        Log.d(TAG, "assignSurfaceToMediaPlayer(): mSurfaceHolder: " + mSurfaceHolder);
+    public boolean assignSurfaceToMediaPlayer(MediaPlayer2 mp) {
+        Log.d(TAG, "assignSurfaceToMediaPlayer(): mSurface: " + mSurface);
         if (mp == null || !hasAvailableSurface()) {
             return false;
         }
-        mp.setDisplay(mSurfaceHolder);
+        mp.setSurface(mSurface);
         return true;
     }
 
@@ -87,7 +72,7 @@
     }
 
     @Override
-    public void setMediaPlayer(MediaPlayer mp) {
+    public void setMediaPlayer(MediaPlayer2 mp) {
         mMediaPlayer = mp;
         if (mIsTakingOverOldView) {
             takeOver(mOldView);
@@ -111,7 +96,7 @@
 
     @Override
     public boolean hasAvailableSurface() {
-        return (mSurfaceHolder != null && mSurfaceHolder.getSurface() != null);
+        return mSurface != null && mSurface.isValid();
     }
 
     ////////////////////////////////////////////////////
@@ -120,8 +105,8 @@
 
     @Override
     public void surfaceCreated(SurfaceHolder holder) {
-        Log.d(TAG, "surfaceCreated: mSurfaceHolder: " + mSurfaceHolder + ", new holder: " + holder);
-        mSurfaceHolder = holder;
+        Log.d(TAG, "surfaceCreated: mSurface: " + mSurface + ", new : " + holder.getSurface());
+        mSurface = holder.getSurface();
         if (mIsTakingOverOldView) {
             takeOver(mOldView);
         } else {
@@ -129,7 +114,7 @@
         }
 
         if (mSurfaceListener != null) {
-            Rect rect = mSurfaceHolder.getSurfaceFrame();
+            Rect rect = holder.getSurfaceFrame();
             mSurfaceListener.onSurfaceCreated(this, rect.width(), rect.height());
         }
     }
@@ -144,13 +129,12 @@
     @Override
     public void surfaceDestroyed(SurfaceHolder holder) {
         // After we return from this we can't use the surface any more
-        mSurfaceHolder = null;
+        mSurface = null;
         if (mSurfaceListener != null) {
             mSurfaceListener.onSurfaceDestroyed(this);
         }
     }
 
-    // TODO: Investigate the way to move onMeasure() code into FrameLayout.
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         int videoWidth = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoWidth();
@@ -194,10 +178,4 @@
             Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
         }
     }
-
-    @Override
-    public String toString() {
-        return "ViewType: SurfaceView / Visibility: " + getVisibility()
-                + " / surfaceHolder: " + mSurfaceHolder;
-    }
 }
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoTextureView.java b/media-widget/src/main/java/androidx/media/widget/impl/VideoTextureView.java
similarity index 87%
rename from media-widget/src/main/java/androidx/media/widget/VideoTextureView.java
rename to media-widget/src/main/java/androidx/media/widget/impl/VideoTextureView.java
index cdc833b..3726047 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoTextureView.java
+++ b/media-widget/src/main/java/androidx/media/widget/impl/VideoTextureView.java
@@ -20,8 +20,6 @@
 
 import android.content.Context;
 import android.graphics.SurfaceTexture;
-import android.media.MediaPlayer;
-import android.util.AttributeSet;
 import android.util.Log;
 import android.view.Surface;
 import android.view.TextureView;
@@ -29,44 +27,32 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.media.MediaPlayer2;
 
 @RequiresApi(21)
 class VideoTextureView extends TextureView
         implements VideoViewInterface, TextureView.SurfaceTextureListener {
-    private static final String TAG = "VideoTextureView";
-    private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+    private static final String TAG = "VideoTextureViewWithMp1";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private Surface mSurface;
     private SurfaceListener mSurfaceListener;
-    private MediaPlayer mMediaPlayer;
+    private MediaPlayer2 mMediaPlayer;
     // A flag to indicate taking over other view should be proceed.
     private boolean mIsTakingOverOldView;
     private VideoViewInterface mOldView;
 
     VideoTextureView(Context context) {
-        this(context, null);
-    }
-
-    VideoTextureView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    VideoTextureView(Context context, AttributeSet attrs, int defStyleAttr) {
-        this(context, attrs, defStyleAttr, 0);
-    }
-
-    VideoTextureView(
-            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
+        super(context, null);
         setSurfaceTextureListener(this);
     }
 
     ////////////////////////////////////////////////////
-    // implements VideoViewInterface
+    // implements VideoViewInterfaceWithMp1
     ////////////////////////////////////////////////////
 
     @Override
-    public boolean assignSurfaceToMediaPlayer(MediaPlayer mp) {
+    public boolean assignSurfaceToMediaPlayer(MediaPlayer2 mp) {
         if (mp == null || !hasAvailableSurface()) {
             // Surface is not ready.
             return false;
@@ -86,7 +72,7 @@
     }
 
     @Override
-    public void setMediaPlayer(MediaPlayer mp) {
+    public void setMediaPlayer(MediaPlayer2 mp) {
         mMediaPlayer = mp;
         if (mIsTakingOverOldView) {
             takeOver(mOldView);
diff --git a/media-widget/src/main/java/androidx/media/widget/impl/VideoView2ImplBase.java b/media-widget/src/main/java/androidx/media/widget/impl/VideoView2ImplBase.java
new file mode 100644
index 0000000..68477f8
--- /dev/null
+++ b/media-widget/src/main/java/androidx/media/widget/impl/VideoView2ImplBase.java
@@ -0,0 +1,1570 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Point;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.media.MediaMetadataRetriever;
+import android.media.PlaybackParams;
+import android.media.SubtitleData;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaControllerCompat.PlaybackInfo;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.media.AudioAttributesCompat;
+import androidx.media.DataSourceDesc;
+import androidx.media.MediaItem2;
+import androidx.media.MediaMetadata2;
+import androidx.media.MediaPlayer2;
+import androidx.media.MediaPlayer2.MediaPlayer2EventCallback;
+import androidx.media.SessionToken2;
+import androidx.media.subtitle.ClosedCaptionRenderer;
+import androidx.media.subtitle.SubtitleController;
+import androidx.media.subtitle.SubtitleTrack;
+import androidx.mediarouter.media.MediaControlIntent;
+import androidx.mediarouter.media.MediaItemStatus;
+import androidx.mediarouter.media.MediaRouteSelector;
+import androidx.mediarouter.media.MediaRouter;
+import androidx.palette.graphics.Palette;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Base implementation of VideoView2.
+ */
+@RequiresApi(28) // TODO correct minSdk API use incompatibilities and remove before release.
+class VideoView2ImplBase implements VideoView2Impl, VideoViewInterface.SurfaceListener {
+    private static final String TAG = "VideoView2ImplBase";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final long DEFAULT_SHOW_CONTROLLER_INTERVAL_MS = 2000;
+
+    private static final int STATE_ERROR = -1;
+    private static final int STATE_IDLE = 0;
+    private static final int STATE_PREPARING = 1;
+    private static final int STATE_PREPARED = 2;
+    private static final int STATE_PLAYING = 3;
+    private static final int STATE_PAUSED = 4;
+    private static final int STATE_PLAYBACK_COMPLETED = 5;
+
+    private static final int INVALID_TRACK_INDEX = -1;
+    private static final float INVALID_SPEED = 0f;
+
+    private static final int SIZE_TYPE_EMBEDDED = 0;
+    private static final int SIZE_TYPE_FULL = 1;
+    private static final int SIZE_TYPE_MINIMAL = 2;
+
+    private AccessibilityManager mAccessibilityManager;
+    private AudioManager mAudioManager;
+    private AudioAttributesCompat mAudioAttributes;
+    private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain
+    private boolean mAudioFocused = false;
+
+    private Pair<Executor, VideoView2.OnCustomActionListener> mCustomActionListenerRecord;
+    private VideoView2.OnViewTypeChangedListener mViewTypeChangedListener;
+
+    private VideoViewInterface mCurrentView;
+    private VideoTextureView mTextureView;
+    private VideoSurfaceView mSurfaceView;
+
+    private MediaPlayer2 mMediaPlayer;
+    private DataSourceDesc mDsd;
+    private Uri mUri;
+    private Map<String, String> mHeaders;
+    private MediaControlView2 mMediaControlView;
+    private MediaSessionCompat mMediaSession;
+    private MediaControllerCompat mMediaController;
+    private MediaMetadata2 mMediaMetadata;
+    private MediaMetadataRetriever mRetriever;
+    private boolean mNeedUpdateMediaType;
+    private Bundle mMediaTypeData;
+    private String mTitle;
+
+    private WindowManager mManager;
+    private Resources mResources;
+    private View mMusicView;
+    private Drawable mMusicAlbumDrawable;
+    private String mMusicTitleText;
+    private String mMusicArtistText;
+    private boolean mIsMusicMediaType;
+    private int mPrevWidth;
+    private int mPrevHeight;
+    private int mDominantColor;
+    private int mSizeType;
+
+    private PlaybackStateCompat.Builder mStateBuilder;
+    private List<PlaybackStateCompat.CustomAction> mCustomActionList;
+
+    private int mTargetState = STATE_IDLE;
+    private int mCurrentState = STATE_IDLE;
+    private int mCurrentBufferPercentage;
+    private long mSeekWhenPrepared;  // recording the seek position while preparing
+
+    private int mVideoWidth;
+    private int mVideoHeight;
+
+    private ArrayList<Integer> mVideoTrackIndices;
+    private ArrayList<Integer> mAudioTrackIndices;
+    private ArrayList<Pair<Integer, SubtitleTrack>> mSubtitleTrackIndices;
+    private SubtitleController mSubtitleController;
+
+    // selected video/audio/subtitle track index as MediaPlayer returns
+    private int mSelectedVideoTrackIndex;
+    private int mSelectedAudioTrackIndex;
+    private int mSelectedSubtitleTrackIndex;
+
+    private SubtitleView mSubtitleView;
+    private boolean mSubtitleEnabled;
+
+    private float mSpeed;
+    private float mFallbackSpeed;  // keep the original speed before 'pause' is called.
+    private float mVolumeLevelFloat;
+    private int mVolumeLevel;
+    private VideoView2 mInstance;
+
+    private long mShowControllerIntervalMs;
+
+    private MediaRouter mMediaRouter;
+    private MediaRouteSelector mRouteSelector;
+    private MediaRouter.RouteInfo mRoute;
+    private RoutePlayer mRoutePlayer;
+
+    private final MediaRouter.Callback mRouterCallback = new MediaRouter.Callback() {
+        @Override
+        public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
+            if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
+                // Stop local playback (if necessary)
+                resetPlayer();
+                mRoute = route;
+                mRoutePlayer = new RoutePlayer(mInstance.getContext(), route);
+                mRoutePlayer.setPlayerEventCallback(new RoutePlayer.PlayerEventCallback() {
+                    @Override
+                    public void onPlayerStateChanged(MediaItemStatus itemStatus) {
+                        PlaybackStateCompat.Builder psBuilder = new PlaybackStateCompat.Builder();
+                        psBuilder.setActions(RoutePlayer.PLAYBACK_ACTIONS);
+                        long position = itemStatus.getContentPosition();
+                        switch (itemStatus.getPlaybackState()) {
+                            case MediaItemStatus.PLAYBACK_STATE_PENDING:
+                                psBuilder.setState(PlaybackStateCompat.STATE_NONE, position, 0);
+                                mCurrentState = STATE_IDLE;
+                                break;
+                            case MediaItemStatus.PLAYBACK_STATE_PLAYING:
+                                psBuilder.setState(PlaybackStateCompat.STATE_PLAYING, position, 1);
+                                mCurrentState = STATE_PLAYING;
+                                break;
+                            case MediaItemStatus.PLAYBACK_STATE_PAUSED:
+                                psBuilder.setState(PlaybackStateCompat.STATE_PAUSED, position, 0);
+                                mCurrentState = STATE_PAUSED;
+                                break;
+                            case MediaItemStatus.PLAYBACK_STATE_BUFFERING:
+                                psBuilder.setState(
+                                        PlaybackStateCompat.STATE_BUFFERING, position, 0);
+                                mCurrentState = STATE_PAUSED;
+                                break;
+                            case MediaItemStatus.PLAYBACK_STATE_FINISHED:
+                                psBuilder.setState(PlaybackStateCompat.STATE_STOPPED, position, 0);
+                                mCurrentState = STATE_PLAYBACK_COMPLETED;
+                                break;
+                        }
+
+                        PlaybackStateCompat pbState = psBuilder.build();
+                        mMediaSession.setPlaybackState(pbState);
+
+                        MediaMetadataCompat.Builder mmBuilder = new MediaMetadataCompat.Builder();
+                        mmBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION,
+                                itemStatus.getContentDuration());
+                        mMediaSession.setMetadata(mmBuilder.build());
+                    }
+                });
+                // Start remote playback (if necessary)
+                // TODO: b/77556429
+                mRoutePlayer.openVideo(mUri);
+            }
+        }
+
+        @Override
+        public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route, int reason) {
+            if (mRoute != null && mRoutePlayer != null) {
+                mRoutePlayer.release();
+                mRoutePlayer = null;
+            }
+            if (mRoute == route) {
+                mRoute = null;
+            }
+            if (reason != MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
+                // TODO: Resume local playback  (if necessary)
+                // TODO: b/77556429
+                openVideo(mDsd);
+            }
+        }
+    };
+
+    @Override
+    public void initialize(
+            VideoView2 instance, Context context,
+            @Nullable AttributeSet attrs, int defStyleAttr) {
+        mInstance = instance;
+
+        mVideoWidth = 0;
+        mVideoHeight = 0;
+        mSpeed = 1.0f;
+        mFallbackSpeed = mSpeed;
+        mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
+        mShowControllerIntervalMs = DEFAULT_SHOW_CONTROLLER_INTERVAL_MS;
+
+        mAccessibilityManager = (AccessibilityManager) context.getSystemService(
+                Context.ACCESSIBILITY_SERVICE);
+
+        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        mAudioAttributes = new AudioAttributesCompat.Builder()
+                .setUsage(AudioAttributesCompat.USAGE_MEDIA)
+                .setContentType(AudioAttributesCompat.CONTENT_TYPE_MOVIE).build();
+
+        mInstance.setFocusable(true);
+        mInstance.setFocusableInTouchMode(true);
+        mInstance.requestFocus();
+
+        mTextureView = new VideoTextureView(context);
+        mSurfaceView = new VideoSurfaceView(context);
+        LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
+                LayoutParams.MATCH_PARENT);
+        mTextureView.setLayoutParams(params);
+        mSurfaceView.setLayoutParams(params);
+        mTextureView.setSurfaceListener(this);
+        mSurfaceView.setSurfaceListener(this);
+
+        mInstance.addView(mTextureView);
+        mInstance.addView(mSurfaceView);
+
+        mSubtitleView = new SubtitleView(context);
+        mSubtitleView.setLayoutParams(params);
+        mSubtitleView.setBackgroundColor(0);
+        mInstance.addView(mSubtitleView);
+
+        boolean enableControlView = (attrs == null) || attrs.getAttributeBooleanValue(
+                "http://schemas.android.com/apk/res/android",
+                "enableControlView", true);
+        if (enableControlView) {
+            mMediaControlView = new MediaControlView2(context);
+        }
+
+        mSubtitleEnabled = (attrs == null) || attrs.getAttributeBooleanValue(
+                "http://schemas.android.com/apk/res/android",
+                "enableSubtitle", false);
+
+        // Choose surface view by default
+        int viewType = (attrs == null) ? VideoView2.VIEW_TYPE_SURFACEVIEW
+                : attrs.getAttributeIntValue(
+                "http://schemas.android.com/apk/res/android",
+                "viewType", VideoView2.VIEW_TYPE_SURFACEVIEW);
+        if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) {
+            Log.d(TAG, "viewType attribute is surfaceView.");
+            mTextureView.setVisibility(View.GONE);
+            mSurfaceView.setVisibility(View.VISIBLE);
+            mCurrentView = mSurfaceView;
+        } else if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
+            Log.d(TAG, "viewType attribute is textureView.");
+            mTextureView.setVisibility(View.VISIBLE);
+            mSurfaceView.setVisibility(View.GONE);
+            mCurrentView = mTextureView;
+        }
+
+        MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
+        builder.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+        builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
+        builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+        mRouteSelector = builder.build();
+    }
+
+    /**
+     * Sets MediaControlView2 instance. It will replace the previously assigned MediaControlView2
+     * instance if any.
+     *
+     * @param mediaControlView a media control view2 instance.
+     * @param intervalMs a time interval in milliseconds until VideoView2 hides MediaControlView2.
+     */
+    @Override
+    public void setMediaControlView2(MediaControlView2 mediaControlView, long intervalMs) {
+        mMediaControlView = mediaControlView;
+        mShowControllerIntervalMs = intervalMs;
+        mMediaControlView.setRouteSelector(mRouteSelector);
+
+        if (mInstance.isAttachedToWindow()) {
+            attachMediaControlView();
+        }
+    }
+
+    /**
+     * Returns MediaControlView2 instance which is currently attached to VideoView2 by default or by
+     * {@link #setMediaControlView2} method.
+     */
+    @Override
+    public MediaControlView2 getMediaControlView2() {
+        return mMediaControlView;
+    }
+
+    /**
+     * Sets MediaMetadata2 instance. It will replace the previously assigned MediaMetadata2 instance
+     * if any.
+     *
+     * @param metadata a MediaMetadata2 instance.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void setMediaMetadata(MediaMetadata2 metadata) {
+      //mProvider.setMediaMetadata_impl(metadata);
+    }
+
+    /**
+     * Returns MediaMetadata2 instance which is retrieved from MediaPlayer inside VideoView2 by
+     * default or by {@link #setMediaMetadata} method.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public MediaMetadata2 getMediaMetadata() {
+        return mMediaMetadata;
+    }
+
+    /**
+     * Returns MediaController instance which is connected with MediaSession that VideoView2 is
+     * using. This method should be called when VideoView2 is attached to window, or it throws
+     * IllegalStateException, since internal MediaSession instance is not available until
+     * this view is attached to window. Please check {@link View#isAttachedToWindow}
+     * before calling this method.
+     *
+     * @throws IllegalStateException if interal MediaSession is not created yet.
+     * @hide  TODO: remove
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public MediaControllerCompat getMediaController() {
+        if (mMediaSession == null) {
+            throw new IllegalStateException("MediaSession instance is not available.");
+        }
+        return mMediaController;
+    }
+
+    /**
+     * Returns {@link SessionToken2} so that developers create their own
+     * {@link androidx.media.MediaController2} instance. This method should be called when
+     * VideoView2 is attached to window, or it throws IllegalStateException.
+     *
+     * @throws IllegalStateException if interal MediaSession is not created yet.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public SessionToken2 getMediaSessionToken() {
+        //return mProvider.getMediaSessionToken_impl();
+        return null;
+    }
+
+    /**
+     * Shows or hides closed caption or subtitles if there is any.
+     * The first subtitle track will be chosen if there multiple subtitle tracks exist.
+     * Default behavior of VideoView2 is not showing subtitle.
+     * @param enable shows closed caption or subtitles if this value is true, or hides.
+     */
+    @Override
+    public void setSubtitleEnabled(boolean enable) {
+        // No-op on API < 28
+    }
+
+    /**
+     * Returns true if showing subtitle feature is enabled or returns false.
+     * Although there is no subtitle track or closed caption, it can return true, if the feature
+     * has been enabled by {@link #setSubtitleEnabled}.
+     */
+    @Override
+    public boolean isSubtitleEnabled() {
+        // Not supported on API < 28
+        return false;
+    }
+
+    /**
+     * Sets playback speed.
+     *
+     * It is expressed as a multiplicative factor, where normal speed is 1.0f. If it is less than
+     * or equal to zero, it will be just ignored and nothing will be changed. If it exceeds the
+     * maximum speed that internal engine supports, system will determine best handling or it will
+     * be reset to the normal speed 1.0f.
+     * @param speed the playback speed. It should be positive.
+     */
+    @Override
+    public void setSpeed(float speed) {
+        if (speed <= 0.0f) {
+            Log.e(TAG, "Unsupported speed (" + speed + ") is ignored.");
+            return;
+        }
+        mSpeed = speed;
+        if (isPlaying()) {
+            applySpeed();
+        }
+        updatePlaybackState();
+    }
+
+    /**
+     * Returns playback speed.
+     *
+     * It returns the same value that has been set by {@link #setSpeed}, if it was available value.
+     * If {@link #setSpeed} has not been called before, then the normal speed 1.0f will be returned.
+     */
+    @Override
+    public float getSpeed() {
+        return mSpeed;
+    }
+
+    /**
+     * Sets which type of audio focus will be requested during the playback, or configures playback
+     * to not request audio focus. Valid values for focus requests are
+     * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT},
+     * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and
+     * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use
+     * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be
+     * requested when playback starts. You can for instance use this when playing a silent animation
+     * through this class, and you don't want to affect other audio applications playing in the
+     * background.
+     *
+     * @param focusGain the type of audio focus gain that will be requested, or
+     *                  {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during
+     *                  playback.
+     */
+    @Override
+    public void setAudioFocusRequest(int focusGain) {
+        if (focusGain != AudioManager.AUDIOFOCUS_NONE
+                && focusGain != AudioManager.AUDIOFOCUS_GAIN
+                && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
+                && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+                && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) {
+            throw new IllegalArgumentException("Illegal audio focus type " + focusGain);
+        }
+        mAudioFocusType = focusGain;
+    }
+
+    /**
+     * Sets the {@link AudioAttributesCompat} to be used during the playback of the video.
+     *
+     * @param attributes non-null <code>AudioAttributesCompat</code>.
+     */
+    @Override
+    public void setAudioAttributes(@NonNull AudioAttributesCompat attributes) {
+        if (attributes == null) {
+            throw new IllegalArgumentException("Illegal null AudioAttributes");
+        }
+        mAudioAttributes = attributes;
+    }
+
+    /**
+     * Sets video path.
+     *
+     * @param path the path of the video.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void setVideoPath(String path) {
+        setVideoUri(Uri.parse(path));
+    }
+
+    /**
+     * Sets video URI.
+     *
+     * @param uri the URI of the video.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void setVideoUri(Uri uri) {
+        setVideoUri(uri, null);
+    }
+
+    /**
+     * Sets video URI using specific headers.
+     *
+     * @param uri     the URI of the video.
+     * @param headers the headers for the URI request.
+     *                Note that the cross domain redirection is allowed by default, but that can be
+     *                changed with key/value pairs through the headers parameter with
+     *                "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
+     *                to disallow or allow cross domain redirection.
+     */
+    @Override
+    public void setVideoUri(Uri uri, @Nullable Map<String, String> headers) {
+        DataSourceDesc.Builder builder = new DataSourceDesc.Builder();
+        builder.setDataSource(mInstance.getContext(), uri, headers, null);
+        setDataSource(builder.build());
+    }
+
+    /**
+     * Sets {@link MediaItem2} object to render using VideoView2. Alternative way to set media
+     * object to VideoView2 is {@link #setDataSource}.
+     * @param mediaItem the MediaItem2 to play
+     * @see #setDataSource
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void setMediaItem(@NonNull MediaItem2 mediaItem) {
+    }
+
+    /**
+     * Sets {@link DataSourceDesc} object to render using VideoView2.
+     * @param dataSource the {@link DataSourceDesc} object to play.
+     * @see #setMediaItem
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void setDataSource(@NonNull DataSourceDesc dataSource) {
+        mDsd = dataSource;
+        mSeekWhenPrepared = 0;
+        openVideo(dataSource);
+    }
+
+    /**
+     * Selects which view will be used to render video between SurfacView and TextureView.
+     *
+     * @param viewType the view type to render video
+     * <ul>
+     * <li>{@link #VideoView2.VIEW_TYPE_SURFACEVIEW}
+     * <li>{@link #VideoView2.VIEW_TYPE_TEXTUREVIEW}
+     * </ul>
+     */
+    @Override
+    public void setViewType(@VideoView2.ViewType int viewType) {
+        if (viewType == mCurrentView.getViewType()) {
+            return;
+        }
+        VideoViewInterface targetView;
+        if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
+            Log.d(TAG, "switching to TextureView");
+            targetView = mTextureView;
+        } else if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) {
+            Log.d(TAG, "switching to SurfaceView");
+            targetView = mSurfaceView;
+        } else {
+            throw new IllegalArgumentException("Unknown view type: " + viewType);
+        }
+        ((View) targetView).setVisibility(View.VISIBLE);
+        targetView.takeOver(mCurrentView);
+        mInstance.requestLayout();
+    }
+
+    /**
+     * Returns view type.
+     *
+     * @return view type. See {@see setViewType}.
+     */
+    @VideoView2.ViewType
+    @Override
+    public int getViewType() {
+        return mCurrentView.getViewType();
+    }
+
+    /**
+     * Sets custom actions which will be shown as custom buttons in {@link MediaControlView2}.
+     *
+     * @param actionList A list of {@link PlaybackStateCompat.CustomAction}. The return value of
+     *                   {@link PlaybackStateCompat.CustomAction#getIcon()} will be used to draw
+     *                   buttons in {@link MediaControlView2}.
+     * @param executor executor to run callbacks on.
+     * @param listener A listener to be called when a custom button is clicked.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void setCustomActions(List<PlaybackStateCompat.CustomAction> actionList,
+            Executor executor, VideoView2.OnCustomActionListener listener) {
+        mCustomActionList = actionList;
+        mCustomActionListenerRecord = new Pair<>(executor, listener);
+
+        // Create a new playback builder in order to clear existing the custom actions.
+        mStateBuilder = null;
+        updatePlaybackState();
+    }
+
+    /**
+     * Registers a callback to be invoked when a view type change is done.
+     * {@see #setViewType(int)}
+     * @param l The callback that will be run
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void setOnViewTypeChangedListener(VideoView2.OnViewTypeChangedListener l) {
+        mViewTypeChangedListener = l;
+    }
+
+    @Override
+    public void onAttachedToWindowImpl() {
+        // Create MediaSession
+        mMediaSession = new MediaSessionCompat(mInstance.getContext(), "VideoView2MediaSession");
+        mMediaSession.setCallback(new MediaSessionCallback());
+        mMediaSession.setActive(true);
+        mMediaController = mMediaSession.getController();
+        attachMediaControlView();
+        if (mCurrentState == STATE_PREPARED) {
+            extractTracks();
+            extractMetadata();
+            extractAudioMetadata();
+            if (mNeedUpdateMediaType) {
+                mMediaSession.sendSessionEvent(
+                        MediaControlView2.EVENT_UPDATE_MEDIA_TYPE_STATUS,
+                        mMediaTypeData);
+                mNeedUpdateMediaType = false;
+            }
+        }
+
+        mMediaRouter = MediaRouter.getInstance(mInstance.getContext());
+        mMediaRouter.setMediaSessionCompat(mMediaSession);
+        mMediaRouter.addCallback(mRouteSelector, mRouterCallback,
+                MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+    }
+
+    @Override
+    public void onDetachedFromWindowImpl() {
+        mMediaSession.release();
+        mMediaSession = null;
+        mMediaController = null;
+    }
+
+    @Override
+    public void onTouchEventImpl(MotionEvent ev) {
+        if (DEBUG) {
+            Log.d(TAG, "onTouchEvent(). mCurrentState=" + mCurrentState
+                    + ", mTargetState=" + mTargetState);
+        }
+        if (ev.getAction() == MotionEvent.ACTION_UP && mMediaControlView != null) {
+            if (!mIsMusicMediaType || mSizeType != SIZE_TYPE_FULL) {
+                toggleMediaControlViewVisibility();
+            }
+        }
+    }
+
+    @Override
+    public void onTrackballEventImpl(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_UP && mMediaControlView != null) {
+            if (!mIsMusicMediaType || mSizeType != SIZE_TYPE_FULL) {
+                toggleMediaControlViewVisibility();
+            }
+        }
+    }
+
+    @Override
+    public void onMeasureImpl(int widthMeasureSpec, int heightMeasureSpec) {
+        if (mIsMusicMediaType) {
+            int currWidth = mInstance.getMeasuredWidth();
+            int currHeight = mInstance.getMeasuredHeight();
+            if (mPrevWidth != currWidth || mPrevHeight != currHeight) {
+                Point screenSize = new Point();
+                mManager.getDefaultDisplay().getSize(screenSize);
+                int screenWidth = screenSize.x;
+                int screenHeight = screenSize.y;
+
+                if (currWidth == screenWidth && currHeight == screenHeight) {
+                    int orientation = retrieveOrientation();
+                    if (orientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
+                        inflateMusicView(R.layout.full_landscape_music);
+                    } else {
+                        inflateMusicView(R.layout.full_portrait_music);
+                    }
+
+                    if (mSizeType != SIZE_TYPE_FULL) {
+                        mSizeType = SIZE_TYPE_FULL;
+                        // Remove existing mFadeOut callback
+                        mMediaControlView.removeCallbacks(mFadeOut);
+                        mMediaControlView.setVisibility(View.VISIBLE);
+                    }
+                } else {
+                    if (mSizeType != SIZE_TYPE_EMBEDDED) {
+                        mSizeType = SIZE_TYPE_EMBEDDED;
+                        inflateMusicView(R.layout.embedded_music);
+                        // Add new mFadeOut callback
+                        mMediaControlView.postDelayed(mFadeOut, mShowControllerIntervalMs);
+                    }
+                }
+                mPrevWidth = currWidth;
+                mPrevHeight = currHeight;
+            }
+        }
+    }
+
+    ///////////////////////////////////////////////////
+    // Implements VideoViewInterface.SurfaceListener
+    ///////////////////////////////////////////////////
+
+    /**
+     * @hide
+     */
+    @Override
+    @RestrictTo(LIBRARY_GROUP)
+    public void onSurfaceCreated(View view, int width, int height) {
+        if (DEBUG) {
+            Log.d(TAG, "onSurfaceCreated(). mCurrentState=" + mCurrentState
+                    + ", mTargetState=" + mTargetState + ", width/height: " + width + "/" + height
+                    + ", " + view.toString());
+        }
+        if (needToStart()) {
+            mMediaController.getTransportControls().play();
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    @RestrictTo(LIBRARY_GROUP)
+    public void onSurfaceDestroyed(View view) {
+        if (DEBUG) {
+            Log.d(TAG, "onSurfaceDestroyed(). mCurrentState=" + mCurrentState
+                    + ", mTargetState=" + mTargetState + ", " + view.toString());
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    @RestrictTo(LIBRARY_GROUP)
+    public void onSurfaceChanged(View view, int width, int height) {
+        if (DEBUG) {
+            Log.d(TAG, "onSurfaceChanged(). width/height: " + width + "/" + height
+                    + ", " + view.toString());
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    @RestrictTo(LIBRARY_GROUP)
+    public void onSurfaceTakeOverDone(VideoViewInterface view) {
+        if (DEBUG) {
+            Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view);
+        }
+        mCurrentView = view;
+        if (mViewTypeChangedListener != null) {
+            mViewTypeChangedListener.onViewTypeChanged(mInstance, view.getViewType());
+        }
+        if (needToStart()) {
+            mMediaController.getTransportControls().play();
+        }
+    }
+
+    ///////////////////////////////////////////////////
+    // Protected or private methods
+    ///////////////////////////////////////////////////
+
+    private void attachMediaControlView() {
+        // Get MediaController from MediaSession and set it inside MediaControlView
+        mMediaControlView.setController(mMediaSession.getController());
+
+        LayoutParams params =
+                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+        mInstance.addView(mMediaControlView, params);
+    }
+
+    private boolean isInPlaybackState() {
+        return (mMediaPlayer != null || mRoutePlayer != null)
+                && mCurrentState != STATE_ERROR
+                && mCurrentState != STATE_IDLE
+                && mCurrentState != STATE_PREPARING;
+    }
+
+    private boolean needToStart() {
+        return (mMediaPlayer != null || mRoutePlayer != null)
+                && isAudioGranted()
+                && isWaitingPlayback();
+    }
+
+    private boolean isPlaying() {
+        return mMediaPlayer != null
+                && mMediaPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING;
+    }
+
+    private boolean isWaitingPlayback() {
+        return mCurrentState != STATE_PLAYING && mTargetState == STATE_PLAYING;
+    }
+
+    private boolean isAudioGranted() {
+        return mAudioFocused || mAudioFocusType == AudioManager.AUDIOFOCUS_NONE;
+    }
+
+    private AudioManager.OnAudioFocusChangeListener mAudioFocusListener =
+            new AudioManager.OnAudioFocusChangeListener() {
+        @Override
+        public void onAudioFocusChange(int focusChange) {
+            switch (focusChange) {
+                case AudioManager.AUDIOFOCUS_GAIN:
+                    mAudioFocused = true;
+                    if (needToStart()) {
+                        mMediaController.getTransportControls().play();
+                    }
+                    break;
+                case AudioManager.AUDIOFOCUS_LOSS:
+                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+                    mAudioFocused = false;
+                    if (isInPlaybackState() && isPlaying()) {
+                        mMediaController.getTransportControls().pause();
+                    } else {
+                        mTargetState = STATE_PAUSED;
+                    }
+            }
+        }
+    };
+
+    @SuppressWarnings("deprecation")
+    private void requestAudioFocus(int focusType) {
+        int result;
+        if (android.os.Build.VERSION.SDK_INT >= 26) {
+            AudioFocusRequest focusRequest;
+            focusRequest = new AudioFocusRequest.Builder(focusType)
+                    .setAudioAttributes((AudioAttributes) mAudioAttributes.unwrap())
+                    .setOnAudioFocusChangeListener(mAudioFocusListener)
+                    .build();
+            result = mAudioManager.requestAudioFocus(focusRequest);
+        } else {
+            result = mAudioManager.requestAudioFocus(mAudioFocusListener,
+                    AudioManager.STREAM_MUSIC,
+                    focusType);
+        }
+        if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
+            mAudioFocused = false;
+        } else if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+            mAudioFocused = true;
+        } else if (result == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
+            mAudioFocused = false;
+        }
+    }
+
+    // Creates a MediaPlayer instance and prepare playback.
+    private void openVideo(DataSourceDesc dsd) {
+        Uri uri = dsd.getUri();
+        Map<String, String> headers = dsd.getUriHeaders();
+        resetPlayer();
+        if (isRemotePlayback()) {
+            // TODO: b/77556429
+            mRoutePlayer.openVideo(uri);
+            return;
+        }
+
+        try {
+            Log.d(TAG, "openVideo(): creating new MediaPlayer instance.");
+            mMediaPlayer = MediaPlayer2.create();
+            mSurfaceView.setMediaPlayer(mMediaPlayer);
+            mTextureView.setMediaPlayer(mMediaPlayer);
+            mCurrentView.assignSurfaceToMediaPlayer(mMediaPlayer);
+
+            final Context context = mInstance.getContext();
+            mSubtitleController = new SubtitleController(context);
+            mSubtitleController.registerRenderer(new ClosedCaptionRenderer(context));
+            mSubtitleController.setAnchor((SubtitleController.Anchor) mSubtitleView);
+            Executor executor = new Executor() {
+                @Override
+                public void execute(Runnable runnable) {
+                    runnable.run();
+                }
+            };
+            mMediaPlayer.setMediaPlayer2EventCallback(executor, mMediaPlayer2Callback);
+
+            mCurrentBufferPercentage = -1;
+            mMediaPlayer.setDataSource(dsd);
+            mMediaPlayer.setAudioAttributes(mAudioAttributes);
+            // we don't set the target state here either, but preserve the
+            // target state that was there before.
+            mCurrentState = STATE_PREPARING;
+            mMediaPlayer.prepare();
+
+            // Save file name as title since the file may not have a title Metadata.
+            mTitle = uri.getPath();
+            String scheme = uri.getScheme();
+            if (scheme != null && scheme.equals("file")) {
+                mTitle = uri.getLastPathSegment();
+                mRetriever = new MediaMetadataRetriever();
+                mRetriever.setDataSource(context, uri);
+            }
+
+            if (DEBUG) {
+                Log.d(TAG, "openVideo(). mCurrentState=" + mCurrentState
+                        + ", mTargetState=" + mTargetState);
+            }
+        } catch (IllegalArgumentException ex) {
+            Log.w(TAG, "Unable to open content: " + uri, ex);
+            mCurrentState = STATE_ERROR;
+            mTargetState = STATE_ERROR;
+            mMediaPlayer2Callback.onError(mMediaPlayer, dsd,
+                    MediaPlayer2.MEDIA_ERROR_UNKNOWN, MediaPlayer2.MEDIA_ERROR_IO);
+        }
+    }
+
+    /*
+     * Reset the media player in any state
+     */
+    @SuppressWarnings("deprecation")
+    private void resetPlayer() {
+        if (mMediaPlayer != null) {
+            final MediaPlayer2 player = mMediaPlayer;
+            new AsyncTask<MediaPlayer2, Void, Void>() {
+                @Override
+                protected Void doInBackground(MediaPlayer2... players) {
+                    // TODO: Fix NPE while MediaPlayer2.close()
+                    //players[0].close();
+                    return null;
+                }
+            }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, player);
+            mMediaPlayer = null;
+            mTextureView.setMediaPlayer(null);
+            mSurfaceView.setMediaPlayer(null);
+            mCurrentState = STATE_IDLE;
+            mTargetState = STATE_IDLE;
+            if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+                mAudioManager.abandonAudioFocus(null);
+            }
+        }
+        mVideoWidth = 0;
+        mVideoHeight = 0;
+    }
+
+    private void updatePlaybackState() {
+        if (mStateBuilder == null) {
+            long playbackActions = PlaybackStateCompat.ACTION_PLAY
+                    | PlaybackStateCompat.ACTION_PAUSE
+                    | PlaybackStateCompat.ACTION_REWIND | PlaybackStateCompat.ACTION_FAST_FORWARD
+                    | PlaybackStateCompat.ACTION_SEEK_TO;
+            mStateBuilder = new PlaybackStateCompat.Builder();
+            mStateBuilder.setActions(playbackActions);
+
+            if (mCustomActionList != null) {
+                for (PlaybackStateCompat.CustomAction action : mCustomActionList) {
+                    mStateBuilder.addCustomAction(action);
+                }
+            }
+        }
+        mStateBuilder.setState(getCorrespondingPlaybackState(),
+                mMediaPlayer.getCurrentPosition(), mSpeed);
+        if (mCurrentState != STATE_ERROR
+                && mCurrentState != STATE_IDLE
+                && mCurrentState != STATE_PREPARING) {
+            if (mCurrentBufferPercentage == -1) {
+                mStateBuilder.setBufferedPosition(-1);
+            } else {
+                mStateBuilder.setBufferedPosition(
+                        (long) (mCurrentBufferPercentage / 100.0 * mMediaPlayer.getDuration()));
+            }
+        }
+
+        // Set PlaybackState for MediaSession
+        if (mMediaSession != null) {
+            PlaybackStateCompat state = mStateBuilder.build();
+            mMediaSession.setPlaybackState(state);
+        }
+    }
+
+    private int getCorrespondingPlaybackState() {
+        switch (mCurrentState) {
+            case STATE_ERROR:
+                return PlaybackStateCompat.STATE_ERROR;
+            case STATE_IDLE:
+                return PlaybackStateCompat.STATE_NONE;
+            case STATE_PREPARING:
+                return PlaybackStateCompat.STATE_CONNECTING;
+            case STATE_PREPARED:
+                return PlaybackStateCompat.STATE_PAUSED;
+            case STATE_PLAYING:
+                return PlaybackStateCompat.STATE_PLAYING;
+            case STATE_PAUSED:
+                return PlaybackStateCompat.STATE_PAUSED;
+            case STATE_PLAYBACK_COMPLETED:
+                return PlaybackStateCompat.STATE_STOPPED;
+            default:
+                return -1;
+        }
+    }
+
+    private final Runnable mFadeOut = new Runnable() {
+        @Override
+        public void run() {
+            if (mCurrentState == STATE_PLAYING) {
+                mMediaControlView.setVisibility(View.GONE);
+            }
+        }
+    };
+
+    private void showController() {
+        if (mMediaControlView == null || !isInPlaybackState()
+                || (mIsMusicMediaType && mSizeType == SIZE_TYPE_FULL)) {
+            return;
+        }
+        mMediaControlView.removeCallbacks(mFadeOut);
+        mMediaControlView.setVisibility(View.VISIBLE);
+        if (mShowControllerIntervalMs != 0
+                && !mAccessibilityManager.isTouchExplorationEnabled()) {
+            mMediaControlView.postDelayed(mFadeOut, mShowControllerIntervalMs);
+        }
+    }
+
+    private void toggleMediaControlViewVisibility() {
+        if (mMediaControlView.getVisibility() == View.VISIBLE) {
+            mMediaControlView.removeCallbacks(mFadeOut);
+            mMediaControlView.setVisibility(View.GONE);
+        } else {
+            showController();
+        }
+    }
+
+    private void applySpeed() {
+        if (android.os.Build.VERSION.SDK_INT < 23) {
+            return;
+        }
+        PlaybackParams params = mMediaPlayer.getPlaybackParams().allowDefaults();
+        if (mSpeed != params.getSpeed()) {
+            try {
+                params.setSpeed(mSpeed);
+                mMediaPlayer.setPlaybackParams(params);
+                mFallbackSpeed = mSpeed;
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "PlaybackParams has unsupported value: " + e);
+                float fallbackSpeed = mMediaPlayer.getPlaybackParams().allowDefaults().getSpeed();
+                if (fallbackSpeed > 0.0f) {
+                    mFallbackSpeed = fallbackSpeed;
+                }
+                mSpeed = mFallbackSpeed;
+            }
+        }
+    }
+
+    private boolean isRemotePlayback() {
+        if (mMediaController == null) {
+            return false;
+        }
+        PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo();
+        return playbackInfo != null
+                && playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
+    }
+
+    private void selectOrDeselectSubtitle(boolean select) {
+        if (!isInPlaybackState()) {
+            return;
+        }
+        if (select) {
+            if (mSubtitleTrackIndices.size() > 0) {
+                mSelectedSubtitleTrackIndex = mSubtitleTrackIndices.get(0).first;
+                mSubtitleController.selectTrack(mSubtitleTrackIndices.get(0).second);
+                mMediaPlayer.selectTrack(mSelectedSubtitleTrackIndex);
+                mSubtitleView.setVisibility(View.VISIBLE);
+            }
+        } else {
+            if (mSelectedSubtitleTrackIndex != INVALID_TRACK_INDEX) {
+                mMediaPlayer.deselectTrack(mSelectedSubtitleTrackIndex);
+                mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
+                mSubtitleView.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    private void extractTracks() {
+        List<MediaPlayer2.TrackInfo> trackInfos = mMediaPlayer.getTrackInfo();
+        mVideoTrackIndices = new ArrayList<>();
+        mAudioTrackIndices = new ArrayList<>();
+        mSubtitleTrackIndices = new ArrayList<>();
+        mSubtitleController.reset();
+        for (int i = 0; i < trackInfos.size(); ++i) {
+            int trackType = trackInfos.get(i).getTrackType();
+            if (trackType == MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_VIDEO) {
+                mVideoTrackIndices.add(i);
+            } else if (trackType == MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) {
+                mAudioTrackIndices.add(i);
+            } else if (trackType == MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
+                SubtitleTrack track = mSubtitleController.addTrack(trackInfos.get(i).getFormat());
+                if (track != null) {
+                    mSubtitleTrackIndices.add(new Pair<>(i, track));
+                }
+            }
+        }
+        // Select first tracks as default
+        if (mVideoTrackIndices.size() > 0) {
+            mSelectedVideoTrackIndex = 0;
+        }
+        if (mAudioTrackIndices.size() > 0) {
+            mSelectedAudioTrackIndex = 0;
+        }
+        if (mVideoTrackIndices.size() == 0 && mAudioTrackIndices.size() > 0) {
+            mIsMusicMediaType = true;
+        }
+
+        Bundle data = new Bundle();
+        data.putInt(MediaControlView2.KEY_VIDEO_TRACK_COUNT, mVideoTrackIndices.size());
+        data.putInt(MediaControlView2.KEY_AUDIO_TRACK_COUNT, mAudioTrackIndices.size());
+        data.putInt(MediaControlView2.KEY_SUBTITLE_TRACK_COUNT, mSubtitleTrackIndices.size());
+        if (mSubtitleTrackIndices.size() > 0) {
+            selectOrDeselectSubtitle(mSubtitleEnabled);
+        }
+        mMediaSession.sendSessionEvent(MediaControlView2.EVENT_UPDATE_TRACK_STATUS, data);
+    }
+
+    private void extractMetadata() {
+        if (mRetriever == null) {
+            return;
+        }
+        // Get and set duration and title values as MediaMetadata for MediaControlView2
+        MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+        String title = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
+        if (title != null) {
+            mTitle = title;
+        }
+        builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
+        builder.putLong(
+                MediaMetadataCompat.METADATA_KEY_DURATION, mMediaPlayer.getDuration());
+
+        if (mMediaSession != null) {
+            mMediaSession.setMetadata(builder.build());
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    private void extractAudioMetadata() {
+        if (mRetriever == null || !mIsMusicMediaType) {
+            return;
+        }
+
+        mResources = mInstance.getResources();
+        mManager = (WindowManager) mInstance.getContext().getApplicationContext()
+                .getSystemService(Context.WINDOW_SERVICE);
+
+        byte[] album = mRetriever.getEmbeddedPicture();
+        if (album != null) {
+            Bitmap bitmap = BitmapFactory.decodeByteArray(album, 0, album.length);
+            mMusicAlbumDrawable = new BitmapDrawable(bitmap);
+
+            Palette.Builder builder = Palette.from(bitmap);
+            builder.generate(new Palette.PaletteAsyncListener() {
+                @Override
+                public void onGenerated(Palette palette) {
+                    mDominantColor = palette.getDominantColor(0);
+                    if (mMusicView != null) {
+                        mMusicView.setBackgroundColor(mDominantColor);
+                    }
+                }
+            });
+        } else {
+            mMusicAlbumDrawable = mResources.getDrawable(R.drawable.ic_default_album_image);
+        }
+
+        String title = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
+        if (title != null) {
+            mMusicTitleText = title;
+        } else {
+            mMusicTitleText = mResources.getString(R.string.mcv2_music_title_unknown_text);
+        }
+
+        String artist = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
+        if (artist != null) {
+            mMusicArtistText = artist;
+        } else {
+            mMusicArtistText = mResources.getString(R.string.mcv2_music_artist_unknown_text);
+        }
+
+        // Send title and artist string to MediaControlView2
+        MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+        builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mMusicTitleText);
+        builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, mMusicArtistText);
+        mMediaSession.setMetadata(builder.build());
+
+        // Display Embedded mode as default
+        mInstance.removeView(mSurfaceView);
+        mInstance.removeView(mTextureView);
+        inflateMusicView(R.layout.embedded_music);
+    }
+
+    private int retrieveOrientation() {
+        DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
+        int width = dm.widthPixels;
+        int height = dm.heightPixels;
+
+        return (height > width)
+                ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+                : ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+    }
+
+    private void inflateMusicView(int layoutId) {
+        mInstance.removeView(mMusicView);
+
+        LayoutInflater inflater = (LayoutInflater) mInstance.getContext()
+                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        View v = inflater.inflate(layoutId, null);
+        v.setBackgroundColor(mDominantColor);
+
+        ImageView albumView = v.findViewById(R.id.album);
+        if (albumView != null) {
+            albumView.setImageDrawable(mMusicAlbumDrawable);
+        }
+
+        TextView titleView = v.findViewById(R.id.title);
+        if (titleView != null) {
+            titleView.setText(mMusicTitleText);
+        }
+
+        TextView artistView = v.findViewById(R.id.artist);
+        if (artistView != null) {
+            artistView.setText(mMusicArtistText);
+        }
+
+        mMusicView = v;
+        mInstance.addView(mMusicView, 0);
+    }
+
+    MediaPlayer2EventCallback mMediaPlayer2Callback =
+            new MediaPlayer2EventCallback() {
+                @Override
+                public void onVideoSizeChanged(
+                        MediaPlayer2 mp, DataSourceDesc dsd, int width, int height) {
+                    if (DEBUG) {
+                        Log.d(TAG, "onVideoSizeChanged(): size: " + width + "/" + height);
+                    }
+                    mVideoWidth = mp.getVideoWidth();
+                    mVideoHeight = mp.getVideoHeight();
+                    if (DEBUG) {
+                        Log.d(TAG, "onVideoSizeChanged(): mVideoSize:" + mVideoWidth + "/"
+                                + mVideoHeight);
+                    }
+                    if (mVideoWidth != 0 && mVideoHeight != 0) {
+                        mInstance.requestLayout();
+                    }
+                }
+
+                @Override
+                public void onInfo(
+                        MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                    if (what == MediaPlayer2.MEDIA_INFO_METADATA_UPDATE) {
+                        extractTracks();
+                    } else if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                        this.onPrepared(mp, dsd);
+                    } else if (what == MediaPlayer2.MEDIA_INFO_PLAYBACK_COMPLETE) {
+                        this.onCompletion(mp, dsd);
+                    } else if (what == MediaPlayer2.MEDIA_INFO_BUFFERING_UPDATE) {
+                        this.onBufferingUpdate(mp, dsd, extra);
+                    }
+                }
+
+                @Override
+                public void onError(
+                        MediaPlayer2 mp, DataSourceDesc dsd, int frameworkErr, int implErr) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Error: " + frameworkErr + "," + implErr);
+                    }
+                    mCurrentState = STATE_ERROR;
+                    mTargetState = STATE_ERROR;
+                    updatePlaybackState();
+
+                    if (mMediaControlView != null) {
+                        mMediaControlView.setVisibility(View.GONE);
+                    }
+                }
+
+                @Override
+                public void onCallCompleted(
+                        MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                    if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+                        updatePlaybackState();
+                    }
+                }
+
+                @Override
+                public void onSubtitleData(MediaPlayer2 mp, DataSourceDesc dsd, SubtitleData data) {
+                    if (DEBUG) {
+                        Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex()
+                                + ", getCurrentPosition: " + mp.getCurrentPosition()
+                                + ", getStartTimeUs(): " + data.getStartTimeUs()
+                                + ", diff: "
+                                + (data.getStartTimeUs() / 1000 - mp.getCurrentPosition())
+                                + "ms, getDurationUs(): " + data.getDurationUs());
+
+                    }
+                    final int index = data.getTrackIndex();
+                    if (index != mSelectedSubtitleTrackIndex) {
+                        Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex()
+                                + ", selected track index: " + mSelectedSubtitleTrackIndex);
+                        return;
+                    }
+                    for (Pair<Integer, SubtitleTrack> p : mSubtitleTrackIndices) {
+                        if (p.first == index) {
+                            SubtitleTrack track = p.second;
+                            track.onData(data);
+                        }
+                    }
+                }
+
+                private void onPrepared(MediaPlayer2 mp, DataSourceDesc dsd) {
+                    if (DEBUG) {
+                        Log.d(TAG, "OnPreparedListener(). mCurrentState=" + mCurrentState
+                                + ", mTargetState=" + mTargetState);
+                    }
+                    mCurrentState = STATE_PREPARED;
+                    // Create and set playback state for MediaControlView2
+                    updatePlaybackState();
+
+                    if (mMediaSession != null) {
+                        extractTracks();
+                        extractMetadata();
+                        extractAudioMetadata();
+                    }
+
+                    if (mMediaControlView != null) {
+                        mMediaControlView.setEnabled(true);
+                    }
+                    int videoWidth = mp.getVideoWidth();
+                    int videoHeight = mp.getVideoHeight();
+
+                    // mSeekWhenPrepared may be changed after seekTo() call
+                    long seekToPosition = mSeekWhenPrepared;
+                    if (seekToPosition != 0) {
+                        mMediaController.getTransportControls().seekTo(seekToPosition);
+                    }
+
+                    if (videoWidth != 0 && videoHeight != 0) {
+                        if (videoWidth != mVideoWidth || videoHeight != mVideoHeight) {
+                            mVideoWidth = videoWidth;
+                            mVideoHeight = videoHeight;
+                            mInstance.requestLayout();
+                        }
+
+                        if (needToStart()) {
+                            mMediaController.getTransportControls().play();
+                        }
+
+                    } else {
+                        // We don't know the video size yet, but should start anyway.
+                        // The video size might be reported to us later.
+                        if (needToStart()) {
+                            mMediaController.getTransportControls().play();
+                        }
+                    }
+                    // Get and set duration and title values as MediaMetadata for MediaControlView2
+                    MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+
+                    builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
+                    builder.putLong(
+                            MediaMetadataCompat.METADATA_KEY_DURATION, mMediaPlayer.getDuration());
+
+                    if (mMediaSession != null) {
+                        mMediaSession.setMetadata(builder.build());
+
+                        if (mNeedUpdateMediaType) {
+                            mMediaSession.sendSessionEvent(
+                                    MediaControlView2.EVENT_UPDATE_MEDIA_TYPE_STATUS,
+                                    mMediaTypeData);
+                            mNeedUpdateMediaType = false;
+                        }
+                    }
+                }
+
+                @SuppressWarnings("deprecation")
+                private void onCompletion(MediaPlayer2 mp, DataSourceDesc dsd) {
+                    mCurrentState = STATE_PLAYBACK_COMPLETED;
+                    mTargetState = STATE_PLAYBACK_COMPLETED;
+                    updatePlaybackState();
+                    if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+                        mAudioManager.abandonAudioFocus(null);
+                    }
+                }
+
+                private void onBufferingUpdate(MediaPlayer2 mp, DataSourceDesc dsd, int percent) {
+                    mCurrentBufferPercentage = percent;
+                    updatePlaybackState();
+                }
+            };
+
+    private class MediaSessionCallback extends MediaSessionCompat.Callback {
+        @Override
+        public void onCommand(String command, Bundle args, ResultReceiver receiver) {
+            if (isRemotePlayback()) {
+                mRoutePlayer.onCommand(command, args, receiver);
+            } else {
+                switch (command) {
+                    case MediaControlView2.COMMAND_SHOW_SUBTITLE:
+                        int subtitleIndex = args.getInt(
+                                MediaControlView2.KEY_SELECTED_SUBTITLE_INDEX,
+                                INVALID_TRACK_INDEX);
+                        if (subtitleIndex != INVALID_TRACK_INDEX) {
+                            int subtitleTrackIndex = mSubtitleTrackIndices.get(subtitleIndex).first;
+                            if (subtitleTrackIndex != mSelectedSubtitleTrackIndex) {
+                                mSelectedSubtitleTrackIndex = subtitleTrackIndex;
+                                mInstance.setSubtitleEnabled(true);
+                            }
+                        }
+                        break;
+                    case MediaControlView2.COMMAND_HIDE_SUBTITLE:
+                        mInstance.setSubtitleEnabled(false);
+                        break;
+                    case MediaControlView2.COMMAND_SELECT_AUDIO_TRACK:
+                        int audioIndex = args.getInt(MediaControlView2.KEY_SELECTED_AUDIO_INDEX,
+                                INVALID_TRACK_INDEX);
+                        if (audioIndex != INVALID_TRACK_INDEX) {
+                            int audioTrackIndex = mAudioTrackIndices.get(audioIndex);
+                            if (audioTrackIndex != mSelectedAudioTrackIndex) {
+                                mSelectedAudioTrackIndex = audioTrackIndex;
+                                mMediaPlayer.selectTrack(mSelectedAudioTrackIndex);
+                            }
+                        }
+                        break;
+                    case MediaControlView2.COMMAND_SET_PLAYBACK_SPEED:
+                        float speed = args.getFloat(
+                                MediaControlView2.KEY_PLAYBACK_SPEED, INVALID_SPEED);
+                        if (speed != INVALID_SPEED && speed != mSpeed) {
+                            setSpeed(speed);
+                            mSpeed = speed;
+                        }
+                        break;
+                    case MediaControlView2.COMMAND_MUTE:
+                        mVolumeLevel = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+                        mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, 0);
+                        break;
+                    case MediaControlView2.COMMAND_UNMUTE:
+                        mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeLevel, 0);
+                        break;
+                }
+            }
+            showController();
+        }
+
+        @Override
+        public void onCustomAction(final String action, final Bundle extras) {
+            mCustomActionListenerRecord.first.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCustomActionListenerRecord.second.onCustomAction(action, extras);
+                }
+            });
+            showController();
+        }
+
+        @Override
+        public void onPlay() {
+            if (!isAudioGranted()) {
+                requestAudioFocus(mAudioFocusType);
+            }
+
+            if ((isInPlaybackState() && mCurrentView.hasAvailableSurface()) || mIsMusicMediaType) {
+                if (isRemotePlayback()) {
+                    mRoutePlayer.onPlay();
+                } else {
+                    applySpeed();
+                    mMediaPlayer.play();
+                    mCurrentState = STATE_PLAYING;
+                    updatePlaybackState();
+                }
+                mCurrentState = STATE_PLAYING;
+            }
+            mTargetState = STATE_PLAYING;
+            if (DEBUG) {
+                Log.d(TAG, "onPlay(). mCurrentState=" + mCurrentState
+                        + ", mTargetState=" + mTargetState);
+            }
+            showController();
+        }
+
+        @Override
+        public void onPause() {
+            if (isInPlaybackState()) {
+                if (isRemotePlayback()) {
+                    mRoutePlayer.onPlay();
+                    mCurrentState = STATE_PAUSED;
+                } else if (isPlaying()) {
+                    mMediaPlayer.pause();
+                    mCurrentState = STATE_PAUSED;
+                    updatePlaybackState();
+                }
+            }
+            mTargetState = STATE_PAUSED;
+            if (DEBUG) {
+                Log.d(TAG, "onPause(). mCurrentState=" + mCurrentState
+                        + ", mTargetState=" + mTargetState);
+            }
+            showController();
+        }
+
+        @Override
+        public void onSeekTo(long pos) {
+            if (isInPlaybackState()) {
+                if (isRemotePlayback()) {
+                    mRoutePlayer.onPlay();
+                } else {
+                    if (android.os.Build.VERSION.SDK_INT < 26) {
+                        mMediaPlayer.seekTo((int) pos);
+                    } else {
+                        mMediaPlayer.seekTo(pos, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+                    }
+                    mSeekWhenPrepared = 0;
+                }
+            } else {
+                mSeekWhenPrepared = pos;
+            }
+            showController();
+        }
+
+        @Override
+        public void onStop() {
+            if (isRemotePlayback()) {
+                mRoutePlayer.onPlay();
+            } else {
+                resetPlayer();
+            }
+            showController();
+        }
+    }
+}
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java b/media-widget/src/main/java/androidx/media/widget/impl/VideoViewInterface.java
similarity index 94%
rename from media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java
rename to media-widget/src/main/java/androidx/media/widget/impl/VideoViewInterface.java
index 81b40a9..a497e32 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java
+++ b/media-widget/src/main/java/androidx/media/widget/impl/VideoViewInterface.java
@@ -16,10 +16,10 @@
 
 package androidx.media.widget;
 
-import android.media.MediaPlayer;
 import android.view.View;
 
 import androidx.annotation.NonNull;
+import androidx.media.MediaPlayer2;
 
 interface VideoViewInterface {
     /**
@@ -29,10 +29,10 @@
      * @return true if the surface is successfully assigned, false if not. It will fail to assign
      *         if any of MediaPlayer or surface is unavailable.
      */
-    boolean assignSurfaceToMediaPlayer(MediaPlayer mp);
+    boolean assignSurfaceToMediaPlayer(MediaPlayer2 mp);
     void setSurfaceListener(SurfaceListener l);
     int getViewType();
-    void setMediaPlayer(MediaPlayer mp);
+    void setMediaPlayer(MediaPlayer2 mp);
 
     /**
      * Takes over oldView. It means that the MediaPlayer will start rendering on this view.
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoSurfaceViewWithMp1.java
similarity index 82%
rename from media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java
rename to media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoSurfaceViewWithMp1.java
index d417bd2..740d908 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java
+++ b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoSurfaceViewWithMp1.java
@@ -21,7 +21,6 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.media.MediaPlayer;
-import android.util.AttributeSet;
 import android.util.Log;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
@@ -31,39 +30,24 @@
 import androidx.annotation.RequiresApi;
 
 @RequiresApi(21)
-class VideoSurfaceView extends SurfaceView implements VideoViewInterface, SurfaceHolder.Callback {
-    private static final String TAG = "VideoSurfaceView";
-    private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+class VideoSurfaceViewWithMp1 extends SurfaceView
+        implements VideoViewInterfaceWithMp1, SurfaceHolder.Callback {
+    private static final String TAG = "VideoSurfaceViewWithMp1";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     private SurfaceHolder mSurfaceHolder = null;
     private SurfaceListener mSurfaceListener = null;
     private MediaPlayer mMediaPlayer;
     // A flag to indicate taking over other view should be proceed.
     private boolean mIsTakingOverOldView;
-    private VideoViewInterface mOldView;
+    private VideoViewInterfaceWithMp1 mOldView;
 
-
-    VideoSurfaceView(Context context) {
-        this(context, null);
-    }
-
-    VideoSurfaceView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        getHolder().addCallback(this);
-    }
-
-    @RequiresApi(21)
-    VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr,
-                            int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
+    VideoSurfaceViewWithMp1(Context context) {
+        super(context, null);
         getHolder().addCallback(this);
     }
 
     ////////////////////////////////////////////////////
-    // implements VideoViewInterface
+    // implements VideoViewInterfaceWithMp1
     ////////////////////////////////////////////////////
 
     @Override
@@ -95,7 +79,7 @@
     }
 
     @Override
-    public void takeOver(@NonNull VideoViewInterface oldView) {
+    public void takeOver(@NonNull VideoViewInterfaceWithMp1 oldView) {
         if (assignSurfaceToMediaPlayer(mMediaPlayer)) {
             ((View) oldView).setVisibility(GONE);
             mIsTakingOverOldView = false;
@@ -150,7 +134,6 @@
         }
     }
 
-    // TODO: Investigate the way to move onMeasure() code into FrameLayout.
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         int videoWidth = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoWidth();
@@ -194,10 +177,4 @@
             Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
         }
     }
-
-    @Override
-    public String toString() {
-        return "ViewType: SurfaceView / Visibility: " + getVisibility()
-                + " / surfaceHolder: " + mSurfaceHolder;
-    }
 }
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoTextureView.java b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoTextureViewWithMp1.java
similarity index 85%
copy from media-widget/src/main/java/androidx/media/widget/VideoTextureView.java
copy to media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoTextureViewWithMp1.java
index cdc833b..e20d78f 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoTextureView.java
+++ b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoTextureViewWithMp1.java
@@ -21,7 +21,6 @@
 import android.content.Context;
 import android.graphics.SurfaceTexture;
 import android.media.MediaPlayer;
-import android.util.AttributeSet;
 import android.util.Log;
 import android.view.Surface;
 import android.view.TextureView;
@@ -31,38 +30,25 @@
 import androidx.annotation.RequiresApi;
 
 @RequiresApi(21)
-class VideoTextureView extends TextureView
-        implements VideoViewInterface, TextureView.SurfaceTextureListener {
-    private static final String TAG = "VideoTextureView";
-    private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+class VideoTextureViewWithMp1 extends TextureView
+        implements VideoViewInterfaceWithMp1, TextureView.SurfaceTextureListener {
+    private static final String TAG = "VideoTextureViewWithMp1";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private Surface mSurface;
     private SurfaceListener mSurfaceListener;
     private MediaPlayer mMediaPlayer;
     // A flag to indicate taking over other view should be proceed.
     private boolean mIsTakingOverOldView;
-    private VideoViewInterface mOldView;
+    private VideoViewInterfaceWithMp1 mOldView;
 
-    VideoTextureView(Context context) {
-        this(context, null);
-    }
-
-    VideoTextureView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    VideoTextureView(Context context, AttributeSet attrs, int defStyleAttr) {
-        this(context, attrs, defStyleAttr, 0);
-    }
-
-    VideoTextureView(
-            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
+    VideoTextureViewWithMp1(Context context) {
+        super(context, null);
         setSurfaceTextureListener(this);
     }
 
     ////////////////////////////////////////////////////
-    // implements VideoViewInterface
+    // implements VideoViewInterfaceWithMp1
     ////////////////////////////////////////////////////
 
     @Override
@@ -94,7 +80,7 @@
     }
 
     @Override
-    public void takeOver(@NonNull VideoViewInterface oldView) {
+    public void takeOver(@NonNull VideoViewInterfaceWithMp1 oldView) {
         if (assignSurfaceToMediaPlayer(mMediaPlayer)) {
             ((View) oldView).setVisibility(GONE);
             mIsTakingOverOldView = false;
diff --git a/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoView2ImplApi28WithMp1.java b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoView2ImplApi28WithMp1.java
new file mode 100644
index 0000000..e0f6d36
--- /dev/null
+++ b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoView2ImplApi28WithMp1.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.widget;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnSubtitleDataListener;
+import android.media.SubtitleData;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Pair;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.media.subtitle.ClosedCaptionRenderer;
+import androidx.media.subtitle.SubtitleController;
+import androidx.media.subtitle.SubtitleTrack;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+
+/**
+ * Base implementation of VideoView2.
+ */
+@RequiresApi(28)
+class VideoView2ImplApi28WithMp1 extends VideoView2ImplBaseWithMp1 {
+    private static final String TAG = "VideoView2ImplApi28_1";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final int INVALID_TRACK_INDEX = -1;
+
+    private ArrayList<Pair<Integer, SubtitleTrack>> mSubtitleTrackIndices;
+    private SubtitleController mSubtitleController;
+
+    // selected video/audio/subtitle track index as MediaPlayer returns
+    private int mSelectedSubtitleTrackIndex;
+
+    private SubtitleView mSubtitleView;
+    private boolean mSubtitleEnabled;
+
+    @Override
+    public void initialize(
+            VideoView2 instance, Context context,
+            @Nullable AttributeSet attrs, int defStyleAttr) {
+        super.initialize(instance, context, attrs, defStyleAttr);
+        mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
+
+        LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
+                LayoutParams.MATCH_PARENT);
+        mSubtitleView = new SubtitleView(context);
+        mSubtitleView.setLayoutParams(params);
+        mSubtitleView.setBackgroundColor(0);
+        mInstance.addView(mSubtitleView);
+
+        mSubtitleEnabled = (attrs == null) || attrs.getAttributeBooleanValue(
+                "http://schemas.android.com/apk/res/android",
+                "enableSubtitle", false);
+    }
+
+    /**
+     * Shows or hides closed caption or subtitles if there is any.
+     * The first subtitle track will be chosen if there multiple subtitle tracks exist.
+     * Default behavior of VideoView2 is not showing subtitle.
+     * @param enable shows closed caption or subtitles if this value is true, or hides.
+     */
+    @Override
+    public void setSubtitleEnabled(boolean enable) {
+        if (enable != mSubtitleEnabled) {
+            selectOrDeselectSubtitle(enable);
+        }
+        mSubtitleEnabled = enable;
+    }
+
+    /**
+     * Returns true if showing subtitle feature is enabled or returns false.
+     * Although there is no subtitle track or closed caption, it can return true, if the feature
+     * has been enabled by {@link #setSubtitleEnabled}.
+     */
+    @Override
+    public boolean isSubtitleEnabled() {
+        return mSubtitleEnabled;
+    }
+
+    ///////////////////////////////////////////////////
+    // Protected or private methods
+    ///////////////////////////////////////////////////
+
+    /**
+     * Used in openVideo(). Setup MediaPlayer and related objects before calling prepare.
+     */
+    @Override
+    protected void setupMediaPlayer(Context context, Uri uri, Map<String, String> headers)
+            throws IOException {
+        super.setupMediaPlayer(context, uri, headers);
+
+        mSubtitleController = new SubtitleController(context);
+        mSubtitleController.registerRenderer(new ClosedCaptionRenderer(context));
+        mSubtitleController.setAnchor((SubtitleController.Anchor) mSubtitleView);
+
+        mMediaPlayer.setOnSubtitleDataListener(mSubtitleListener);
+    }
+
+    private void selectOrDeselectSubtitle(boolean select) {
+        if (!isInPlaybackState()) {
+            return;
+        }
+        if (select) {
+            if (mSubtitleTrackIndices.size() > 0) {
+                mSelectedSubtitleTrackIndex = mSubtitleTrackIndices.get(0).first;
+                mSubtitleController.selectTrack(mSubtitleTrackIndices.get(0).second);
+                mMediaPlayer.selectTrack(mSelectedSubtitleTrackIndex);
+                mSubtitleView.setVisibility(View.VISIBLE);
+            }
+        } else {
+            if (mSelectedSubtitleTrackIndex != INVALID_TRACK_INDEX) {
+                mMediaPlayer.deselectTrack(mSelectedSubtitleTrackIndex);
+                mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
+                mSubtitleView.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    @Override
+    protected void extractTracks() {
+        MediaPlayer.TrackInfo[] trackInfos = mMediaPlayer.getTrackInfo();
+        mVideoTrackIndices = new ArrayList<>();
+        mAudioTrackIndices = new ArrayList<>();
+        mSubtitleTrackIndices = new ArrayList<>();
+        mSubtitleController.reset();
+        for (int i = 0; i < trackInfos.length; ++i) {
+            int trackType = trackInfos[i].getTrackType();
+            if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO) {
+                mVideoTrackIndices.add(i);
+            } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) {
+                mAudioTrackIndices.add(i);
+            } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
+                SubtitleTrack track = mSubtitleController.addTrack(trackInfos[i].getFormat());
+                if (track != null) {
+                    mSubtitleTrackIndices.add(new Pair<>(i, track));
+                }
+            }
+        }
+        // Select first tracks as default
+        if (mVideoTrackIndices.size() > 0) {
+            mSelectedVideoTrackIndex = 0;
+        }
+        if (mAudioTrackIndices.size() > 0) {
+            mSelectedAudioTrackIndex = 0;
+        }
+
+        Bundle data = new Bundle();
+        data.putInt(MediaControlView2.KEY_VIDEO_TRACK_COUNT, mVideoTrackIndices.size());
+        data.putInt(MediaControlView2.KEY_AUDIO_TRACK_COUNT, mAudioTrackIndices.size());
+        data.putInt(MediaControlView2.KEY_SUBTITLE_TRACK_COUNT, mSubtitleTrackIndices.size());
+        if (mSubtitleTrackIndices.size() > 0) {
+            selectOrDeselectSubtitle(mSubtitleEnabled);
+        }
+        mMediaSession.sendSessionEvent(MediaControlView2.EVENT_UPDATE_TRACK_STATUS, data);
+    }
+
+    private OnSubtitleDataListener mSubtitleListener =
+            new OnSubtitleDataListener() {
+                @Override
+                public void onSubtitleData(MediaPlayer mp, SubtitleData data) {
+                    if (DEBUG) {
+                        Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex()
+                                + ", getCurrentPosition: " + mp.getCurrentPosition()
+                                + ", getStartTimeUs(): " + data.getStartTimeUs()
+                                + ", diff: "
+                                + (data.getStartTimeUs() / 1000 - mp.getCurrentPosition())
+                                + "ms, getDurationUs(): " + data.getDurationUs());
+
+                    }
+                    final int index = data.getTrackIndex();
+                    if (index != mSelectedSubtitleTrackIndex) {
+                        Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex()
+                                + ", selected track index: " + mSelectedSubtitleTrackIndex);
+                        return;
+                    }
+                    for (Pair<Integer, SubtitleTrack> p : mSubtitleTrackIndices) {
+                        if (p.first == index) {
+                            SubtitleTrack track = p.second;
+                            track.onData(data);
+                        }
+                    }
+                }
+            };
+
+    @Override
+    protected void doShowSubtitleCommand(Bundle args) {
+        int subtitleIndex = args.getInt(
+                MediaControlView2.KEY_SELECTED_SUBTITLE_INDEX,
+                INVALID_TRACK_INDEX);
+        if (subtitleIndex != INVALID_TRACK_INDEX) {
+            int subtitleTrackIndex = mSubtitleTrackIndices.get(subtitleIndex).first;
+            if (subtitleTrackIndex != mSelectedSubtitleTrackIndex) {
+                mSelectedSubtitleTrackIndex = subtitleTrackIndex;
+                mInstance.setSubtitleEnabled(true);
+            }
+        }
+    }
+
+    @Override
+    protected void doHideSubtitleCommand() {
+        mInstance.setSubtitleEnabled(false);
+    }
+}
diff --git a/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoView2ImplBaseWithMp1.java b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoView2ImplBaseWithMp1.java
new file mode 100644
index 0000000..8fdb1b1
--- /dev/null
+++ b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoView2ImplBaseWithMp1.java
@@ -0,0 +1,1473 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Point;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.media.MediaMetadataRetriever;
+import android.media.MediaPlayer;
+import android.media.PlaybackParams;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaControllerCompat.PlaybackInfo;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.media.AudioAttributesCompat;
+import androidx.media.DataSourceDesc;
+import androidx.media.MediaItem2;
+import androidx.media.MediaMetadata2;
+import androidx.media.SessionToken2;
+import androidx.mediarouter.media.MediaControlIntent;
+import androidx.mediarouter.media.MediaItemStatus;
+import androidx.mediarouter.media.MediaRouteSelector;
+import androidx.mediarouter.media.MediaRouter;
+import androidx.palette.graphics.Palette;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Base implementation of VideoView2.
+ */
+@RequiresApi(21) // TODO correct minSdk API use incompatibilities and remove before release.
+class VideoView2ImplBaseWithMp1
+        implements VideoView2Impl, VideoViewInterfaceWithMp1.SurfaceListener {
+    private static final String TAG = "VideoView2ImplBase_1";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final long DEFAULT_SHOW_CONTROLLER_INTERVAL_MS = 2000;
+
+    private static final int STATE_ERROR = -1;
+    private static final int STATE_IDLE = 0;
+    private static final int STATE_PREPARING = 1;
+    private static final int STATE_PREPARED = 2;
+    private static final int STATE_PLAYING = 3;
+    private static final int STATE_PAUSED = 4;
+    private static final int STATE_PLAYBACK_COMPLETED = 5;
+
+    private static final int INVALID_TRACK_INDEX = -1;
+    private static final float INVALID_SPEED = 0f;
+
+    private static final int SIZE_TYPE_EMBEDDED = 0;
+    private static final int SIZE_TYPE_FULL = 1;
+    private static final int SIZE_TYPE_MINIMAL = 2;
+
+    private AccessibilityManager mAccessibilityManager;
+    private AudioManager mAudioManager;
+    private AudioAttributes mAudioAttributes;
+    private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain
+    private boolean mAudioFocused = false;
+
+    private Pair<Executor, VideoView2.OnCustomActionListener> mCustomActionListenerRecord;
+    private VideoView2.OnViewTypeChangedListener mViewTypeChangedListener;
+
+    private VideoViewInterfaceWithMp1 mCurrentView;
+    private VideoTextureViewWithMp1 mTextureView;
+    private VideoSurfaceViewWithMp1 mSurfaceView;
+
+    protected MediaPlayer mMediaPlayer;
+    private DataSourceDesc mDsd;
+    private Uri mUri;
+    private Map<String, String> mHeaders;
+    private MediaControlView2 mMediaControlView;
+    protected MediaSessionCompat mMediaSession;
+    private MediaControllerCompat mMediaController;
+    private MediaMetadata2 mMediaMetadata;
+    private MediaMetadataRetriever mRetriever;
+    private boolean mNeedUpdateMediaType;
+    private Bundle mMediaTypeData;
+    private String mTitle;
+
+    private WindowManager mManager;
+    private Resources mResources;
+    private View mMusicView;
+    private Drawable mMusicAlbumDrawable;
+    private String mMusicTitleText;
+    private String mMusicArtistText;
+    private int mPrevWidth;
+    private int mPrevHeight;
+    private int mDominantColor;
+    private int mSizeType;
+
+    private PlaybackStateCompat.Builder mStateBuilder;
+    private List<PlaybackStateCompat.CustomAction> mCustomActionList;
+
+    private int mTargetState = STATE_IDLE;
+    private int mCurrentState = STATE_IDLE;
+    private int mCurrentBufferPercentage;
+    private long mSeekWhenPrepared;  // recording the seek position while preparing
+
+    private int mVideoWidth;
+    private int mVideoHeight;
+
+    protected ArrayList<Integer> mVideoTrackIndices;
+    protected ArrayList<Integer> mAudioTrackIndices;
+
+    // selected video/audio/subtitle track index as MediaPlayer returns
+    protected int mSelectedVideoTrackIndex;
+    protected int mSelectedAudioTrackIndex;
+
+    private float mSpeed;
+    private float mFallbackSpeed;  // keep the original speed before 'pause' is called.
+    private float mVolumeLevelFloat;
+    private int mVolumeLevel;
+    protected VideoView2 mInstance;
+
+    private long mShowControllerIntervalMs;
+
+    private MediaRouter mMediaRouter;
+    private MediaRouteSelector mRouteSelector;
+    private MediaRouter.RouteInfo mRoute;
+    private RoutePlayer mRoutePlayer;
+
+    private final MediaRouter.Callback mRouterCallback = new MediaRouter.Callback() {
+        @Override
+        public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
+            if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
+                // Stop local playback (if necessary)
+                resetPlayer();
+                mRoute = route;
+                mRoutePlayer = new RoutePlayer(mInstance.getContext(), route);
+                mRoutePlayer.setPlayerEventCallback(new RoutePlayer.PlayerEventCallback() {
+                    @Override
+                    public void onPlayerStateChanged(MediaItemStatus itemStatus) {
+                        PlaybackStateCompat.Builder psBuilder = new PlaybackStateCompat.Builder();
+                        psBuilder.setActions(RoutePlayer.PLAYBACK_ACTIONS);
+                        long position = itemStatus.getContentPosition();
+                        switch (itemStatus.getPlaybackState()) {
+                            case MediaItemStatus.PLAYBACK_STATE_PENDING:
+                                psBuilder.setState(PlaybackStateCompat.STATE_NONE, position, 0);
+                                mCurrentState = STATE_IDLE;
+                                break;
+                            case MediaItemStatus.PLAYBACK_STATE_PLAYING:
+                                psBuilder.setState(PlaybackStateCompat.STATE_PLAYING, position, 1);
+                                mCurrentState = STATE_PLAYING;
+                                break;
+                            case MediaItemStatus.PLAYBACK_STATE_PAUSED:
+                                psBuilder.setState(PlaybackStateCompat.STATE_PAUSED, position, 0);
+                                mCurrentState = STATE_PAUSED;
+                                break;
+                            case MediaItemStatus.PLAYBACK_STATE_BUFFERING:
+                                psBuilder.setState(
+                                        PlaybackStateCompat.STATE_BUFFERING, position, 0);
+                                mCurrentState = STATE_PAUSED;
+                                break;
+                            case MediaItemStatus.PLAYBACK_STATE_FINISHED:
+                                psBuilder.setState(PlaybackStateCompat.STATE_STOPPED, position, 0);
+                                mCurrentState = STATE_PLAYBACK_COMPLETED;
+                                break;
+                        }
+
+                        PlaybackStateCompat pbState = psBuilder.build();
+                        mMediaSession.setPlaybackState(pbState);
+
+                        MediaMetadataCompat.Builder mmBuilder = new MediaMetadataCompat.Builder();
+                        mmBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION,
+                                itemStatus.getContentDuration());
+                        mMediaSession.setMetadata(mmBuilder.build());
+                    }
+                });
+                // Start remote playback (if necessary)
+                // TODO: b/77556429
+                mRoutePlayer.openVideo(mUri);
+            }
+        }
+
+        @Override
+        public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route, int reason) {
+            if (mRoute != null && mRoutePlayer != null) {
+                mRoutePlayer.release();
+                mRoutePlayer = null;
+            }
+            if (mRoute == route) {
+                mRoute = null;
+            }
+            if (reason != MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
+                // TODO: Resume local playback  (if necessary)
+                // TODO: b/77556429
+                openVideo(mUri, mHeaders);
+            }
+        }
+    };
+
+    @Override
+    public void initialize(
+            VideoView2 instance, Context context,
+            @Nullable AttributeSet attrs, int defStyleAttr) {
+        mInstance = instance;
+
+        mVideoWidth = 0;
+        mVideoHeight = 0;
+        mSpeed = 1.0f;
+        mFallbackSpeed = mSpeed;
+        mShowControllerIntervalMs = DEFAULT_SHOW_CONTROLLER_INTERVAL_MS;
+
+        mAccessibilityManager = (AccessibilityManager) context.getSystemService(
+                Context.ACCESSIBILITY_SERVICE);
+
+        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        mAudioAttributes = new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_MEDIA)
+                .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE).build();
+
+        mInstance.setFocusable(true);
+        mInstance.setFocusableInTouchMode(true);
+        mInstance.requestFocus();
+
+        mTextureView = new VideoTextureViewWithMp1(context);
+        mSurfaceView = new VideoSurfaceViewWithMp1(context);
+        LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
+                LayoutParams.MATCH_PARENT);
+        mTextureView.setLayoutParams(params);
+        mSurfaceView.setLayoutParams(params);
+        mTextureView.setSurfaceListener(this);
+        mSurfaceView.setSurfaceListener(this);
+
+        mInstance.addView(mTextureView);
+        mInstance.addView(mSurfaceView);
+
+        boolean enableControlView = (attrs == null) || attrs.getAttributeBooleanValue(
+                "http://schemas.android.com/apk/res/android",
+                "enableControlView", true);
+        if (enableControlView) {
+            mMediaControlView = new MediaControlView2(context);
+        }
+
+        // Choose surface view by default
+        int viewType = (attrs == null) ? VideoView2.VIEW_TYPE_SURFACEVIEW
+                : attrs.getAttributeIntValue(
+                "http://schemas.android.com/apk/res/android",
+                "viewType", VideoView2.VIEW_TYPE_SURFACEVIEW);
+        if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) {
+            Log.d(TAG, "viewType attribute is surfaceView.");
+            mTextureView.setVisibility(View.GONE);
+            mSurfaceView.setVisibility(View.VISIBLE);
+            mCurrentView = mSurfaceView;
+        } else if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
+            Log.d(TAG, "viewType attribute is textureView.");
+            mTextureView.setVisibility(View.VISIBLE);
+            mSurfaceView.setVisibility(View.GONE);
+            mCurrentView = mTextureView;
+        }
+
+        MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
+        builder.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+        builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
+        builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+        mRouteSelector = builder.build();
+    }
+
+    /**
+     * Sets MediaControlView2 instance. It will replace the previously assigned MediaControlView2
+     * instance if any.
+     *
+     * @param mediaControlView a media control view2 instance.
+     * @param intervalMs a time interval in milliseconds until VideoView2 hides MediaControlView2.
+     */
+    @Override
+    public void setMediaControlView2(MediaControlView2 mediaControlView, long intervalMs) {
+        mMediaControlView = mediaControlView;
+        mShowControllerIntervalMs = intervalMs;
+        mMediaControlView.setRouteSelector(mRouteSelector);
+
+        if (mInstance.isAttachedToWindow()) {
+            attachMediaControlView();
+        }
+    }
+
+    /**
+     * Returns MediaControlView2 instance which is currently attached to VideoView2 by default or by
+     * {@link #setMediaControlView2} method.
+     */
+    @Override
+    public MediaControlView2 getMediaControlView2() {
+        return mMediaControlView;
+    }
+
+    /**
+     * Sets MediaMetadata2 instance. It will replace the previously assigned MediaMetadata2 instance
+     * if any.
+     *
+     * @param metadata a MediaMetadata2 instance.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void setMediaMetadata(MediaMetadata2 metadata) {
+      //mProvider.setMediaMetadata_impl(metadata);
+    }
+
+    /**
+     * Returns MediaMetadata2 instance which is retrieved from MediaPlayer inside VideoView2 by
+     * default or by {@link #setMediaMetadata} method.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public MediaMetadata2 getMediaMetadata() {
+        return mMediaMetadata;
+    }
+
+    /**
+     * Returns MediaController instance which is connected with MediaSession that VideoView2 is
+     * using. This method should be called when VideoView2 is attached to window, or it throws
+     * IllegalStateException, since internal MediaSession instance is not available until
+     * this view is attached to window. Please check {@link View#isAttachedToWindow}
+     * before calling this method.
+     *
+     * @throws IllegalStateException if interal MediaSession is not created yet.
+     * @hide  TODO: remove
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public MediaControllerCompat getMediaController() {
+        if (mMediaSession == null) {
+            throw new IllegalStateException("MediaSession instance is not available.");
+        }
+        return mMediaController;
+    }
+
+    /**
+     * Returns {@link SessionToken2} so that developers create their own
+     * {@link androidx.media.MediaController2} instance. This method should be called when
+     * VideoView2 is attached to window, or it throws IllegalStateException.
+     *
+     * @throws IllegalStateException if interal MediaSession is not created yet.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public SessionToken2 getMediaSessionToken() {
+        //return mProvider.getMediaSessionToken_impl();
+        return null;
+    }
+
+    /**
+     * Shows or hides closed caption or subtitles if there is any.
+     * The first subtitle track will be chosen if there multiple subtitle tracks exist.
+     * Default behavior of VideoView2 is not showing subtitle.
+     * @param enable shows closed caption or subtitles if this value is true, or hides.
+     */
+    @Override
+    public void setSubtitleEnabled(boolean enable) {
+        // No-op on API < 28
+    }
+
+    /**
+     * Returns true if showing subtitle feature is enabled or returns false.
+     * Although there is no subtitle track or closed caption, it can return true, if the feature
+     * has been enabled by {@link #setSubtitleEnabled}.
+     */
+    @Override
+    public boolean isSubtitleEnabled() {
+        // Not supported on API < 28
+        return false;
+    }
+
+    /**
+     * Sets playback speed.
+     *
+     * It is expressed as a multiplicative factor, where normal speed is 1.0f. If it is less than
+     * or equal to zero, it will be just ignored and nothing will be changed. If it exceeds the
+     * maximum speed that internal engine supports, system will determine best handling or it will
+     * be reset to the normal speed 1.0f.
+     * @param speed the playback speed. It should be positive.
+     */
+    @Override
+    public void setSpeed(float speed) {
+        if (speed <= 0.0f) {
+            Log.e(TAG, "Unsupported speed (" + speed + ") is ignored.");
+            return;
+        }
+        mSpeed = speed;
+        if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
+            applySpeed();
+        }
+        updatePlaybackState();
+    }
+
+    /**
+     * Returns playback speed.
+     *
+     * It returns the same value that has been set by {@link #setSpeed}, if it was available value.
+     * If {@link #setSpeed} has not been called before, then the normal speed 1.0f will be returned.
+     */
+    @Override
+    public float getSpeed() {
+        return mSpeed;
+    }
+
+    /**
+     * Sets which type of audio focus will be requested during the playback, or configures playback
+     * to not request audio focus. Valid values for focus requests are
+     * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT},
+     * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and
+     * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use
+     * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be
+     * requested when playback starts. You can for instance use this when playing a silent animation
+     * through this class, and you don't want to affect other audio applications playing in the
+     * background.
+     *
+     * @param focusGain the type of audio focus gain that will be requested, or
+     *                  {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during
+     *                  playback.
+     */
+    @Override
+    public void setAudioFocusRequest(int focusGain) {
+        if (focusGain != AudioManager.AUDIOFOCUS_NONE
+                && focusGain != AudioManager.AUDIOFOCUS_GAIN
+                && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
+                && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+                && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) {
+            throw new IllegalArgumentException("Illegal audio focus type " + focusGain);
+        }
+        mAudioFocusType = focusGain;
+    }
+
+    /**
+     * Sets the {@link AudioAttributesCompat} to be used during the playback of the video.
+     *
+     * @param attributes non-null <code>AudioAttributesCompat</code>.
+     */
+    @Override
+    public void setAudioAttributes(@NonNull AudioAttributesCompat attributes) {
+        if (attributes == null) {
+            throw new IllegalArgumentException("Illegal null AudioAttributesCompat");
+        }
+        mAudioAttributes = (AudioAttributes) attributes.unwrap();
+    }
+
+    /**
+     * Sets video path.
+     *
+     * @param path the path of the video.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void setVideoPath(String path) {
+        setVideoUri(Uri.parse(path));
+    }
+
+    /**
+     * Sets video URI.
+     *
+     * @param uri the URI of the video.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void setVideoUri(Uri uri) {
+        setVideoUri(uri, null);
+    }
+
+    /**
+     * Sets video URI using specific headers.
+     *
+     * @param uri     the URI of the video.
+     * @param headers the headers for the URI request.
+     *                Note that the cross domain redirection is allowed by default, but that can be
+     *                changed with key/value pairs through the headers parameter with
+     *                "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
+     *                to disallow or allow cross domain redirection.
+     */
+    @Override
+    public void setVideoUri(Uri uri, @Nullable Map<String, String> headers) {
+        mSeekWhenPrepared = 0;
+        openVideo(uri, headers);
+    }
+
+    /**
+     * Sets {@link MediaItem2} object to render using VideoView2. Alternative way to set media
+     * object to VideoView2 is {@link #setDataSource}.
+     * @param mediaItem the MediaItem2 to play
+     * @see #setDataSource
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void setMediaItem(@NonNull MediaItem2 mediaItem) {
+    }
+
+    /**
+     * Sets {@link DataSourceDesc} object to render using VideoView2.
+     * @param dataSource the {@link DataSourceDesc} object to play.
+     * @see #setMediaItem
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void setDataSource(@NonNull DataSourceDesc dataSource) {
+    }
+
+    /**
+     * Selects which view will be used to render video between SurfacView and TextureView.
+     *
+     * @param viewType the view type to render video
+     * <ul>
+     * <li>{@link #VideoView2.VIEW_TYPE_SURFACEVIEW}
+     * <li>{@link #VideoView2.VIEW_TYPE_TEXTUREVIEW}
+     * </ul>
+     */
+    @Override
+    public void setViewType(@VideoView2.ViewType int viewType) {
+        if (viewType == mCurrentView.getViewType()) {
+            return;
+        }
+        VideoViewInterfaceWithMp1 targetView;
+        if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
+            Log.d(TAG, "switching to TextureView");
+            targetView = mTextureView;
+        } else if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) {
+            Log.d(TAG, "switching to SurfaceView");
+            targetView = mSurfaceView;
+        } else {
+            throw new IllegalArgumentException("Unknown view type: " + viewType);
+        }
+        ((View) targetView).setVisibility(View.VISIBLE);
+        targetView.takeOver(mCurrentView);
+        mInstance.requestLayout();
+    }
+
+    /**
+     * Returns view type.
+     *
+     * @return view type. See {@see setViewType}.
+     */
+    @Override
+    @VideoView2.ViewType
+    public int getViewType() {
+        return mCurrentView.getViewType();
+    }
+
+    /**
+     * Sets custom actions which will be shown as custom buttons in {@link MediaControlView2}.
+     *
+     * @param actionList A list of {@link PlaybackStateCompat.CustomAction}. The return value of
+     *                   {@link PlaybackStateCompat.CustomAction#getIcon()} will be used to draw
+     *                   buttons in {@link MediaControlView2}.
+     * @param executor executor to run callbacks on.
+     * @param listener A listener to be called when a custom button is clicked.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void setCustomActions(List<PlaybackStateCompat.CustomAction> actionList,
+            Executor executor, VideoView2.OnCustomActionListener listener) {
+        mCustomActionList = actionList;
+        mCustomActionListenerRecord = new Pair<>(executor, listener);
+
+        // Create a new playback builder in order to clear existing the custom actions.
+        mStateBuilder = null;
+        updatePlaybackState();
+    }
+
+    /**
+     * Registers a callback to be invoked when a view type change is done.
+     * {@see #setViewType(int)}
+     * @param l The callback that will be run
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void setOnViewTypeChangedListener(VideoView2.OnViewTypeChangedListener l) {
+        mViewTypeChangedListener = l;
+    }
+
+    @Override
+    public void onAttachedToWindowImpl() {
+        // Create MediaSession
+        mMediaSession = new MediaSessionCompat(mInstance.getContext(), "VideoView2MediaSession");
+        mMediaSession.setCallback(new MediaSessionCallback());
+        mMediaSession.setActive(true);
+        mMediaController = mMediaSession.getController();
+        attachMediaControlView();
+        if (mCurrentState == STATE_PREPARED) {
+            extractTracks();
+            extractMetadata();
+            extractAudioMetadata();
+            if (mNeedUpdateMediaType) {
+                mMediaSession.sendSessionEvent(
+                        MediaControlView2.EVENT_UPDATE_MEDIA_TYPE_STATUS,
+                        mMediaTypeData);
+                mNeedUpdateMediaType = false;
+            }
+        }
+
+        mMediaRouter = MediaRouter.getInstance(mInstance.getContext());
+        mMediaRouter.setMediaSessionCompat(mMediaSession);
+        mMediaRouter.addCallback(mRouteSelector, mRouterCallback,
+                MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+    }
+
+    @Override
+    public void onDetachedFromWindowImpl() {
+        mMediaSession.release();
+        mMediaSession = null;
+        mMediaController = null;
+    }
+
+    @Override
+    public void onTouchEventImpl(MotionEvent ev) {
+        if (DEBUG) {
+            Log.d(TAG, "onTouchEvent(). mCurrentState=" + mCurrentState
+                    + ", mTargetState=" + mTargetState);
+        }
+        if (ev.getAction() == MotionEvent.ACTION_UP && mMediaControlView != null) {
+            if (!isMusicMediaType() || mSizeType != SIZE_TYPE_FULL) {
+                toggleMediaControlViewVisibility();
+            }
+        }
+    }
+
+    @Override
+    public void onTrackballEventImpl(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_UP && mMediaControlView != null) {
+            if (!isMusicMediaType() || mSizeType != SIZE_TYPE_FULL) {
+                toggleMediaControlViewVisibility();
+            }
+        }
+    }
+
+    @Override
+    public void onMeasureImpl(int widthMeasureSpec, int heightMeasureSpec) {
+        if (isMusicMediaType()) {
+            int currWidth = mInstance.getMeasuredWidth();
+            int currHeight = mInstance.getMeasuredHeight();
+            if (mPrevWidth != currWidth || mPrevHeight != currHeight) {
+                Point screenSize = new Point();
+                mManager.getDefaultDisplay().getSize(screenSize);
+                int screenWidth = screenSize.x;
+                int screenHeight = screenSize.y;
+
+                if (currWidth == screenWidth && currHeight == screenHeight) {
+                    int orientation = retrieveOrientation();
+                    if (orientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
+                        inflateMusicView(R.layout.full_landscape_music);
+                    } else {
+                        inflateMusicView(R.layout.full_portrait_music);
+                    }
+
+                    if (mSizeType != SIZE_TYPE_FULL) {
+                        mSizeType = SIZE_TYPE_FULL;
+                        // Remove existing mFadeOut callback
+                        mMediaControlView.removeCallbacks(mFadeOut);
+                        mMediaControlView.setVisibility(View.VISIBLE);
+                    }
+                } else {
+                    if (mSizeType != SIZE_TYPE_EMBEDDED) {
+                        mSizeType = SIZE_TYPE_EMBEDDED;
+                        inflateMusicView(R.layout.embedded_music);
+                        // Add new mFadeOut callback
+                        mMediaControlView.postDelayed(mFadeOut, mShowControllerIntervalMs);
+                    }
+                }
+                mPrevWidth = currWidth;
+                mPrevHeight = currHeight;
+            }
+        }
+    }
+
+    ///////////////////////////////////////////////////
+    // Implements VideoViewInterfaceWithMp1.SurfaceListener
+    ///////////////////////////////////////////////////
+
+    @Override
+    public void onSurfaceCreated(View view, int width, int height) {
+        if (DEBUG) {
+            Log.d(TAG, "onSurfaceCreated(). mCurrentState=" + mCurrentState
+                    + ", mTargetState=" + mTargetState + ", width/height: " + width + "/" + height
+                    + ", " + view.toString());
+        }
+        if (needToStart()) {
+            mMediaController.getTransportControls().play();
+        }
+    }
+
+    @Override
+    public void onSurfaceDestroyed(View view) {
+        if (DEBUG) {
+            Log.d(TAG, "onSurfaceDestroyed(). mCurrentState=" + mCurrentState
+                    + ", mTargetState=" + mTargetState + ", " + view.toString());
+        }
+    }
+
+    @Override
+    public void onSurfaceChanged(View view, int width, int height) {
+        if (DEBUG) {
+            Log.d(TAG, "onSurfaceChanged(). width/height: " + width + "/" + height
+                    + ", " + view.toString());
+        }
+    }
+
+    @Override
+    public void onSurfaceTakeOverDone(VideoViewInterfaceWithMp1 view) {
+        if (DEBUG) {
+            Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view);
+        }
+        mCurrentView = view;
+        if (mViewTypeChangedListener != null) {
+            mViewTypeChangedListener.onViewTypeChanged(mInstance, view.getViewType());
+        }
+        if (needToStart()) {
+            mMediaController.getTransportControls().play();
+        }
+    }
+
+    ///////////////////////////////////////////////////
+    // Protected or private methods
+    ///////////////////////////////////////////////////
+
+    private void attachMediaControlView() {
+        // Get MediaController from MediaSession and set it inside MediaControlView
+        mMediaControlView.setController(mMediaSession.getController());
+
+        LayoutParams params =
+                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+        mInstance.addView(mMediaControlView, params);
+    }
+
+    protected boolean isInPlaybackState() {
+        return (mMediaPlayer != null || mRoutePlayer != null)
+                && mCurrentState != STATE_ERROR
+                && mCurrentState != STATE_IDLE
+                && mCurrentState != STATE_PREPARING;
+    }
+
+    private boolean needToStart() {
+        return (mMediaPlayer != null || mRoutePlayer != null)
+                && isAudioGranted()
+                && isWaitingPlayback();
+    }
+
+    private boolean isMusicMediaType() {
+        return mVideoTrackIndices != null && mVideoTrackIndices.size() == 0;
+    }
+
+    private boolean isWaitingPlayback() {
+        return mCurrentState != STATE_PLAYING && mTargetState == STATE_PLAYING;
+    }
+
+    private boolean isAudioGranted() {
+        return mAudioFocused || mAudioFocusType == AudioManager.AUDIOFOCUS_NONE;
+    }
+
+    private AudioManager.OnAudioFocusChangeListener mAudioFocusListener =
+            new AudioManager.OnAudioFocusChangeListener() {
+        @Override
+        public void onAudioFocusChange(int focusChange) {
+            switch (focusChange) {
+                case AudioManager.AUDIOFOCUS_GAIN:
+                    mAudioFocused = true;
+                    if (needToStart()) {
+                        mMediaController.getTransportControls().play();
+                    }
+                    break;
+                case AudioManager.AUDIOFOCUS_LOSS:
+                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+                    mAudioFocused = false;
+                    if (isInPlaybackState() && mMediaPlayer.isPlaying()) {
+                        mMediaController.getTransportControls().pause();
+                    } else {
+                        mTargetState = STATE_PAUSED;
+                    }
+            }
+        }
+    };
+
+    @SuppressWarnings("deprecation")
+    private void requestAudioFocus(int focusType) {
+        int result;
+        if (android.os.Build.VERSION.SDK_INT >= 26) {
+            AudioFocusRequest focusRequest;
+            focusRequest = new AudioFocusRequest.Builder(focusType)
+                    .setAudioAttributes(mAudioAttributes)
+                    .setOnAudioFocusChangeListener(mAudioFocusListener)
+                    .build();
+            result = mAudioManager.requestAudioFocus(focusRequest);
+        } else {
+            result = mAudioManager.requestAudioFocus(mAudioFocusListener,
+                    AudioManager.STREAM_MUSIC,
+                    focusType);
+        }
+        if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
+            mAudioFocused = false;
+        } else if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+            mAudioFocused = true;
+        } else if (result == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
+            mAudioFocused = false;
+        }
+    }
+
+    // Creates a MediaPlayer instance and prepare playback.
+    private void openVideo(Uri uri, Map<String, String> headers) {
+        resetPlayer();
+        mUri = uri;
+        if (isRemotePlayback()) {
+            // TODO: b/77556429
+            mRoutePlayer.openVideo(uri);
+            return;
+        }
+
+        try {
+            Log.d(TAG, "openVideo(): creating new MediaPlayer instance.");
+            mMediaPlayer = new MediaPlayer();
+            final Context context = mInstance.getContext();
+            setupMediaPlayer(context, uri, headers);
+
+            // we don't set the target state here either, but preserve the
+            // target state that was there before.
+            mCurrentState = STATE_PREPARING;
+            mMediaPlayer.prepareAsync();
+
+            // Save file name as title since the file may not have a title Metadata.
+            mTitle = uri.getPath();
+            String scheme = uri.getScheme();
+            if (scheme != null && scheme.equals("file")) {
+                mTitle = uri.getLastPathSegment();
+                mRetriever = new MediaMetadataRetriever();
+                mRetriever.setDataSource(context, uri);
+            }
+
+            if (DEBUG) {
+                Log.d(TAG, "openVideo(). mCurrentState=" + mCurrentState
+                        + ", mTargetState=" + mTargetState);
+            }
+        } catch (IOException | IllegalArgumentException ex) {
+            Log.w(TAG, "Unable to open content: " + uri, ex);
+            mCurrentState = STATE_ERROR;
+            mTargetState = STATE_ERROR;
+            mErrorListener.onError(mMediaPlayer,
+                    MediaPlayer.MEDIA_ERROR_UNKNOWN, MediaPlayer.MEDIA_ERROR_IO);
+        }
+    }
+
+    /**
+     * Used in openVideo(). Setup MediaPlayer and related objects before calling prepare.
+     */
+    protected void setupMediaPlayer(Context context, Uri uri, Map<String, String> headers)
+            throws IOException {
+        mSurfaceView.setMediaPlayer(mMediaPlayer);
+        mTextureView.setMediaPlayer(mMediaPlayer);
+        mCurrentView.assignSurfaceToMediaPlayer(mMediaPlayer);
+
+        mMediaPlayer.setOnPreparedListener(mPreparedListener);
+        mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
+        mMediaPlayer.setOnCompletionListener(mCompletionListener);
+        mMediaPlayer.setOnSeekCompleteListener(mSeekCompleteListener);
+        mMediaPlayer.setOnErrorListener(mErrorListener);
+        mMediaPlayer.setOnInfoListener(mInfoListener);
+        mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
+
+        mCurrentBufferPercentage = -1;
+        mMediaPlayer.setDataSource(context, uri, headers);
+        mMediaPlayer.setAudioAttributes(mAudioAttributes);
+    }
+
+    /*
+     * Reset the media player in any state
+     */
+    @SuppressWarnings("deprecation")
+    private void resetPlayer() {
+        if (mMediaPlayer != null) {
+            mMediaPlayer.reset();
+            mMediaPlayer.release();
+            mMediaPlayer = null;
+            mTextureView.setMediaPlayer(null);
+            mSurfaceView.setMediaPlayer(null);
+            mCurrentState = STATE_IDLE;
+            mTargetState = STATE_IDLE;
+            if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+                mAudioManager.abandonAudioFocus(null);
+            }
+        }
+        mVideoWidth = 0;
+        mVideoHeight = 0;
+    }
+
+    private void updatePlaybackState() {
+        if (mStateBuilder == null) {
+            long playbackActions = PlaybackStateCompat.ACTION_PLAY
+                    | PlaybackStateCompat.ACTION_PAUSE
+                    | PlaybackStateCompat.ACTION_REWIND | PlaybackStateCompat.ACTION_FAST_FORWARD
+                    | PlaybackStateCompat.ACTION_SEEK_TO;
+            mStateBuilder = new PlaybackStateCompat.Builder();
+            mStateBuilder.setActions(playbackActions);
+
+            if (mCustomActionList != null) {
+                for (PlaybackStateCompat.CustomAction action : mCustomActionList) {
+                    mStateBuilder.addCustomAction(action);
+                }
+            }
+        }
+        mStateBuilder.setState(getCorrespondingPlaybackState(),
+                mMediaPlayer.getCurrentPosition(), mSpeed);
+        if (mCurrentState != STATE_ERROR
+                && mCurrentState != STATE_IDLE
+                && mCurrentState != STATE_PREPARING) {
+            if (mCurrentBufferPercentage == -1) {
+                mStateBuilder.setBufferedPosition(-1);
+            } else {
+                mStateBuilder.setBufferedPosition(
+                        (long) (mCurrentBufferPercentage / 100.0 * mMediaPlayer.getDuration()));
+            }
+        }
+
+        // Set PlaybackState for MediaSession
+        if (mMediaSession != null) {
+            PlaybackStateCompat state = mStateBuilder.build();
+            mMediaSession.setPlaybackState(state);
+        }
+    }
+
+    private int getCorrespondingPlaybackState() {
+        switch (mCurrentState) {
+            case STATE_ERROR:
+                return PlaybackStateCompat.STATE_ERROR;
+            case STATE_IDLE:
+                return PlaybackStateCompat.STATE_NONE;
+            case STATE_PREPARING:
+                return PlaybackStateCompat.STATE_CONNECTING;
+            case STATE_PREPARED:
+                return PlaybackStateCompat.STATE_PAUSED;
+            case STATE_PLAYING:
+                return PlaybackStateCompat.STATE_PLAYING;
+            case STATE_PAUSED:
+                return PlaybackStateCompat.STATE_PAUSED;
+            case STATE_PLAYBACK_COMPLETED:
+                return PlaybackStateCompat.STATE_STOPPED;
+            default:
+                return -1;
+        }
+    }
+
+    private final Runnable mFadeOut = new Runnable() {
+        @Override
+        public void run() {
+            if (mCurrentState == STATE_PLAYING) {
+                mMediaControlView.setVisibility(View.GONE);
+            }
+        }
+    };
+
+    private void showController() {
+        if (mMediaControlView == null || !isInPlaybackState()
+                || (isMusicMediaType() && mSizeType == SIZE_TYPE_FULL)) {
+            return;
+        }
+        mMediaControlView.removeCallbacks(mFadeOut);
+        mMediaControlView.setVisibility(View.VISIBLE);
+        if (mShowControllerIntervalMs != 0
+                && !mAccessibilityManager.isTouchExplorationEnabled()) {
+            mMediaControlView.postDelayed(mFadeOut, mShowControllerIntervalMs);
+        }
+    }
+
+    private void toggleMediaControlViewVisibility() {
+        if (mMediaControlView.getVisibility() == View.VISIBLE) {
+            mMediaControlView.removeCallbacks(mFadeOut);
+            mMediaControlView.setVisibility(View.GONE);
+        } else {
+            showController();
+        }
+    }
+
+    private void applySpeed() {
+        if (android.os.Build.VERSION.SDK_INT < 23) {
+            return;
+        }
+        PlaybackParams params = mMediaPlayer.getPlaybackParams().allowDefaults();
+        if (mSpeed != params.getSpeed()) {
+            try {
+                params.setSpeed(mSpeed);
+                mMediaPlayer.setPlaybackParams(params);
+                mFallbackSpeed = mSpeed;
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "PlaybackParams has unsupported value: " + e);
+                float fallbackSpeed = mMediaPlayer.getPlaybackParams().allowDefaults().getSpeed();
+                if (fallbackSpeed > 0.0f) {
+                    mFallbackSpeed = fallbackSpeed;
+                }
+                mSpeed = mFallbackSpeed;
+            }
+        }
+    }
+
+    private boolean isRemotePlayback() {
+        if (mMediaController == null) {
+            return false;
+        }
+        PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo();
+        return playbackInfo != null
+                && playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
+    }
+
+    protected void extractTracks() {
+        MediaPlayer.TrackInfo[] trackInfos = mMediaPlayer.getTrackInfo();
+        mVideoTrackIndices = new ArrayList<>();
+        mAudioTrackIndices = new ArrayList<>();
+        for (int i = 0; i < trackInfos.length; ++i) {
+            int trackType = trackInfos[i].getTrackType();
+            if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO) {
+                mVideoTrackIndices.add(i);
+            } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) {
+                mAudioTrackIndices.add(i);
+            }
+        }
+        // Select first tracks as default
+        if (mVideoTrackIndices.size() > 0) {
+            mSelectedVideoTrackIndex = 0;
+        }
+        if (mAudioTrackIndices.size() > 0) {
+            mSelectedAudioTrackIndex = 0;
+        }
+
+        Bundle data = new Bundle();
+        data.putInt(MediaControlView2.KEY_VIDEO_TRACK_COUNT, mVideoTrackIndices.size());
+        data.putInt(MediaControlView2.KEY_AUDIO_TRACK_COUNT, mAudioTrackIndices.size());
+        mMediaSession.sendSessionEvent(MediaControlView2.EVENT_UPDATE_TRACK_STATUS, data);
+    }
+
+    protected void doShowSubtitleCommand(Bundle args) {
+        // No-op
+    }
+
+    protected void doHideSubtitleCommand() {
+        // No-op
+    }
+
+    private void extractMetadata() {
+        if (mRetriever == null) {
+            return;
+        }
+        // Get and set duration and title values as MediaMetadata for MediaControlView2
+        MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+        String title = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
+        if (title != null) {
+            mTitle = title;
+        }
+        builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
+        builder.putLong(
+                MediaMetadataCompat.METADATA_KEY_DURATION, mMediaPlayer.getDuration());
+
+        if (mMediaSession != null) {
+            mMediaSession.setMetadata(builder.build());
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    private void extractAudioMetadata() {
+        if (mRetriever == null || !isMusicMediaType()) {
+            return;
+        }
+
+        mResources = mInstance.getResources();
+        mManager = (WindowManager) mInstance.getContext().getApplicationContext()
+                .getSystemService(Context.WINDOW_SERVICE);
+
+        byte[] album = mRetriever.getEmbeddedPicture();
+        if (album != null) {
+            Bitmap bitmap = BitmapFactory.decodeByteArray(album, 0, album.length);
+            mMusicAlbumDrawable = new BitmapDrawable(bitmap);
+
+            Palette.Builder builder = Palette.from(bitmap);
+            builder.generate(new Palette.PaletteAsyncListener() {
+                @Override
+                public void onGenerated(Palette palette) {
+                    mDominantColor = palette.getDominantColor(0);
+                    if (mMusicView != null) {
+                        mMusicView.setBackgroundColor(mDominantColor);
+                    }
+                }
+            });
+        } else {
+            mMusicAlbumDrawable = mResources.getDrawable(R.drawable.ic_default_album_image);
+        }
+
+        String title = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
+        if (title != null) {
+            mMusicTitleText = title;
+        } else {
+            mMusicTitleText = mResources.getString(R.string.mcv2_music_title_unknown_text);
+        }
+
+        String artist = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
+        if (artist != null) {
+            mMusicArtistText = artist;
+        } else {
+            mMusicArtistText = mResources.getString(R.string.mcv2_music_artist_unknown_text);
+        }
+
+        // Send title and artist string to MediaControlView2
+        MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+        builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mMusicTitleText);
+        builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, mMusicArtistText);
+        mMediaSession.setMetadata(builder.build());
+
+        // Display Embedded mode as default
+        mInstance.removeView(mSurfaceView);
+        mInstance.removeView(mTextureView);
+        inflateMusicView(R.layout.embedded_music);
+    }
+
+    private int retrieveOrientation() {
+        DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
+        int width = dm.widthPixels;
+        int height = dm.heightPixels;
+
+        return (height > width)
+                ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+                : ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+    }
+
+    private void inflateMusicView(int layoutId) {
+        mInstance.removeView(mMusicView);
+
+        LayoutInflater inflater = (LayoutInflater) mInstance.getContext()
+                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        View v = inflater.inflate(layoutId, null);
+        v.setBackgroundColor(mDominantColor);
+
+        ImageView albumView = v.findViewById(R.id.album);
+        if (albumView != null) {
+            albumView.setImageDrawable(mMusicAlbumDrawable);
+        }
+
+        TextView titleView = v.findViewById(R.id.title);
+        if (titleView != null) {
+            titleView.setText(mMusicTitleText);
+        }
+
+        TextView artistView = v.findViewById(R.id.artist);
+        if (artistView != null) {
+            artistView.setText(mMusicArtistText);
+        }
+
+        mMusicView = v;
+        mInstance.addView(mMusicView, 0);
+    }
+
+    private MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener =
+            new MediaPlayer.OnVideoSizeChangedListener() {
+                @Override
+                public void onVideoSizeChanged(
+                        MediaPlayer mp, int width, int height) {
+                    if (DEBUG) {
+                        Log.d(TAG, "onVideoSizeChanged(): size: " + width + "/" + height);
+                    }
+                    mVideoWidth = mp.getVideoWidth();
+                    mVideoHeight = mp.getVideoHeight();
+                    if (DEBUG) {
+                        Log.d(TAG, "onVideoSizeChanged(): mVideoSize:" + mVideoWidth + "/"
+                                + mVideoHeight);
+                    }
+                    if (mVideoWidth != 0 && mVideoHeight != 0) {
+                        mInstance.requestLayout();
+                    }
+                }
+            };
+
+    private MediaPlayer.OnPreparedListener mPreparedListener =
+            new MediaPlayer.OnPreparedListener() {
+        @Override
+        public void onPrepared(MediaPlayer mp) {
+            if (DEBUG) {
+                Log.d(TAG, "OnPreparedListener(). mCurrentState=" + mCurrentState
+                        + ", mTargetState=" + mTargetState);
+            }
+            mCurrentState = STATE_PREPARED;
+            // Create and set playback state for MediaControlView2
+            updatePlaybackState();
+
+            if (mMediaSession != null) {
+                extractTracks();
+                extractMetadata();
+                extractAudioMetadata();
+            }
+
+            if (mMediaControlView != null) {
+                mMediaControlView.setEnabled(true);
+            }
+            int videoWidth = mp.getVideoWidth();
+            int videoHeight = mp.getVideoHeight();
+
+            // mSeekWhenPrepared may be changed after seekTo() call
+            long seekToPosition = mSeekWhenPrepared;
+            if (seekToPosition != 0) {
+                mMediaController.getTransportControls().seekTo(seekToPosition);
+            }
+
+            if (videoWidth != 0 && videoHeight != 0) {
+                if (videoWidth != mVideoWidth || videoHeight != mVideoHeight) {
+                    mVideoWidth = videoWidth;
+                    mVideoHeight = videoHeight;
+                    mInstance.requestLayout();
+                }
+
+                if (needToStart()) {
+                    mMediaController.getTransportControls().play();
+                }
+            } else {
+                // We don't know the video size yet, but should start anyway.
+                // The video size might be reported to us later.
+                if (needToStart()) {
+                    mMediaController.getTransportControls().play();
+                }
+            }
+            // Get and set duration and title values as MediaMetadata for MediaControlView2
+            MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+
+            builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
+            builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, mMediaPlayer.getDuration());
+
+            if (mMediaSession != null) {
+                mMediaSession.setMetadata(builder.build());
+
+                if (mNeedUpdateMediaType) {
+                    mMediaSession.sendSessionEvent(
+                            MediaControlView2.EVENT_UPDATE_MEDIA_TYPE_STATUS, mMediaTypeData);
+                    mNeedUpdateMediaType = false;
+                }
+            }
+        }
+    };
+
+    private MediaPlayer.OnSeekCompleteListener mSeekCompleteListener =
+            new MediaPlayer.OnSeekCompleteListener() {
+                @Override
+                public void onSeekComplete(MediaPlayer mp) {
+                    updatePlaybackState();
+                }
+    };
+
+    private MediaPlayer.OnCompletionListener mCompletionListener =
+            new MediaPlayer.OnCompletionListener() {
+        @Override
+        @SuppressWarnings("deprecation")
+        public void onCompletion(MediaPlayer mp) {
+            mCurrentState = STATE_PLAYBACK_COMPLETED;
+            mTargetState = STATE_PLAYBACK_COMPLETED;
+            updatePlaybackState();
+            if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+                mAudioManager.abandonAudioFocus(null);
+            }
+        }
+    };
+
+    private MediaPlayer.OnInfoListener mInfoListener = new MediaPlayer.OnInfoListener() {
+        @Override
+        public boolean onInfo(MediaPlayer mp, int what, int extra) {
+            if (what == MediaPlayer.MEDIA_INFO_METADATA_UPDATE) {
+                extractTracks();
+            }
+            return true;
+        }
+    };
+
+    private MediaPlayer.OnErrorListener mErrorListener = new MediaPlayer.OnErrorListener() {
+        @Override
+        public boolean onError(MediaPlayer mp, int frameworkErr, int implErr) {
+            if (DEBUG) {
+                Log.d(TAG, "Error: " + frameworkErr + "," + implErr);
+            }
+            mCurrentState = STATE_ERROR;
+            mTargetState = STATE_ERROR;
+            updatePlaybackState();
+
+            if (mMediaControlView != null) {
+                mMediaControlView.setVisibility(View.GONE);
+            }
+            return true;
+        }
+    };
+
+    private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener =
+            new MediaPlayer.OnBufferingUpdateListener() {
+        @Override
+        public void onBufferingUpdate(MediaPlayer mp, int percent) {
+            mCurrentBufferPercentage = percent;
+            updatePlaybackState();
+        }
+    };
+
+    private class MediaSessionCallback extends MediaSessionCompat.Callback {
+        @Override
+        public void onCommand(String command, Bundle args, ResultReceiver receiver) {
+            if (isRemotePlayback()) {
+                mRoutePlayer.onCommand(command, args, receiver);
+            } else {
+                switch (command) {
+                    case MediaControlView2.COMMAND_SHOW_SUBTITLE:
+                        doShowSubtitleCommand(args);
+                        break;
+                    case MediaControlView2.COMMAND_HIDE_SUBTITLE:
+                        doHideSubtitleCommand();
+                        break;
+                    case MediaControlView2.COMMAND_SELECT_AUDIO_TRACK:
+                        int audioIndex = args.getInt(MediaControlView2.KEY_SELECTED_AUDIO_INDEX,
+                                INVALID_TRACK_INDEX);
+                        if (audioIndex != INVALID_TRACK_INDEX) {
+                            int audioTrackIndex = mAudioTrackIndices.get(audioIndex);
+                            if (audioTrackIndex != mSelectedAudioTrackIndex) {
+                                mSelectedAudioTrackIndex = audioTrackIndex;
+                                mMediaPlayer.selectTrack(mSelectedAudioTrackIndex);
+                            }
+                        }
+                        break;
+                    case MediaControlView2.COMMAND_SET_PLAYBACK_SPEED:
+                        float speed = args.getFloat(
+                                MediaControlView2.KEY_PLAYBACK_SPEED, INVALID_SPEED);
+                        if (speed != INVALID_SPEED && speed != mSpeed) {
+                            setSpeed(speed);
+                            mSpeed = speed;
+                        }
+                        break;
+                    case MediaControlView2.COMMAND_MUTE:
+                        mVolumeLevel = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+                        mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, 0);
+                        break;
+                    case MediaControlView2.COMMAND_UNMUTE:
+                        mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeLevel, 0);
+                        break;
+                }
+            }
+            showController();
+        }
+
+        @Override
+        public void onCustomAction(final String action, final Bundle extras) {
+            mCustomActionListenerRecord.first.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCustomActionListenerRecord.second.onCustomAction(action, extras);
+                }
+            });
+            showController();
+        }
+
+        @Override
+        public void onPlay() {
+            if (!isAudioGranted()) {
+                requestAudioFocus(mAudioFocusType);
+            }
+
+            if ((isInPlaybackState() && mCurrentView.hasAvailableSurface()) || isMusicMediaType()) {
+                if (isRemotePlayback()) {
+                    mRoutePlayer.onPlay();
+                } else {
+                    applySpeed();
+                    mMediaPlayer.start();
+                    mCurrentState = STATE_PLAYING;
+                    updatePlaybackState();
+                }
+                mCurrentState = STATE_PLAYING;
+            }
+            mTargetState = STATE_PLAYING;
+            if (DEBUG) {
+                Log.d(TAG, "onPlay(). mCurrentState=" + mCurrentState
+                        + ", mTargetState=" + mTargetState);
+            }
+            showController();
+        }
+
+        @Override
+        public void onPause() {
+            if (isInPlaybackState()) {
+                if (isRemotePlayback()) {
+                    mRoutePlayer.onPlay();
+                    mCurrentState = STATE_PAUSED;
+                } else if (mMediaPlayer.isPlaying()) {
+                    mMediaPlayer.pause();
+                    mCurrentState = STATE_PAUSED;
+                    updatePlaybackState();
+                }
+            }
+            mTargetState = STATE_PAUSED;
+            if (DEBUG) {
+                Log.d(TAG, "onPause(). mCurrentState=" + mCurrentState
+                        + ", mTargetState=" + mTargetState);
+            }
+            showController();
+        }
+
+        @Override
+        public void onSeekTo(long pos) {
+            if (isInPlaybackState()) {
+                if (isRemotePlayback()) {
+                    mRoutePlayer.onPlay();
+                } else {
+                    if (android.os.Build.VERSION.SDK_INT < 26) {
+                        mMediaPlayer.seekTo((int) pos);
+                    } else {
+                        mMediaPlayer.seekTo(pos, MediaPlayer.SEEK_PREVIOUS_SYNC);
+                    }
+                    mSeekWhenPrepared = 0;
+                }
+            } else {
+                mSeekWhenPrepared = pos;
+            }
+            showController();
+        }
+
+        @Override
+        public void onStop() {
+            if (isRemotePlayback()) {
+                mRoutePlayer.onPlay();
+            } else {
+                resetPlayer();
+            }
+            showController();
+        }
+    }
+}
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoViewInterfaceWithMp1.java
similarity index 92%
copy from media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java
copy to media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoViewInterfaceWithMp1.java
index 81b40a9..a3bac28 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java
+++ b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoViewInterfaceWithMp1.java
@@ -21,7 +21,7 @@
 
 import androidx.annotation.NonNull;
 
-interface VideoViewInterface {
+interface VideoViewInterfaceWithMp1 {
     /**
      * Assigns the view's surface to the given MediaPlayer instance.
      *
@@ -44,7 +44,7 @@
      *
      * @param oldView The view that MediaPlayer is currently rendering on.
      */
-    void takeOver(@NonNull VideoViewInterface oldView);
+    void takeOver(@NonNull VideoViewInterfaceWithMp1 oldView);
 
     /**
      * Indicates if the view's surface is available.
@@ -61,6 +61,6 @@
         void onSurfaceCreated(View view, int width, int height);
         void onSurfaceDestroyed(View view);
         void onSurfaceChanged(View view, int width, int height);
-        void onSurfaceTakeOverDone(VideoViewInterface view);
+        void onSurfaceTakeOverDone(VideoViewInterfaceWithMp1 view);
     }
 }
diff --git a/media-widget/src/main/res/layout/embedded_settings_list_item.xml b/media-widget/src/main/res/layout/embedded_settings_list_item.xml
index 1156dca..c690c48 100644
--- a/media-widget/src/main/res/layout/embedded_settings_list_item.xml
+++ b/media-widget/src/main/res/layout/embedded_settings_list_item.xml
@@ -16,48 +16,50 @@
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="wrap_content"
-    android:layout_height="@dimen/mcv2_embedded_settings_height"
+    android:layout_height="@dimen/mcv2_settings_height"
     android:orientation="horizontal"
     android:background="@color/black_opacity_70">
 
     <LinearLayout
         android:layout_width="wrap_content"
-        android:layout_height="@dimen/mcv2_embedded_settings_height"
+        android:layout_height="@dimen/mcv2_settings_height"
         android:gravity="center"
         android:orientation="horizontal">
 
         <ImageView
             android:id="@+id/icon"
-            android:layout_width="@dimen/mcv2_embedded_settings_icon_size"
-            android:layout_height="@dimen/mcv2_embedded_settings_icon_size"
+            android:layout_width="@dimen/mcv2_settings_icon_size"
+            android:layout_height="@dimen/mcv2_settings_icon_size"
             android:layout_margin="8dp"
             android:gravity="center" />
     </LinearLayout>
 
-    <RelativeLayout
+    <LinearLayout
         android:layout_width="wrap_content"
-        android:layout_height="@dimen/mcv2_embedded_settings_height"
-        android:gravity="center"
-        android:orientation="vertical">
+        android:layout_height="match_parent"
+        android:gravity="center|left">
 
-        <TextView
-            android:id="@+id/main_text"
+        <LinearLayout
             android:layout_width="wrap_content"
-            android:layout_height="@dimen/mcv2_embedded_settings_text_height"
-            android:gravity="center"
-            android:paddingLeft="2dp"
-            android:textColor="@color/white"
-            android:textSize="@dimen/mcv2_embedded_settings_main_text_size"/>
+            android:layout_height="wrap_content"
+            android:gravity="center|left"
+            android:orientation="vertical">
 
-        <TextView
-            android:id="@+id/sub_text"
-            android:layout_width="wrap_content"
-            android:layout_height="@dimen/mcv2_embedded_settings_text_height"
-            android:layout_below="@id/main_text"
-            android:gravity="center"
-            android:paddingLeft="2dp"
-            android:textColor="@color/white_opacity_70"
-            android:textSize="@dimen/mcv2_embedded_settings_sub_text_size"/>
-    </RelativeLayout>
+            <TextView
+                android:id="@+id/main_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingLeft="2dp"
+                android:textColor="@color/white"
+                android:textSize="@dimen/mcv2_settings_main_text_size"/>
+
+            <TextView
+                android:id="@+id/sub_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingLeft="2dp"
+                android:textColor="@color/white_opacity_70"
+                android:textSize="@dimen/mcv2_settings_sub_text_size"/>
+        </LinearLayout>
+    </LinearLayout>
 </LinearLayout>
-
diff --git a/media-widget/src/main/res/layout/embedded_sub_settings_list_item.xml b/media-widget/src/main/res/layout/embedded_sub_settings_list_item.xml
index 5947a72..c455504 100644
--- a/media-widget/src/main/res/layout/embedded_sub_settings_list_item.xml
+++ b/media-widget/src/main/res/layout/embedded_sub_settings_list_item.xml
@@ -16,20 +16,20 @@
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="wrap_content"
-    android:layout_height="@dimen/mcv2_embedded_settings_height"
+    android:layout_height="@dimen/mcv2_settings_height"
     android:orientation="horizontal"
     android:background="@color/black_opacity_70">
 
     <LinearLayout
         android:layout_width="wrap_content"
-        android:layout_height="@dimen/mcv2_embedded_settings_height"
+        android:layout_height="@dimen/mcv2_settings_height"
         android:gravity="center"
         android:orientation="horizontal">
 
         <ImageView
             android:id="@+id/check"
-            android:layout_width="@dimen/mcv2_embedded_settings_icon_size"
-            android:layout_height="@dimen/mcv2_embedded_settings_icon_size"
+            android:layout_width="@dimen/mcv2_settings_icon_size"
+            android:layout_height="@dimen/mcv2_settings_icon_size"
             android:layout_margin="8dp"
             android:gravity="center"
             android:src="@drawable/ic_check"/>
@@ -37,17 +37,17 @@
 
     <RelativeLayout
         android:layout_width="wrap_content"
-        android:layout_height="@dimen/mcv2_embedded_settings_height"
+        android:layout_height="@dimen/mcv2_settings_height"
         android:gravity="center"
         android:orientation="vertical">
 
         <TextView
             android:id="@+id/text"
             android:layout_width="wrap_content"
-            android:layout_height="@dimen/mcv2_embedded_settings_text_height"
+            android:layout_height="@dimen/mcv2_settings_text_height"
             android:gravity="center"
             android:paddingLeft="2dp"
             android:textColor="@color/white"
-            android:textSize="@dimen/mcv2_embedded_settings_main_text_size"/>
+            android:textSize="@dimen/mcv2_settings_main_text_size"/>
     </RelativeLayout>
 </LinearLayout>
diff --git a/media-widget/src/main/res/layout/embedded_transport_controls.xml b/media-widget/src/main/res/layout/embedded_transport_controls.xml
index a3a5957..89b98b7 100644
--- a/media-widget/src/main/res/layout/embedded_transport_controls.xml
+++ b/media-widget/src/main/res/layout/embedded_transport_controls.xml
@@ -15,12 +15,10 @@
 -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
     android:gravity="center"
     android:orientation="horizontal"
-    android:paddingLeft="@dimen/mcv2_transport_controls_padding"
-    android:paddingRight="@dimen/mcv2_transport_controls_padding"
     android:visibility="visible">
 
     <ImageButton android:id="@+id/prev" style="@style/EmbeddedTransportControlsButton.Previous" />
diff --git a/media-widget/src/main/res/layout/full_settings_list_item.xml b/media-widget/src/main/res/layout/full_settings_list_item.xml
index f92ea5e..c4406d9 100644
--- a/media-widget/src/main/res/layout/full_settings_list_item.xml
+++ b/media-widget/src/main/res/layout/full_settings_list_item.xml
@@ -16,47 +16,50 @@
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="wrap_content"
-    android:layout_height="@dimen/mcv2_full_settings_height"
+    android:layout_height="@dimen/mcv2_settings_height"
     android:orientation="horizontal"
     android:background="@color/black_opacity_70">
 
     <LinearLayout
         android:layout_width="wrap_content"
-        android:layout_height="@dimen/mcv2_full_settings_height"
+        android:layout_height="@dimen/mcv2_settings_height"
         android:gravity="center"
         android:orientation="horizontal">
 
         <ImageView
             android:id="@+id/icon"
-            android:layout_width="@dimen/mcv2_full_settings_icon_size"
-            android:layout_height="@dimen/mcv2_full_settings_icon_size"
+            android:layout_width="@dimen/mcv2_settings_icon_size"
+            android:layout_height="@dimen/mcv2_settings_icon_size"
             android:layout_margin="8dp"
             android:gravity="center"/>
     </LinearLayout>
 
-    <RelativeLayout
+    <LinearLayout
         android:layout_width="wrap_content"
-        android:layout_height="@dimen/mcv2_full_settings_height"
-        android:gravity="center"
-        android:orientation="vertical">
+        android:layout_height="match_parent"
+        android:gravity="center|left">
 
-        <TextView
-            android:id="@+id/main_text"
+        <LinearLayout
             android:layout_width="wrap_content"
-            android:layout_height="@dimen/mcv2_full_settings_text_height"
-            android:paddingLeft="2dp"
-            android:gravity="center"
-            android:textColor="@color/white"
-            android:textSize="@dimen/mcv2_full_settings_main_text_size"/>
+            android:layout_height="wrap_content"
+            android:gravity="center|left"
+            android:orientation="vertical">
 
-        <TextView
-            android:id="@+id/sub_text"
-            android:layout_width="wrap_content"
-            android:layout_height="@dimen/mcv2_full_settings_text_height"
-            android:layout_below="@id/main_text"
-            android:gravity="center"
-            android:paddingLeft="2dp"
-            android:textColor="@color/white_opacity_70"
-            android:textSize="@dimen/mcv2_full_settings_sub_text_size"/>
-    </RelativeLayout>
+            <TextView
+                android:id="@+id/main_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingLeft="2dp"
+                android:textColor="@color/white"
+                android:textSize="@dimen/mcv2_settings_main_text_size"/>
+
+            <TextView
+                android:id="@+id/sub_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingLeft="2dp"
+                android:textColor="@color/white_opacity_70"
+                android:textSize="@dimen/mcv2_settings_sub_text_size"/>
+        </LinearLayout>
+    </LinearLayout>
 </LinearLayout>
diff --git a/media-widget/src/main/res/layout/full_sub_settings_list_item.xml b/media-widget/src/main/res/layout/full_sub_settings_list_item.xml
index 49128d0..3bd4ed1 100644
--- a/media-widget/src/main/res/layout/full_sub_settings_list_item.xml
+++ b/media-widget/src/main/res/layout/full_sub_settings_list_item.xml
@@ -16,20 +16,20 @@
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="wrap_content"
-    android:layout_height="@dimen/mcv2_full_settings_height"
+    android:layout_height="@dimen/mcv2_settings_height"
     android:orientation="horizontal"
     android:background="@color/black_opacity_70">
 
     <LinearLayout
         android:layout_width="wrap_content"
-        android:layout_height="@dimen/mcv2_full_settings_height"
+        android:layout_height="@dimen/mcv2_settings_height"
         android:gravity="center"
         android:orientation="horizontal">
 
         <ImageView
             android:id="@+id/check"
-            android:layout_width="@dimen/mcv2_full_settings_icon_size"
-            android:layout_height="@dimen/mcv2_full_settings_icon_size"
+            android:layout_width="@dimen/mcv2_settings_icon_size"
+            android:layout_height="@dimen/mcv2_settings_icon_size"
             android:layout_margin="8dp"
             android:gravity="center"
             android:src="@drawable/ic_check"/>
@@ -37,17 +37,17 @@
 
     <RelativeLayout
         android:layout_width="wrap_content"
-        android:layout_height="@dimen/mcv2_full_settings_height"
+        android:layout_height="@dimen/mcv2_settings_height"
         android:gravity="center"
         android:orientation="vertical">
 
         <TextView
             android:id="@+id/text"
             android:layout_width="wrap_content"
-            android:layout_height="@dimen/mcv2_full_settings_text_height"
+            android:layout_height="@dimen/mcv2_settings_text_height"
             android:gravity="center"
             android:paddingLeft="2dp"
             android:textColor="@color/white"
-            android:textSize="@dimen/mcv2_full_settings_main_text_size"/>
+            android:textSize="@dimen/mcv2_settings_main_text_size"/>
     </RelativeLayout>
 </LinearLayout>
\ No newline at end of file
diff --git a/media-widget/src/main/res/layout/full_transport_controls.xml b/media-widget/src/main/res/layout/full_transport_controls.xml
index 0914785..f5d8b00 100644
--- a/media-widget/src/main/res/layout/full_transport_controls.xml
+++ b/media-widget/src/main/res/layout/full_transport_controls.xml
@@ -15,12 +15,10 @@
 -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
     android:gravity="center"
     android:orientation="horizontal"
-    android:paddingLeft="@dimen/mcv2_transport_controls_padding"
-    android:paddingRight="@dimen/mcv2_transport_controls_padding"
     android:visibility="visible">
 
     <ImageButton android:id="@+id/prev" style="@style/FullTransportControlsButton.Previous" />
diff --git a/media-widget/src/main/res/layout/media_controller.xml b/media-widget/src/main/res/layout/media_controller.xml
index 8749a7b..7fec8cc 100644
--- a/media-widget/src/main/res/layout/media_controller.xml
+++ b/media-widget/src/main/res/layout/media_controller.xml
@@ -29,19 +29,16 @@
             android:id="@+id/title_bar_left"
             android:gravity="center"
             android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_alignParentLeft="true"
+            android:layout_height="match_parent"
+            android:layout_alignParentStart="true"
             android:layout_centerVertical="true"
             android:orientation="horizontal">
 
             <ImageButton
                 android:id="@+id/back"
                 android:clickable="true"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
                 android:layout_alignParentStart="true"
                 android:layout_centerVertical="true"
-                android:paddingLeft="5dip"
                 android:visibility="visible"
                 style="@style/TitleBarButton.Back"/>
 
@@ -53,8 +50,7 @@
                 android:layout_toRightOf="@id/back"
                 android:layout_centerVertical="true"
                 android:maxLines="1"
-                android:paddingLeft="5dip"
-                android:paddingRight="5dip"
+                android:paddingStart="@dimen/mcv2_embedded_icon_padding"
                 android:textSize="15sp"
                 android:textColor="#FFFFFFFF"/>
         </LinearLayout>
@@ -99,13 +95,12 @@
                     style="@style/TitleBarButton.Launch" />
             </LinearLayout>
 
-            <!-- TODO (b/77158231): Causes java.lang.ClassNotFoundException as of Apr 02 2018 -->
-            <!--view class="androidx.mediarouter.app.MediaRouteButton"
+            <view class="androidx.mediarouter.app.MediaRouteButton"
                 android:id="@+id/cast"
                 android:layout_centerVertical="true"
                 android:visibility="gone"
                 android:contentDescription="@string/mr_button_content_description"
-                style="@style/TitleBarButton" /-->
+                style="@style/TitleBarButton" />
         </LinearLayout>
 
     </RelativeLayout>
@@ -157,9 +152,9 @@
     <RelativeLayout
         android:id="@+id/bottom_bar"
         android:layout_width="match_parent"
-        android:layout_height="44dp"
-        android:orientation="horizontal"
-        android:background="@color/bottom_bar_background">
+        android:layout_height="@dimen/mcv2_bottom_bar_height"
+        android:background="@color/bottom_bar_background"
+        android:orientation="horizontal">
 
         <LinearLayout
             android:id="@+id/bottom_bar_left"
@@ -170,12 +165,12 @@
 
             <TextView
                 android:id="@+id/ad_skip_time"
-                android:gravity="center"
                 android:layout_width="wrap_content"
                 android:layout_height="match_parent"
                 android:layout_marginLeft="4dp"
-                android:textSize="12sp"
+                android:gravity="center"
                 android:textColor="#FFFFFF"
+                android:textSize="12sp"
                 android:visibility="gone" />
         </LinearLayout>
 
@@ -184,19 +179,21 @@
             android:layout_width="wrap_content"
             android:layout_height="match_parent"
             android:layout_toRightOf="@id/bottom_bar_left"
+            android:gravity="center"
             android:paddingLeft="10dp"
-            android:paddingRight="10dp"
-            android:gravity="center" >
+            android:paddingRight="10dp">
 
             <TextView
                 android:id="@+id/time_current"
-                style="@style/TimeText.Current"/>
+                style="@style/TimeText.Current" />
+
             <TextView
                 android:id="@+id/time_interpunct"
-                style="@style/TimeText.Interpunct"/>
+                style="@style/TimeText.Interpunct" />
+
             <TextView
                 android:id="@+id/time_end"
-                style="@style/TimeText.End"/>
+                style="@style/TimeText.End" />
         </LinearLayout>
 
         <LinearLayout
@@ -204,63 +201,68 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_alignParentEnd="true"
-            android:gravity="right">
+            android:layout_centerVertical="true">
 
             <LinearLayout
                 android:id="@+id/basic_controls"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:gravity="center_vertical"
-                android:orientation="horizontal" >
+                android:gravity="center"
+                android:orientation="horizontal">
 
                 <TextView
                     android:id="@+id/ad_remaining"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:gravity="center"
-                    android:textSize="12sp"
                     android:textColor="#FFFFFF"
+                    android:textSize="12sp"
                     android:visibility="gone" />
 
                 <ImageButton
                     android:id="@+id/mute"
                     style="@style/BottomBarButton.Mute" />
+
                 <ImageButton
                     android:id="@+id/subtitle"
+                    style="@style/BottomBarButton.CC"
                     android:scaleType="fitCenter"
-                    android:visibility="gone"
-                    style="@style/BottomBarButton.CC" />
+                    android:visibility="gone" />
+
                 <ImageButton
                     android:id="@+id/fullscreen"
-                    style="@style/BottomBarButton.FullScreen"/>
+                    style="@style/BottomBarButton.FullScreen" />
+
                 <ImageButton
                     android:id="@+id/overflow_right"
-                    style="@style/BottomBarButton.OverflowRight"/>
+                    style="@style/BottomBarButton.OverflowRight" />
             </LinearLayout>
 
             <LinearLayout
                 android:id="@+id/extra_controls"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:visibility="gone"
+                android:gravity="center"
                 android:orientation="horizontal"
-                android:gravity="center_vertical">
+                android:visibility="gone">
 
                 <LinearLayout
                     android:id="@+id/custom_buttons"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:orientation="horizontal"/>
+                    android:orientation="horizontal" />
 
                 <ImageButton
                     android:id="@+id/video_quality"
                     style="@style/BottomBarButton.VideoQuality" />
+
                 <ImageButton
                     android:id="@+id/settings"
                     style="@style/BottomBarButton.Settings" />
+
                 <ImageButton
                     android:id="@+id/overflow_left"
-                    style="@style/BottomBarButton.OverflowLeft"/>
+                    style="@style/BottomBarButton.OverflowLeft" />
             </LinearLayout>
         </LinearLayout>
     </RelativeLayout>
diff --git a/media-widget/src/main/res/layout/minimal_transport_controls.xml b/media-widget/src/main/res/layout/minimal_transport_controls.xml
index 800c80b..a72adc7 100644
--- a/media-widget/src/main/res/layout/minimal_transport_controls.xml
+++ b/media-widget/src/main/res/layout/minimal_transport_controls.xml
@@ -15,8 +15,8 @@
 -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
     android:gravity="center"
     android:orientation="horizontal">
 
diff --git a/media-widget/src/main/res/layout/settings_list.xml b/media-widget/src/main/res/layout/settings_list.xml
index ea30538..f08f81b 100644
--- a/media-widget/src/main/res/layout/settings_list.xml
+++ b/media-widget/src/main/res/layout/settings_list.xml
@@ -16,7 +16,8 @@
 
 <ListView xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="@dimen/mcv2_embedded_settings_width"
-    android:layout_height="@dimen/mcv2_embedded_settings_height"
+    android:layout_height="@dimen/mcv2_settings_height"
+    android:layout_gravity="center_vertical"
     android:divider="@null"
     android:dividerHeight="0dp">
 </ListView>
\ No newline at end of file
diff --git a/media-widget/src/main/res/values/dimens.xml b/media-widget/src/main/res/values/dimens.xml
index 796f345..f03c871 100644
--- a/media-widget/src/main/res/values/dimens.xml
+++ b/media-widget/src/main/res/values/dimens.xml
@@ -17,25 +17,19 @@
 
 <resources>
     <dimen name="mcv2_embedded_settings_width">150dp</dimen>
-    <dimen name="mcv2_embedded_settings_height">36dp</dimen>
-    <dimen name="mcv2_embedded_settings_icon_size">20dp</dimen>
-    <dimen name="mcv2_embedded_settings_text_height">18dp</dimen>
-    <dimen name="mcv2_embedded_settings_main_text_size">12sp</dimen>
-    <dimen name="mcv2_embedded_settings_sub_text_size">10sp</dimen>
     <dimen name="mcv2_full_settings_width">225dp</dimen>
-    <dimen name="mcv2_full_settings_height">54dp</dimen>
-    <dimen name="mcv2_full_settings_icon_size">30dp</dimen>
-    <dimen name="mcv2_full_settings_text_height">27dp</dimen>
-    <dimen name="mcv2_full_settings_main_text_size">16sp</dimen>
-    <dimen name="mcv2_full_settings_sub_text_size">13sp</dimen>
+    <dimen name="mcv2_settings_height">48dp</dimen>
+    <dimen name="mcv2_settings_icon_size">24dp</dimen>
+    <dimen name="mcv2_settings_text_height">24dp</dimen>
+    <dimen name="mcv2_settings_main_text_size">14sp</dimen>
+    <dimen name="mcv2_settings_sub_text_size">12sp</dimen>
     <dimen name="mcv2_settings_offset">8dp</dimen>
 
-    <dimen name="mcv2_transport_controls_padding">4dp</dimen>
-    <dimen name="mcv2_pause_icon_size">36dp</dimen>
-    <dimen name="mcv2_full_icon_size">28dp</dimen>
-    <dimen name="mcv2_embedded_icon_size">24dp</dimen>
-    <dimen name="mcv2_minimal_icon_size">24dp</dimen>
+    <dimen name="mcv2_icon_size">48dp</dimen>
     <dimen name="mcv2_icon_margin">10dp</dimen>
+    <dimen name="mcv2_pause_icon_padding">6dp</dimen>
+    <dimen name="mcv2_full_icon_padding">10dp</dimen>
+    <dimen name="mcv2_embedded_icon_padding">12dp</dimen>
 
     <dimen name="mcv2_full_album_image_portrait_size">232dp</dimen>
     <dimen name="mcv2_full_album_image_landscape_size">176dp</dimen>
@@ -43,4 +37,7 @@
     <dimen name="mcv2_custom_progress_max_size">2dp</dimen>
     <dimen name="mcv2_custom_progress_thumb_size">12dp</dimen>
     <dimen name="mcv2_buffer_view_height">5dp</dimen>
+
+    <dimen name="mcv2_title_bar_height">54dp</dimen>
+    <dimen name="mcv2_bottom_bar_height">58dp</dimen>
 </resources>
diff --git a/media-widget/src/main/res/values/styles.xml b/media-widget/src/main/res/values/styles.xml
index fc42bd4..2e03acb 100644
--- a/media-widget/src/main/res/values/styles.xml
+++ b/media-widget/src/main/res/values/styles.xml
@@ -2,91 +2,99 @@
 <resources>
     <style name="FullTransportControlsButton">
         <item name="android:background">@null</item>
-        <item name="android:layout_margin">@dimen/mcv2_icon_margin</item>
         <item name="android:scaleType">fitXY</item>
     </style>
 
     <style name="FullTransportControlsButton.Previous">
         <item name="android:src">@drawable/ic_skip_previous</item>
-        <item name="android:layout_width">@dimen/mcv2_full_icon_size</item>
-        <item name="android:layout_height">@dimen/mcv2_full_icon_size</item>
+        <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+        <item name="android:layout_height">@dimen/mcv2_icon_size</item>
         <item name="android:contentDescription">@string/mcv2_previous_button_desc</item>
+        <item name="android:padding">@dimen/mcv2_full_icon_padding</item>
     </style>
 
     <style name="FullTransportControlsButton.Next">
         <item name="android:src">@drawable/ic_skip_next</item>
-        <item name="android:layout_width">@dimen/mcv2_full_icon_size</item>
-        <item name="android:layout_height">@dimen/mcv2_full_icon_size</item>
+        <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+        <item name="android:layout_height">@dimen/mcv2_icon_size</item>
         <item name="android:contentDescription">@string/mcv2_next_button_desc</item>
+        <item name="android:padding">@dimen/mcv2_full_icon_padding</item>
     </style>
 
     <style name="FullTransportControlsButton.Pause">
         <item name="android:src">@drawable/ic_pause_circle_filled</item>
-        <item name="android:layout_width">@dimen/mcv2_pause_icon_size</item>
-        <item name="android:layout_height">@dimen/mcv2_pause_icon_size</item>
+        <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+        <item name="android:layout_height">@dimen/mcv2_icon_size</item>
         <item name="android:contentDescription">@string/mcv2_pause_button_desc</item>
+        <item name="android:padding">@dimen/mcv2_pause_icon_padding</item>
     </style>
 
     <style name="FullTransportControlsButton.Ffwd">
         <item name="android:src">@drawable/ic_forward_30</item>
-        <item name="android:layout_width">@dimen/mcv2_full_icon_size</item>
-        <item name="android:layout_height">@dimen/mcv2_full_icon_size</item>
+        <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+        <item name="android:layout_height">@dimen/mcv2_icon_size</item>
         <item name="android:contentDescription">@string/mcv2_ffwd_button_desc</item>
+        <item name="android:padding">@dimen/mcv2_full_icon_padding</item>
     </style>
 
     <style name="FullTransportControlsButton.Rew">
         <item name="android:src">@drawable/ic_rewind_10</item>
-        <item name="android:layout_width">@dimen/mcv2_full_icon_size</item>
-        <item name="android:layout_height">@dimen/mcv2_full_icon_size</item>
+        <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+        <item name="android:layout_height">@dimen/mcv2_icon_size</item>
         <item name="android:contentDescription">@string/mcv2_rewind_button_desc</item>
+        <item name="android:padding">@dimen/mcv2_full_icon_padding</item>
     </style>
 
     <style name="EmbeddedTransportControlsButton">
         <item name="android:background">@null</item>
-        <item name="android:layout_margin">@dimen/mcv2_icon_margin</item>
         <item name="android:scaleType">fitXY</item>
     </style>
 
     <style name="EmbeddedTransportControlsButton.Previous">
         <item name="android:src">@drawable/ic_skip_previous</item>
-        <item name="android:layout_width">@dimen/mcv2_embedded_icon_size</item>
-        <item name="android:layout_height">@dimen/mcv2_embedded_icon_size</item>
+        <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+        <item name="android:layout_height">@dimen/mcv2_icon_size</item>
         <item name="android:contentDescription">@string/mcv2_previous_button_desc</item>
+        <item name="android:padding">@dimen/mcv2_embedded_icon_padding</item>
     </style>
 
     <style name="EmbeddedTransportControlsButton.Next">
         <item name="android:src">@drawable/ic_skip_next</item>
-        <item name="android:layout_width">@dimen/mcv2_embedded_icon_size</item>
-        <item name="android:layout_height">@dimen/mcv2_embedded_icon_size</item>
+        <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+        <item name="android:layout_height">@dimen/mcv2_icon_size</item>
         <item name="android:contentDescription">@string/mcv2_next_button_desc</item>
+        <item name="android:padding">@dimen/mcv2_embedded_icon_padding</item>
     </style>
 
     <style name="EmbeddedTransportControlsButton.Pause">
         <item name="android:src">@drawable/ic_pause_circle_filled</item>
-        <item name="android:layout_width">@dimen/mcv2_pause_icon_size</item>
-        <item name="android:layout_height">@dimen/mcv2_pause_icon_size</item>
+        <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+        <item name="android:layout_height">@dimen/mcv2_icon_size</item>
         <item name="android:contentDescription">@string/mcv2_pause_button_desc</item>
+        <item name="android:padding">@dimen/mcv2_pause_icon_padding</item>
     </style>
 
     <style name="EmbeddedTransportControlsButton.Ffwd">
         <item name="android:src">@drawable/ic_forward_30</item>
-        <item name="android:layout_width">@dimen/mcv2_embedded_icon_size</item>
-        <item name="android:layout_height">@dimen/mcv2_embedded_icon_size</item>
+        <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+        <item name="android:layout_height">@dimen/mcv2_icon_size</item>
         <item name="android:contentDescription">@string/mcv2_ffwd_button_desc</item>
+        <item name="android:padding">@dimen/mcv2_embedded_icon_padding</item>
     </style>
 
     <style name="EmbeddedTransportControlsButton.Rew">
         <item name="android:src">@drawable/ic_rewind_10</item>
-        <item name="android:layout_width">@dimen/mcv2_embedded_icon_size</item>
-        <item name="android:layout_height">@dimen/mcv2_embedded_icon_size</item>
+        <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+        <item name="android:layout_height">@dimen/mcv2_icon_size</item>
         <item name="android:contentDescription">@string/mcv2_rewind_button_desc</item>
+        <item name="android:padding">@dimen/mcv2_embedded_icon_padding</item>
     </style>
 
     <style name="MinimalTransportControlsButton">
         <item name="android:background">@null</item>
-        <item name="android:layout_width">@dimen/mcv2_pause_icon_size</item>
-        <item name="android:layout_height">@dimen/mcv2_pause_icon_size</item>
-        <item name="android:layout_margin">@dimen/mcv2_icon_margin</item>
+        <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+        <item name="android:layout_height">@dimen/mcv2_icon_size</item>
+        <item name="android:padding">@dimen/mcv2_pause_icon_padding</item>
         <item name="android:scaleType">fitXY</item>
         <item name="android:src">@drawable/ic_pause_circle_filled</item>
         <item name="android:contentDescription">@string/mcv2_pause_button_desc</item>
@@ -94,15 +102,15 @@
 
     <style name="TitleBar">
         <item name="android:layout_width">match_parent</item>
-        <item name="android:layout_height">46dp</item>
-        <item name="android:paddingLeft">5dp</item>
-        <item name="android:paddingRight">5dp</item>
+        <item name="android:layout_height">@dimen/mcv2_title_bar_height</item>
     </style>
 
     <style name="TitleBarButton">
         <item name="android:background">@null</item>
-        <item name="android:layout_width">36dp</item>
-        <item name="android:layout_height">36dp</item>
+        <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+        <item name="android:layout_height">@dimen/mcv2_icon_size</item>
+        <item name="android:padding">@dimen/mcv2_embedded_icon_padding</item>
+        <item name="android:scaleType">fitXY</item>
     </style>
 
     <style name="TitleBarButton.Back">
@@ -142,10 +150,10 @@
 
     <style name="BottomBarButton">
         <item name="android:background">@null</item>
-        <item name="android:layout_width">@dimen/mcv2_embedded_icon_size</item>
-        <item name="android:layout_height">@dimen/mcv2_embedded_icon_size</item>
-        <item name="android:layout_margin">@dimen/mcv2_icon_margin</item>
+        <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+        <item name="android:layout_height">@dimen/mcv2_icon_size</item>
         <item name="android:gravity">center_horizontal</item>
+        <item name="android:padding">@dimen/mcv2_embedded_icon_padding</item>
         <item name="android:scaleType">fitXY</item>
     </style>
 
diff --git a/media/api/current.txt b/media/api/current.txt
index a9e2161..c3c28f5 100644
--- a/media/api/current.txt
+++ b/media/api/current.txt
@@ -300,6 +300,7 @@
     method public void addOnActiveChangeListener(android.support.v4.media.session.MediaSessionCompat.OnActiveChangeListener);
     method public static android.support.v4.media.session.MediaSessionCompat fromMediaSession(android.content.Context, java.lang.Object);
     method public android.support.v4.media.session.MediaControllerCompat getController();
+    method public final androidx.media.MediaSessionManager.RemoteUserInfo getCurrentControllerInfo();
     method public java.lang.Object getMediaSession();
     method public java.lang.Object getRemoteControlClient();
     method public android.support.v4.media.session.MediaSessionCompat.Token getSessionToken();
@@ -600,10 +601,38 @@
     method public androidx.media.DataSourceDesc.Builder setStartPosition(long);
   }
 
+  public abstract class Media2DataSource implements java.io.Closeable {
+    ctor public Media2DataSource();
+    method public abstract long getSize() throws java.io.IOException;
+    method public abstract int readAt(long, byte[], int, int) throws java.io.IOException;
+  }
+
+  public class MediaBrowser2 extends androidx.media.MediaController2 {
+    ctor public MediaBrowser2(android.content.Context, androidx.media.SessionToken2, java.util.concurrent.Executor, androidx.media.MediaBrowser2.BrowserCallback);
+    method public void getChildren(java.lang.String, int, int, android.os.Bundle);
+    method public void getItem(java.lang.String);
+    method public void getLibraryRoot(android.os.Bundle);
+    method public void getSearchResult(java.lang.String, int, int, android.os.Bundle);
+    method public void search(java.lang.String, android.os.Bundle);
+    method public void subscribe(java.lang.String, android.os.Bundle);
+    method public void unsubscribe(java.lang.String);
+  }
+
+  public static class MediaBrowser2.BrowserCallback extends androidx.media.MediaController2.ControllerCallback {
+    ctor public MediaBrowser2.BrowserCallback();
+    method public void onChildrenChanged(androidx.media.MediaBrowser2, java.lang.String, int, android.os.Bundle);
+    method public void onGetChildrenDone(androidx.media.MediaBrowser2, java.lang.String, int, int, java.util.List<androidx.media.MediaItem2>, android.os.Bundle);
+    method public void onGetItemDone(androidx.media.MediaBrowser2, java.lang.String, androidx.media.MediaItem2);
+    method public void onGetLibraryRootDone(androidx.media.MediaBrowser2, android.os.Bundle, java.lang.String, android.os.Bundle);
+    method public void onGetSearchResultDone(androidx.media.MediaBrowser2, java.lang.String, int, int, java.util.List<androidx.media.MediaItem2>, android.os.Bundle);
+    method public void onSearchResultChanged(androidx.media.MediaBrowser2, java.lang.String, int, android.os.Bundle);
+  }
+
   public abstract class MediaBrowserServiceCompat extends android.app.Service {
     ctor public MediaBrowserServiceCompat();
     method public void dump(java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]);
     method public final android.os.Bundle getBrowserRootHints();
+    method public final androidx.media.MediaSessionManager.RemoteUserInfo getCurrentBrowserInfo();
     method public android.support.v4.media.session.MediaSessionCompat.Token getSessionToken();
     method public void notifyChildrenChanged(java.lang.String);
     method public void notifyChildrenChanged(java.lang.String, android.os.Bundle);
@@ -739,6 +768,48 @@
     method public androidx.media.MediaItem2.Builder setMetadata(androidx.media.MediaMetadata2);
   }
 
+  public abstract class MediaLibraryService2 extends androidx.media.MediaSessionService2 {
+    ctor public MediaLibraryService2();
+    method public abstract androidx.media.MediaLibraryService2.MediaLibrarySession onCreateSession(java.lang.String);
+    field public static final java.lang.String SERVICE_INTERFACE = "android.media.MediaLibraryService2";
+  }
+
+  public static final class MediaLibraryService2.LibraryRoot {
+    ctor public MediaLibraryService2.LibraryRoot(java.lang.String, android.os.Bundle);
+    method public android.os.Bundle getExtras();
+    method public java.lang.String getRootId();
+    field public static final java.lang.String EXTRA_OFFLINE = "android.media.extra.OFFLINE";
+    field public static final java.lang.String EXTRA_RECENT = "android.media.extra.RECENT";
+    field public static final java.lang.String EXTRA_SUGGESTED = "android.media.extra.SUGGESTED";
+  }
+
+  public static final class MediaLibraryService2.MediaLibrarySession extends androidx.media.MediaSession2 {
+    method public void notifyChildrenChanged(androidx.media.MediaSession2.ControllerInfo, java.lang.String, int, android.os.Bundle);
+    method public void notifyChildrenChanged(java.lang.String, int, android.os.Bundle);
+    method public void notifySearchResultChanged(androidx.media.MediaSession2.ControllerInfo, java.lang.String, int, android.os.Bundle);
+  }
+
+  public static final class MediaLibraryService2.MediaLibrarySession.Builder {
+    ctor public MediaLibraryService2.MediaLibrarySession.Builder(androidx.media.MediaLibraryService2, java.util.concurrent.Executor, androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback);
+    method public androidx.media.MediaLibraryService2.MediaLibrarySession build();
+    method public androidx.media.MediaLibraryService2.MediaLibrarySession.Builder setId(java.lang.String);
+    method public androidx.media.MediaLibraryService2.MediaLibrarySession.Builder setPlayer(androidx.media.MediaPlayerInterface);
+    method public androidx.media.MediaLibraryService2.MediaLibrarySession.Builder setPlaylistAgent(androidx.media.MediaPlaylistAgent);
+    method public androidx.media.MediaLibraryService2.MediaLibrarySession.Builder setSessionActivity(android.app.PendingIntent);
+    method public androidx.media.MediaLibraryService2.MediaLibrarySession.Builder setVolumeProvider(androidx.media.VolumeProviderCompat);
+  }
+
+  public static class MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback extends androidx.media.MediaSession2.SessionCallback {
+    ctor public MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback();
+    method public java.util.List<androidx.media.MediaItem2> onGetChildren(androidx.media.MediaLibraryService2.MediaLibrarySession, androidx.media.MediaSession2.ControllerInfo, java.lang.String, int, int, android.os.Bundle);
+    method public androidx.media.MediaItem2 onGetItem(androidx.media.MediaLibraryService2.MediaLibrarySession, androidx.media.MediaSession2.ControllerInfo, java.lang.String);
+    method public androidx.media.MediaLibraryService2.LibraryRoot onGetLibraryRoot(androidx.media.MediaLibraryService2.MediaLibrarySession, androidx.media.MediaSession2.ControllerInfo, android.os.Bundle);
+    method public java.util.List<androidx.media.MediaItem2> onGetSearchResult(androidx.media.MediaLibraryService2.MediaLibrarySession, androidx.media.MediaSession2.ControllerInfo, java.lang.String, int, int, android.os.Bundle);
+    method public void onSearch(androidx.media.MediaLibraryService2.MediaLibrarySession, androidx.media.MediaSession2.ControllerInfo, java.lang.String, android.os.Bundle);
+    method public void onSubscribe(androidx.media.MediaLibraryService2.MediaLibrarySession, androidx.media.MediaSession2.ControllerInfo, java.lang.String, android.os.Bundle);
+    method public void onUnsubscribe(androidx.media.MediaLibraryService2.MediaLibrarySession, androidx.media.MediaSession2.ControllerInfo, java.lang.String);
+  }
+
   public final class MediaMetadata2 {
     method public boolean containsKey(java.lang.String);
     method public static androidx.media.MediaMetadata2 fromBundle(android.os.Bundle);
@@ -810,8 +881,201 @@
     method public androidx.media.MediaMetadata2.Builder setExtras(android.os.Bundle);
   }
 
-  public abstract class MediaPlayerBase implements java.lang.AutoCloseable {
-    ctor public MediaPlayerBase();
+  public abstract class MediaPlayer2 {
+    method public abstract void attachAuxEffect(int);
+    method public abstract void clearDrmEventCallback();
+    method public abstract void clearMediaPlayer2EventCallback();
+    method public abstract void clearPendingCommands();
+    method public abstract void close();
+    method public static final androidx.media.MediaPlayer2 create();
+    method public abstract void deselectTrack(int);
+    method public abstract androidx.media.AudioAttributesCompat getAudioAttributes();
+    method public abstract int getAudioSessionId();
+    method public abstract long getBufferedPosition();
+    method public abstract androidx.media.DataSourceDesc getCurrentDataSource();
+    method public abstract long getCurrentPosition();
+    method public abstract androidx.media.MediaPlayer2.DrmInfo getDrmInfo();
+    method public abstract android.media.MediaDrm.KeyRequest getDrmKeyRequest(byte[], byte[], java.lang.String, int, java.util.Map<java.lang.String, java.lang.String>) throws androidx.media.MediaPlayer2.NoDrmSchemeException;
+    method public abstract java.lang.String getDrmPropertyString(java.lang.String) throws androidx.media.MediaPlayer2.NoDrmSchemeException;
+    method public abstract long getDuration();
+    method public float getMaxPlayerVolume();
+    method public abstract int getMediaPlayer2State();
+    method public abstract androidx.media.MediaPlayerInterface getMediaPlayerInterface();
+    method public abstract android.os.PersistableBundle getMetrics();
+    method public abstract android.media.PlaybackParams getPlaybackParams();
+    method public float getPlaybackSpeed();
+    method public abstract float getPlayerVolume();
+    method public abstract int getSelectedTrack(int);
+    method public abstract android.media.SyncParams getSyncParams();
+    method public abstract android.media.MediaTimestamp getTimestamp();
+    method public abstract java.util.List<androidx.media.MediaPlayer2.TrackInfo> getTrackInfo();
+    method public abstract int getVideoHeight();
+    method public abstract int getVideoWidth();
+    method public boolean isReversePlaybackSupported();
+    method public abstract void loopCurrent(boolean);
+    method public void notifyWhenCommandLabelReached(java.lang.Object);
+    method public abstract void pause();
+    method public abstract void play();
+    method public abstract void prepare();
+    method public abstract void prepareDrm(java.util.UUID) throws androidx.media.MediaPlayer2.ProvisioningNetworkErrorException, androidx.media.MediaPlayer2.ProvisioningServerErrorException, android.media.ResourceBusyException, android.media.UnsupportedSchemeException;
+    method public abstract byte[] provideDrmKeyResponse(byte[], byte[]) throws android.media.DeniedByServerException, androidx.media.MediaPlayer2.NoDrmSchemeException;
+    method public abstract void releaseDrm() throws androidx.media.MediaPlayer2.NoDrmSchemeException;
+    method public abstract void reset();
+    method public abstract void restoreDrmKeys(byte[]) throws androidx.media.MediaPlayer2.NoDrmSchemeException;
+    method public void seekTo(long);
+    method public abstract void seekTo(long, int);
+    method public abstract void selectTrack(int);
+    method public abstract void setAudioAttributes(androidx.media.AudioAttributesCompat);
+    method public abstract void setAudioSessionId(int);
+    method public abstract void setAuxEffectSendLevel(float);
+    method public abstract void setDataSource(androidx.media.DataSourceDesc);
+    method public abstract void setDrmEventCallback(java.util.concurrent.Executor, androidx.media.MediaPlayer2.DrmEventCallback);
+    method public abstract void setDrmPropertyString(java.lang.String, java.lang.String) throws androidx.media.MediaPlayer2.NoDrmSchemeException;
+    method public abstract void setMediaPlayer2EventCallback(java.util.concurrent.Executor, androidx.media.MediaPlayer2.MediaPlayer2EventCallback);
+    method public abstract void setNextDataSource(androidx.media.DataSourceDesc);
+    method public abstract void setNextDataSources(java.util.List<androidx.media.DataSourceDesc>);
+    method public abstract void setOnDrmConfigHelper(androidx.media.MediaPlayer2.OnDrmConfigHelper);
+    method public abstract void setPlaybackParams(android.media.PlaybackParams);
+    method public abstract void setPlaybackSpeed(float);
+    method public abstract void setPlayerVolume(float);
+    method public abstract void setSurface(android.view.Surface);
+    method public abstract void setSyncParams(android.media.SyncParams);
+    method public abstract void skipToNext();
+    field public static final int CALL_COMPLETED_ATTACH_AUX_EFFECT = 1; // 0x1
+    field public static final int CALL_COMPLETED_DESELECT_TRACK = 2; // 0x2
+    field public static final int CALL_COMPLETED_LOOP_CURRENT = 3; // 0x3
+    field public static final int CALL_COMPLETED_PAUSE = 4; // 0x4
+    field public static final int CALL_COMPLETED_PLAY = 5; // 0x5
+    field public static final int CALL_COMPLETED_PREPARE = 6; // 0x6
+    field public static final int CALL_COMPLETED_SEEK_TO = 14; // 0xe
+    field public static final int CALL_COMPLETED_SELECT_TRACK = 15; // 0xf
+    field public static final int CALL_COMPLETED_SET_AUDIO_ATTRIBUTES = 16; // 0x10
+    field public static final int CALL_COMPLETED_SET_AUDIO_SESSION_ID = 17; // 0x11
+    field public static final int CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL = 18; // 0x12
+    field public static final int CALL_COMPLETED_SET_DATA_SOURCE = 19; // 0x13
+    field public static final int CALL_COMPLETED_SET_NEXT_DATA_SOURCE = 22; // 0x16
+    field public static final int CALL_COMPLETED_SET_NEXT_DATA_SOURCES = 23; // 0x17
+    field public static final int CALL_COMPLETED_SET_PLAYBACK_PARAMS = 24; // 0x18
+    field public static final int CALL_COMPLETED_SET_PLAYBACK_SPEED = 25; // 0x19
+    field public static final int CALL_COMPLETED_SET_PLAYER_VOLUME = 26; // 0x1a
+    field public static final int CALL_COMPLETED_SET_SURFACE = 27; // 0x1b
+    field public static final int CALL_COMPLETED_SET_SYNC_PARAMS = 28; // 0x1c
+    field public static final int CALL_COMPLETED_SKIP_TO_NEXT = 29; // 0x1d
+    field public static final int CALL_STATUS_BAD_VALUE = 2; // 0x2
+    field public static final int CALL_STATUS_ERROR_IO = 4; // 0x4
+    field public static final int CALL_STATUS_ERROR_UNKNOWN = -2147483648; // 0x80000000
+    field public static final int CALL_STATUS_INVALID_OPERATION = 1; // 0x1
+    field public static final int CALL_STATUS_NO_ERROR = 0; // 0x0
+    field public static final int CALL_STATUS_PERMISSION_DENIED = 3; // 0x3
+    field public static final int MEDIAPLAYER2_STATE_ERROR = 1005; // 0x3ed
+    field public static final int MEDIAPLAYER2_STATE_IDLE = 1001; // 0x3e9
+    field public static final int MEDIAPLAYER2_STATE_PAUSED = 1003; // 0x3eb
+    field public static final int MEDIAPLAYER2_STATE_PLAYING = 1004; // 0x3ec
+    field public static final int MEDIAPLAYER2_STATE_PREPARED = 1002; // 0x3ea
+    field public static final int MEDIA_ERROR_IO = -1004; // 0xfffffc14
+    field public static final int MEDIA_ERROR_MALFORMED = -1007; // 0xfffffc11
+    field public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200; // 0xc8
+    field public static final int MEDIA_ERROR_TIMED_OUT = -110; // 0xffffff92
+    field public static final int MEDIA_ERROR_UNKNOWN = 1; // 0x1
+    field public static final int MEDIA_ERROR_UNSUPPORTED = -1010; // 0xfffffc0e
+    field public static final int MEDIA_INFO_AUDIO_NOT_PLAYING = 804; // 0x324
+    field public static final int MEDIA_INFO_AUDIO_RENDERING_START = 4; // 0x4
+    field public static final int MEDIA_INFO_BAD_INTERLEAVING = 800; // 0x320
+    field public static final int MEDIA_INFO_BUFFERING_END = 702; // 0x2be
+    field public static final int MEDIA_INFO_BUFFERING_START = 701; // 0x2bd
+    field public static final int MEDIA_INFO_BUFFERING_UPDATE = 704; // 0x2c0
+    field public static final int MEDIA_INFO_METADATA_UPDATE = 802; // 0x322
+    field public static final int MEDIA_INFO_NOT_SEEKABLE = 801; // 0x321
+    field public static final int MEDIA_INFO_PLAYBACK_COMPLETE = 5; // 0x5
+    field public static final int MEDIA_INFO_PLAYLIST_END = 6; // 0x6
+    field public static final int MEDIA_INFO_PREPARED = 100; // 0x64
+    field public static final int MEDIA_INFO_SUBTITLE_TIMED_OUT = 902; // 0x386
+    field public static final int MEDIA_INFO_UNKNOWN = 1; // 0x1
+    field public static final int MEDIA_INFO_UNSUPPORTED_SUBTITLE = 901; // 0x385
+    field public static final int MEDIA_INFO_VIDEO_NOT_PLAYING = 805; // 0x325
+    field public static final int MEDIA_INFO_VIDEO_RENDERING_START = 3; // 0x3
+    field public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700; // 0x2bc
+    field public static final int PREPARE_DRM_STATUS_PREPARATION_ERROR = 3; // 0x3
+    field public static final int PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR = 1; // 0x1
+    field public static final int PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR = 2; // 0x2
+    field public static final int PREPARE_DRM_STATUS_SUCCESS = 0; // 0x0
+    field public static final int SEEK_CLOSEST = 3; // 0x3
+    field public static final int SEEK_CLOSEST_SYNC = 2; // 0x2
+    field public static final int SEEK_NEXT_SYNC = 1; // 0x1
+    field public static final int SEEK_PREVIOUS_SYNC = 0; // 0x0
+    field public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT = 1; // 0x1
+  }
+
+  public static abstract class MediaPlayer2.DrmEventCallback {
+    ctor public MediaPlayer2.DrmEventCallback();
+    method public void onDrmInfo(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc, androidx.media.MediaPlayer2.DrmInfo);
+    method public void onDrmPrepared(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc, int);
+  }
+
+  public static abstract class MediaPlayer2.DrmInfo {
+    ctor public MediaPlayer2.DrmInfo();
+    method public abstract java.util.Map<java.util.UUID, byte[]> getPssh();
+    method public abstract java.util.List<java.util.UUID> getSupportedSchemes();
+  }
+
+  public static abstract class MediaPlayer2.MediaPlayer2EventCallback {
+    ctor public MediaPlayer2.MediaPlayer2EventCallback();
+    method public void onCallCompleted(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc, int, int);
+    method public void onCommandLabelReached(androidx.media.MediaPlayer2, java.lang.Object);
+    method public void onError(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc, int, int);
+    method public void onInfo(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc, int, int);
+    method public void onMediaTimeDiscontinuity(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc, android.media.MediaTimestamp);
+    method public void onSubtitleData(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc, android.media.SubtitleData);
+    method public void onTimedMetaDataAvailable(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc, android.media.TimedMetaData);
+    method public void onVideoSizeChanged(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc, int, int);
+  }
+
+  public static final class MediaPlayer2.MetricsConstants {
+    field public static final java.lang.String CODEC_AUDIO = "android.media.mediaplayer.audio.codec";
+    field public static final java.lang.String CODEC_VIDEO = "android.media.mediaplayer.video.codec";
+    field public static final java.lang.String DURATION = "android.media.mediaplayer.durationMs";
+    field public static final java.lang.String ERRORS = "android.media.mediaplayer.err";
+    field public static final java.lang.String ERROR_CODE = "android.media.mediaplayer.errcode";
+    field public static final java.lang.String FRAMES = "android.media.mediaplayer.frames";
+    field public static final java.lang.String FRAMES_DROPPED = "android.media.mediaplayer.dropped";
+    field public static final java.lang.String HEIGHT = "android.media.mediaplayer.height";
+    field public static final java.lang.String MIME_TYPE_AUDIO = "android.media.mediaplayer.audio.mime";
+    field public static final java.lang.String MIME_TYPE_VIDEO = "android.media.mediaplayer.video.mime";
+    field public static final java.lang.String PLAYING = "android.media.mediaplayer.playingMs";
+    field public static final java.lang.String WIDTH = "android.media.mediaplayer.width";
+  }
+
+  public static class MediaPlayer2.NoDrmSchemeException extends android.media.MediaDrmException {
+    ctor public MediaPlayer2.NoDrmSchemeException(java.lang.String);
+  }
+
+  public static abstract interface MediaPlayer2.OnDrmConfigHelper {
+    method public abstract void onDrmConfig(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc);
+  }
+
+  public static class MediaPlayer2.ProvisioningNetworkErrorException extends android.media.MediaDrmException {
+    ctor public MediaPlayer2.ProvisioningNetworkErrorException(java.lang.String);
+  }
+
+  public static class MediaPlayer2.ProvisioningServerErrorException extends android.media.MediaDrmException {
+    ctor public MediaPlayer2.ProvisioningServerErrorException(java.lang.String);
+  }
+
+  public static abstract class MediaPlayer2.TrackInfo {
+    ctor public MediaPlayer2.TrackInfo();
+    method public abstract android.media.MediaFormat getFormat();
+    method public abstract java.lang.String getLanguage();
+    method public abstract int getTrackType();
+    method public abstract java.lang.String toString();
+    field public static final int MEDIA_TRACK_TYPE_AUDIO = 2; // 0x2
+    field public static final int MEDIA_TRACK_TYPE_METADATA = 5; // 0x5
+    field public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4; // 0x4
+    field public static final int MEDIA_TRACK_TYPE_UNKNOWN = 0; // 0x0
+    field public static final int MEDIA_TRACK_TYPE_VIDEO = 1; // 0x1
+  }
+
+  public abstract class MediaPlayerInterface implements java.lang.AutoCloseable {
+    ctor public MediaPlayerInterface();
     method public abstract androidx.media.AudioAttributesCompat getAudioAttributes();
     method public long getBufferedPosition();
     method public abstract int getBufferingState();
@@ -827,7 +1091,7 @@
     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 registerPlayerEventCallback(java.util.concurrent.Executor, androidx.media.MediaPlayerInterface.PlayerEventCallback);
     method public abstract void reset();
     method public abstract void seekTo(long);
     method public abstract void setAudioAttributes(androidx.media.AudioAttributesCompat);
@@ -837,7 +1101,7 @@
     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);
+    method public abstract void unregisterPlayerEventCallback(androidx.media.MediaPlayerInterface.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
@@ -849,14 +1113,14 @@
     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 static abstract class MediaPlayerInterface.PlayerEventCallback {
+    ctor public MediaPlayerInterface.PlayerEventCallback();
+    method public void onBufferingStateChanged(androidx.media.MediaPlayerInterface, androidx.media.DataSourceDesc, int);
+    method public void onCurrentDataSourceChanged(androidx.media.MediaPlayerInterface, androidx.media.DataSourceDesc);
+    method public void onMediaPrepared(androidx.media.MediaPlayerInterface, androidx.media.DataSourceDesc);
+    method public void onPlaybackSpeedChanged(androidx.media.MediaPlayerInterface, float);
+    method public void onPlayerStateChanged(androidx.media.MediaPlayerInterface, int);
+    method public void onSeekCompleted(androidx.media.MediaPlayerInterface, long);
   }
 
   public abstract class MediaPlaylistAgent {
@@ -911,7 +1175,7 @@
     method public long getCurrentPosition();
     method public long getDuration();
     method public float getPlaybackSpeed();
-    method public androidx.media.MediaPlayerBase getPlayer();
+    method public androidx.media.MediaPlayerInterface getPlayer();
     method public int getPlayerState();
     method public java.util.List<androidx.media.MediaItem2> getPlaylist();
     method public androidx.media.MediaPlaylistAgent getPlaylistAgent();
@@ -942,7 +1206,7 @@
     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 updatePlayer(androidx.media.MediaPlayerInterface, 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
@@ -963,7 +1227,7 @@
     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 setPlayer(androidx.media.MediaPlayerInterface);
     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);
@@ -999,19 +1263,19 @@
 
   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 void onBufferingStateChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerInterface, 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 onCurrentMediaItemChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerInterface, 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 onMediaPrepared(androidx.media.MediaSession2, androidx.media.MediaPlayerInterface, 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 onPlaybackSpeedChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerInterface, float);
+    method public void onPlayerStateChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerInterface, 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);
@@ -1019,7 +1283,7 @@
     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 onSeekCompleted(androidx.media.MediaSession2, androidx.media.MediaPlayerInterface, 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);
@@ -1027,6 +1291,35 @@
     method public void onUnsubscribeRoutesInfo(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo);
   }
 
+  public final class MediaSessionManager {
+    method public static androidx.media.MediaSessionManager getSessionManager(android.content.Context);
+    method public boolean isTrustedForMediaControl(androidx.media.MediaSessionManager.RemoteUserInfo);
+  }
+
+  public static final class MediaSessionManager.RemoteUserInfo {
+    ctor public MediaSessionManager.RemoteUserInfo(java.lang.String, int, int);
+    method public java.lang.String getPackageName();
+    method public int getPid();
+    method public int getUid();
+    field public static java.lang.String LEGACY_CONTROLLER;
+  }
+
+  public abstract class MediaSessionService2 extends android.app.Service {
+    ctor public MediaSessionService2();
+    method public final androidx.media.MediaSession2 getSession();
+    method public android.os.IBinder onBind(android.content.Intent);
+    method public abstract androidx.media.MediaSession2 onCreateSession(java.lang.String);
+    method public androidx.media.MediaSessionService2.MediaNotification onUpdateNotification();
+    field public static final java.lang.String SERVICE_INTERFACE = "android.media.MediaSessionService2";
+    field public static final java.lang.String SERVICE_META_DATA = "android.media.session";
+  }
+
+  public static class MediaSessionService2.MediaNotification {
+    ctor public MediaSessionService2.MediaNotification(int, android.app.Notification);
+    method public android.app.Notification getNotification();
+    method public int getNotificationId();
+  }
+
   public final class Rating2 {
     method public static androidx.media.Rating2 fromBundle(android.os.Bundle);
     method public float getPercentRating();
@@ -1057,6 +1350,13 @@
     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_LIBRARY_GET_CHILDREN = 29; // 0x1d
+    field public static final int COMMAND_CODE_LIBRARY_GET_ITEM = 30; // 0x1e
+    field public static final int COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT = 31; // 0x1f
+    field public static final int COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT = 32; // 0x20
+    field public static final int COMMAND_CODE_LIBRARY_SEARCH = 33; // 0x21
+    field public static final int COMMAND_CODE_LIBRARY_SUBSCRIBE = 34; // 0x22
+    field public static final int COMMAND_CODE_LIBRARY_UNSUBSCRIBE = 35; // 0x23
     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
@@ -1106,6 +1406,7 @@
   }
 
   public final class SessionToken2 {
+    ctor public SessionToken2(android.content.Context, android.content.ComponentName);
     method public static androidx.media.SessionToken2 fromBundle(android.os.Bundle);
     method public java.lang.String getId();
     method public java.lang.String getPackageName();
@@ -1113,7 +1414,9 @@
     method public int getType();
     method public int getUid();
     method public android.os.Bundle toBundle();
+    field public static final int TYPE_LIBRARY_SERVICE = 2; // 0x2
     field public static final int TYPE_SESSION = 0; // 0x0
+    field public static final int TYPE_SESSION_SERVICE = 1; // 0x1
   }
 
   public abstract class VolumeProviderCompat {
diff --git a/media/src/androidTest/java/androidx/media/MediaBrowser2Test.java b/media/src/androidTest/java/androidx/media/MediaBrowser2Test.java
index 4b75986..211b5a9 100644
--- a/media/src/androidTest/java/androidx/media/MediaBrowser2Test.java
+++ b/media/src/androidTest/java/androidx/media/MediaBrowser2Test.java
@@ -29,9 +29,12 @@
 import static org.junit.Assert.assertNotEquals;
 
 import android.content.Context;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Process;
 import android.os.ResultReceiver;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SdkSuppress;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 
@@ -65,17 +68,15 @@
  * {@link MediaController2} works cleanly.
  */
 // TODO(jaewan): Implement host-side test so browser and service can run in different processes.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
 @RunWith(AndroidJUnit4.class)
 @SmallTest
-@Ignore
 public class MediaBrowser2Test extends MediaController2Test {
     private static final String TAG = "MediaBrowser2Test";
 
     @Override
     TestControllerInterface onCreateController(final @NonNull SessionToken2 token,
-            @Nullable ControllerCallback callback) throws InterruptedException {
-        final BrowserCallback browserCallback =
-                callback != null ? (BrowserCallback) callback : new BrowserCallback() {};
+            final @Nullable ControllerCallback callback) throws InterruptedException {
         final AtomicReference<TestControllerInterface> controller = new AtomicReference<>();
         sHandler.postAndSync(new Runnable() {
             @Override
@@ -84,7 +85,7 @@
                 // Looper. Otherwise, MediaBrowserCompat will post all the commands to the handler
                 // and commands wouldn't be run if tests codes waits on the test handler.
                 controller.set(new TestMediaBrowser(
-                        mContext, token, new TestBrowserCallback(browserCallback)));
+                        mContext, token, new TestBrowserCallback(callback)));
             }
         });
         return controller.get();
@@ -120,7 +121,9 @@
                     Bundle rootHints, String rootMediaId, Bundle rootExtra) {
                 assertTrue(TestUtils.equals(param, rootHints));
                 assertEquals(ROOT_ID, rootMediaId);
-                assertTrue(TestUtils.equals(EXTRAS, rootExtra));
+                // Note that TestUtils#equals() cannot be used for this because
+                // MediaBrowserServiceCompat adds extra_client_version to the rootHints.
+                assertTrue(TestUtils.contains(rootExtra, EXTRAS));
                 latch.countDown();
             }
         };
@@ -259,7 +262,6 @@
         assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
     }
 
-    @Ignore
     @Test
     public void testSearch() throws InterruptedException {
         prepareLooper();
@@ -317,6 +319,7 @@
     }
 
     @Test
+    @LargeTest
     public void testSearchTakesTime() throws InterruptedException {
         prepareLooper();
         final String query = MockMediaLibraryService2.SEARCH_QUERY_TAKES_TIME;
@@ -418,82 +421,201 @@
     }
 
     @Test
-    public void testBrowserCallback_notifyChildrenChanged() throws InterruptedException {
+    public void testBrowserCallback_onChildrenChangedIsNotCalledWhenNotSubscribed()
+            throws InterruptedException {
+        // This test uses MediaLibrarySession.notifyChildrenChanged().
         prepareLooper();
-        // TODO(jaewan): Add test for the notifyChildrenChanged itself.
-        final String testParentId1 = "testBrowserCallback_notifyChildrenChanged_unexpectedParent";
-        final String testParentId2 = "testBrowserCallback_notifyChildrenChanged";
+        final String subscribedMediaId = "subscribedMediaId";
+        final String anotherMediaId = "anotherMediaId";
         final int testChildrenCount = 101;
-        final Bundle testExtras = new Bundle();
-        testExtras.putString(testParentId1, testParentId1);
+        final CountDownLatch latch = new CountDownLatch(1);
 
-        final CountDownLatch latch = new CountDownLatch(3);
-        final MediaLibrarySessionCallback sessionCallback =
-                new MediaLibrarySessionCallback() {
-                    @Override
-                    public SessionCommandGroup2 onConnect(@NonNull MediaSession2 session,
-                            @NonNull ControllerInfo controller) {
-                        if (Process.myUid() == controller.getUid()) {
-                            assertTrue(session instanceof MediaLibrarySession);
-                            if (mSession != null) {
-                                mSession.close();
-                            }
-                            mSession = session;
-                            // Shouldn't trigger onChildrenChanged() for the browser, because it
-                            // hasn't subscribed.
-                            ((MediaLibrarySession) session).notifyChildrenChanged(
-                                    testParentId1, testChildrenCount, null);
-                            ((MediaLibrarySession) session).notifyChildrenChanged(
-                                    controller, testParentId1, testChildrenCount, null);
-                        }
-                        return super.onConnect(session, controller);
-                    }
+        final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
+            @Override
+            public void onSubscribe(@NonNull MediaLibrarySession session,
+                    @NonNull ControllerInfo controller, @NonNull String parentId,
+                    @Nullable Bundle extras) {
+                if (Process.myUid() == controller.getUid()) {
+                    // Shouldn't trigger onChildrenChanged() for the browser,
+                    // because the browser didn't subscribe this media id.
+                    session.notifyChildrenChanged(anotherMediaId, testChildrenCount, null);
+                }
+            }
 
-                    @Override
-                    public void onSubscribe(@NonNull MediaLibrarySession session,
-                            @NonNull ControllerInfo info, @NonNull String parentId,
-                            @Nullable Bundle extras) {
-                        if (Process.myUid() == info.getUid()) {
-                            session.notifyChildrenChanged(testParentId2, testChildrenCount, null);
-                            session.notifyChildrenChanged(info, testParentId2, testChildrenCount,
-                                    testExtras);
-                        }
-                    }
+            @Override
+            public List<MediaItem2> onGetChildren(MediaLibrarySession session,
+                    ControllerInfo controller, String parentId, int page, int pageSize,
+                    Bundle extras) {
+                return TestUtils.createPlaylist(testChildrenCount);
+            }
         };
-        final BrowserCallback controllerCallbackProxy =
-                new BrowserCallback() {
-                    @Override
-                    public void onChildrenChanged(MediaBrowser2 browser, String parentId,
-                            int itemCount, Bundle extras) {
-                        switch ((int) latch.getCount()) {
-                            case 3:
-                                assertEquals(testParentId2, parentId);
-                                assertEquals(testChildrenCount, itemCount);
-                                assertNull(extras);
-                                latch.countDown();
-                                break;
-                            case 2:
-                                assertEquals(testParentId2, parentId);
-                                assertEquals(testChildrenCount, itemCount);
-                                assertTrue(TestUtils.equals(testExtras, extras));
-                                latch.countDown();
-                                break;
-                            default:
-                                // Unexpected call.
-                                fail();
-                        }
-                    }
-                };
+        final BrowserCallback controllerCallbackProxy = new BrowserCallback() {
+            @Override
+            public void onChildrenChanged(MediaBrowser2 browser, String parentId,
+                    int itemCount, Bundle extras) {
+                // Unexpected call.
+                fail();
+            }
+        };
+
         TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
         final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
         final MediaBrowser2 browser = (MediaBrowser2) createController(
                 token, true, controllerCallbackProxy);
-        assertTrue(mSession instanceof MediaLibrarySession);
-        browser.subscribe(testParentId2, null);
-        // This ensures that onChildrenChanged() is only called for the expected reasons.
+        browser.subscribe(subscribedMediaId, null);
+
+        // onChildrenChanged() should not be called.
         assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
     }
 
+    @Test
+    public void testBrowserCallback_onChildrenChangedIsCalledWhenSubscribed()
+            throws InterruptedException {
+        // This test uses MediaLibrarySession.notifyChildrenChanged().
+        prepareLooper();
+        final String expectedParentId = "expectedParentId";
+        final int testChildrenCount = 101;
+        final Bundle testExtras = TestUtils.createTestBundle();
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
+            @Override
+            public void onSubscribe(@NonNull MediaLibrarySession session,
+                    @NonNull ControllerInfo controller, @NonNull String parentId,
+                    @Nullable Bundle extras) {
+                if (Process.myUid() == controller.getUid()) {
+                    // Should trigger onChildrenChanged() for the browser.
+                    session.notifyChildrenChanged(expectedParentId, testChildrenCount, testExtras);
+                }
+            }
+
+            @Override
+            public List<MediaItem2> onGetChildren(MediaLibrarySession session,
+                    ControllerInfo controller, String parentId, int page, int pageSize,
+                    Bundle extras) {
+                return TestUtils.createPlaylist(testChildrenCount);
+            }
+        };
+        final BrowserCallback controllerCallbackProxy = new BrowserCallback() {
+            @Override
+            public void onChildrenChanged(MediaBrowser2 browser, String parentId,
+                    int itemCount, Bundle extras) {
+                assertEquals(expectedParentId, parentId);
+                assertEquals(testChildrenCount, itemCount);
+                assertTrue(TestUtils.equals(testExtras, extras));
+                latch.countDown();
+            }
+        };
+
+        TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
+        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+        final MediaBrowser2 browser = (MediaBrowser2) createController(
+                token, true, controllerCallbackProxy);
+        browser.subscribe(expectedParentId, null);
+
+        // onChildrenChanged() should be called.
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testBrowserCallback_onChildrenChangedIsNotCalledWhenNotSubscribed2()
+            throws InterruptedException {
+        // This test uses MediaLibrarySession.notifyChildrenChanged(ControllerInfo).
+        prepareLooper();
+        final String subscribedMediaId = "subscribedMediaId";
+        final String anotherMediaId = "anotherMediaId";
+        final int testChildrenCount = 101;
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
+            @Override
+            public void onSubscribe(@NonNull MediaLibrarySession session,
+                    @NonNull ControllerInfo controller, @NonNull String parentId,
+                    @Nullable Bundle extras) {
+                if (Process.myUid() == controller.getUid()) {
+                    // Shouldn't trigger onChildrenChanged() for the browser,
+                    // because the browser didn't subscribe this media id.
+                    session.notifyChildrenChanged(
+                            controller, anotherMediaId, testChildrenCount, null);
+                }
+            }
+
+            @Override
+            public List<MediaItem2> onGetChildren(MediaLibrarySession session,
+                    ControllerInfo controller, String parentId, int page, int pageSize,
+                    Bundle extras) {
+                return TestUtils.createPlaylist(testChildrenCount);
+            }
+        };
+        final BrowserCallback controllerCallbackProxy = new BrowserCallback() {
+            @Override
+            public void onChildrenChanged(MediaBrowser2 browser, String parentId,
+                    int itemCount, Bundle extras) {
+                // Unexpected call.
+                fail();
+            }
+        };
+
+        TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
+        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+        final MediaBrowser2 browser = (MediaBrowser2) createController(
+                token, true, controllerCallbackProxy);
+        browser.subscribe(subscribedMediaId, null);
+
+        // onChildrenChanged() should not be called.
+        assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testBrowserCallback_onChildrenChangedIsCalledWhenSubscribed2()
+            throws InterruptedException {
+        // This test uses MediaLibrarySession.notifyChildrenChanged(ControllerInfo).
+        prepareLooper();
+        final String expectedParentId = "expectedParentId";
+        final int testChildrenCount = 101;
+        final Bundle testExtras = TestUtils.createTestBundle();
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
+            @Override
+            public void onSubscribe(@NonNull MediaLibrarySession session,
+                    @NonNull ControllerInfo controller, @NonNull String parentId,
+                    @Nullable Bundle extras) {
+                if (Process.myUid() == controller.getUid()) {
+                    // Should trigger onChildrenChanged() for the browser.
+                    session.notifyChildrenChanged(
+                            controller, expectedParentId, testChildrenCount, testExtras);
+                }
+            }
+
+            @Override
+            public List<MediaItem2> onGetChildren(MediaLibrarySession session,
+                    ControllerInfo controller, String parentId, int page, int pageSize,
+                    Bundle extras) {
+                return TestUtils.createPlaylist(testChildrenCount);
+            }
+        };
+        final BrowserCallback controllerCallbackProxy = new BrowserCallback() {
+            @Override
+            public void onChildrenChanged(MediaBrowser2 browser, String parentId,
+                    int itemCount, Bundle extras) {
+                assertEquals(expectedParentId, parentId);
+                assertEquals(testChildrenCount, itemCount);
+                assertTrue(TestUtils.equals(testExtras, extras));
+                latch.countDown();
+            }
+        };
+
+        TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
+        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+        final MediaBrowser2 browser = (MediaBrowser2) createController(
+                token, true, controllerCallbackProxy);
+        browser.subscribe(expectedParentId, null);
+
+        // onChildrenChanged() should be called.
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
     public static class TestBrowserCallback extends BrowserCallback
             implements TestControllerCallbackInterface {
         private final ControllerCallback mCallbackProxy;
@@ -504,7 +626,7 @@
 
         TestBrowserCallback(ControllerCallback callbackProxy) {
             if (callbackProxy == null) {
-                throw new IllegalArgumentException("Callback proxy shouldn't be null. Test bug");
+                callbackProxy = new BrowserCallback() {};
             }
             mCallbackProxy = callbackProxy;
         }
@@ -690,9 +812,9 @@
         private final BrowserCallback mCallback;
 
         public TestMediaBrowser(@NonNull Context context, @NonNull SessionToken2 token,
-                @NonNull ControllerCallback callback) {
-            super(context, token, sHandlerExecutor, (BrowserCallback) callback);
-            mCallback = (BrowserCallback) callback;
+                @NonNull BrowserCallback callback) {
+            super(context, token, sHandlerExecutor, callback);
+            mCallback = callback;
         }
 
         @Override
diff --git a/media/src/androidTest/java/androidx/media/MediaController2Test.java b/media/src/androidTest/java/androidx/media/MediaController2Test.java
index 75c9e50..072381e 100644
--- a/media/src/androidTest/java/androidx/media/MediaController2Test.java
+++ b/media/src/androidTest/java/androidx/media/MediaController2Test.java
@@ -25,6 +25,7 @@
 import static org.junit.Assert.fail;
 
 import android.app.PendingIntent;
+import android.content.Context;
 import android.content.Intent;
 import android.media.AudioManager;
 import android.net.Uri;
@@ -41,15 +42,16 @@
 
 import androidx.annotation.NonNull;
 import androidx.media.MediaController2.ControllerCallback;
+import androidx.media.MediaController2.PlaybackInfo;
 import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
 import androidx.media.MediaSession2.ControllerInfo;
 import androidx.media.MediaSession2.SessionCallback;
 import androidx.media.TestServiceRegistry.SessionServiceCallback;
 import androidx.media.TestUtils.SyncHandler;
+import androidx.testutils.PollingCheck;
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -65,7 +67,7 @@
 // TODO(jaewan): Implement host-side test so controller and session can run in different processes.
 // TODO(jaewan): Fix flaky failure -- see MediaController2Impl.getController()
 // TODO(jaeawn): Revisit create/close session in the sHandler. It's no longer necessary.
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 @FlakyTest
@@ -77,6 +79,7 @@
     MediaController2 mController;
     MockPlayer mPlayer;
     MockPlaylistAgent mMockAgent;
+    AudioManager mAudioManager;
 
     @Before
     @Override
@@ -111,6 +114,7 @@
                 .setSessionActivity(mIntent)
                 .setId(TAG).build();
         mController = createController(mSession.getToken());
+        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
         TestServiceRegistry.getInstance().setHandler(sHandler);
     }
 
@@ -207,11 +211,12 @@
     @Test
     public void testGettersAfterConnected() throws InterruptedException {
         prepareLooper();
-        final int state = MediaPlayerBase.PLAYER_STATE_PLAYING;
-        final int bufferingState = MediaPlayerBase.BUFFERING_STATE_BUFFERING_COMPLETE;
+        final int state = MediaPlayerInterface.PLAYER_STATE_PLAYING;
+        final int bufferingState = MediaPlayerInterface.BUFFERING_STATE_BUFFERING_COMPLETE;
         final long position = 150000;
         final long bufferedPosition = 900000;
         final float speed = 0.5f;
+        final long timeDiff = 102;
         final MediaItem2 currentMediaItem = TestUtils.createMediaItemWithMetadata();
 
         mPlayer.mLastPlayerState = state;
@@ -221,22 +226,60 @@
         mPlayer.mPlaybackSpeed = speed;
         mMockAgent.mCurrentMediaItem = currentMediaItem;
 
-        long time1 = System.currentTimeMillis();
         MediaController2 controller = createController(mSession.getToken());
-        long time2 = System.currentTimeMillis();
+        controller.setTimeDiff(timeDiff);
         assertEquals(state, controller.getPlayerState());
         assertEquals(bufferedPosition, controller.getBufferedPosition());
         assertEquals(speed, controller.getPlaybackSpeed(), 0.0f);
-        long positionLowerBound = (long) (position + speed * (System.currentTimeMillis() - time2));
-        long currentPosition = controller.getCurrentPosition();
-        long positionUpperBound = (long) (position + speed * (System.currentTimeMillis() - time1));
-        assertTrue("curPos=" + currentPosition + ", lowerBound=" + positionLowerBound
-                        + ", upperBound=" + positionUpperBound,
-                positionLowerBound <= currentPosition && currentPosition <= positionUpperBound);
+        assertEquals(position + (long) (speed * timeDiff), controller.getCurrentPosition());
         assertEquals(currentMediaItem, controller.getCurrentMediaItem());
     }
 
     @Test
+    public void testUpdatePlayer() throws InterruptedException {
+        prepareLooper();
+        final int testState = MediaPlayerInterface.PLAYER_STATE_PLAYING;
+        final List<MediaItem2> testPlaylist = TestUtils.createPlaylist(3);
+        final AudioAttributesCompat testAudioAttributes = new AudioAttributesCompat.Builder()
+                .setLegacyStreamType(AudioManager.STREAM_RING).build();
+        final CountDownLatch latch = new CountDownLatch(3);
+        mController = createController(mSession.getToken(), true, new ControllerCallback() {
+            @Override
+            public void onPlayerStateChanged(MediaController2 controller, int state) {
+                assertEquals(mController, controller);
+                assertEquals(testState, state);
+                latch.countDown();
+            }
+
+            @Override
+            public void onPlaylistChanged(MediaController2 controller, List<MediaItem2> list,
+                    MediaMetadata2 metadata) {
+                assertEquals(mController, controller);
+                assertEquals(testPlaylist, list);
+                assertNull(metadata);
+                latch.countDown();
+            }
+
+            @Override
+            public void onPlaybackInfoChanged(MediaController2 controller, PlaybackInfo info) {
+                assertEquals(mController, controller);
+                assertEquals(testAudioAttributes, info.getAudioAttributes());
+                latch.countDown();
+            }
+        });
+
+        MockPlayer player = new MockPlayer(0);
+        player.mLastPlayerState = testState;
+        player.setAudioAttributes(testAudioAttributes);
+
+        MockPlaylistAgent agent = new MockPlaylistAgent();
+        agent.mPlaylist = testPlaylist;
+
+        mSession.updatePlayer(player, agent, null);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
     public void testGetSessionActivity() {
         prepareLooper();
         PendingIntent sessionActivity = mController.getSessionActivity();
@@ -420,6 +463,77 @@
         }
     }
 
+
+    @Test
+    public void testControllerCallback_onSeekCompleted() throws InterruptedException {
+        prepareLooper();
+        final long testSeekPosition = 400;
+        final long testPosition = 500;
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onSeekCompleted(MediaController2 controller, long position) {
+                controller.setTimeDiff(Long.valueOf(0));
+                assertEquals(testSeekPosition, position);
+                assertEquals(testPosition, controller.getCurrentPosition());
+                latch.countDown();
+            }
+        };
+        final MediaController2 controller = createController(mSession.getToken(), true, callback);
+        mPlayer.mCurrentPosition = testPosition;
+        mPlayer.notifySeekCompleted(testSeekPosition);
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testControllerCallback_onBufferingStateChanged() throws InterruptedException {
+        prepareLooper();
+        final List<MediaItem2> testPlaylist = TestUtils.createPlaylist(3);
+        final MediaItem2 testItem = testPlaylist.get(0);
+        final int testBufferingState = MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_PLAYABLE;
+        final long testBufferingPosition = 500;
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onBufferingStateChanged(MediaController2 controller, MediaItem2 item,
+                    int state) {
+                controller.setTimeDiff(Long.valueOf(0));
+                assertEquals(testItem, item);
+                assertEquals(testBufferingState, state);
+                assertEquals(testBufferingState, controller.getBufferingState());
+                assertEquals(testBufferingPosition, controller.getBufferedPosition());
+                latch.countDown();
+            }
+        };
+        final MediaController2 controller = createController(mSession.getToken(), true, callback);
+        mSession.setPlaylist(testPlaylist, null);
+        mPlayer.mBufferedPosition = testBufferingPosition;
+        mPlayer.notifyBufferingStateChanged(testItem.getDataSourceDesc(), testBufferingState);
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testControllerCallback_onPlayerStateChanged() throws InterruptedException {
+        prepareLooper();
+        final int testPlayerState = MediaPlayerInterface.PLAYER_STATE_PLAYING;
+        final long testPosition = 500;
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onPlayerStateChanged(MediaController2 controller, int state) {
+                controller.setTimeDiff(Long.valueOf(0));
+                assertEquals(testPlayerState, state);
+                assertEquals(testPlayerState, controller.getPlayerState());
+                assertEquals(testPosition, controller.getCurrentPosition());
+                latch.countDown();
+            }
+        };
+        final MediaController2 controller = createController(mSession.getToken(), true, callback);
+        mPlayer.mCurrentPosition = testPosition;
+        mPlayer.notifyPlaybackState(testPlayerState);
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
     @Test
     public void testAddPlaylistItem() throws InterruptedException {
         prepareLooper();
@@ -572,7 +686,6 @@
 
     @Test
     public void testSetVolumeTo() throws Exception {
-        // TODO(jaewan): Also test with local volume.
         prepareLooper();
         final int maxVolume = 100;
         final int currentVolume = 23;
@@ -592,7 +705,6 @@
 
     @Test
     public void testAdjustVolume() throws Exception {
-        // TODO(jaewan): Also test with local volume.
         prepareLooper();
         final int maxVolume = 100;
         final int currentVolume = 23;
@@ -611,6 +723,87 @@
     }
 
     @Test
+    public void testSetVolumeWithLocalVolume() throws Exception {
+        prepareLooper();
+        if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
+            // This test is not eligible for this device.
+            return;
+        }
+
+        // Here, we intentionally choose STREAM_ALARM in order not to consider
+        // 'Do Not Disturb' or 'Volume limit'.
+        final int stream = AudioManager.STREAM_ALARM;
+        final int maxVolume = mAudioManager.getStreamMaxVolume(stream);
+        final int minVolume = 0;
+        if (maxVolume <= minVolume) {
+            return;
+        }
+
+        // Set stream of the session.
+        AudioAttributesCompat attrs = new AudioAttributesCompat.Builder()
+                .setLegacyStreamType(stream)
+                .build();
+        mPlayer.setAudioAttributes(attrs);
+        mSession.updatePlayer(mPlayer, null, null);
+
+        final int originalVolume = mAudioManager.getStreamVolume(stream);
+        final int targetVolume = originalVolume == minVolume
+                ? originalVolume + 1 : originalVolume - 1;
+
+        mController.setVolumeTo(targetVolume, AudioManager.FLAG_SHOW_UI);
+        new PollingCheck(WAIT_TIME_MS) {
+            @Override
+            protected boolean check() {
+                return targetVolume == mAudioManager.getStreamVolume(stream);
+            }
+        }.run();
+
+        // Set back to original volume.
+        mAudioManager.setStreamVolume(stream, originalVolume, 0 /* flags */);
+    }
+
+    @Test
+    public void testAdjustVolumeWithLocalVolume() throws Exception {
+        prepareLooper();
+        if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
+            // This test is not eligible for this device.
+            return;
+        }
+
+        // Here, we intentionally choose STREAM_ALARM in order not to consider
+        // 'Do Not Disturb' or 'Volume limit'.
+        final int stream = AudioManager.STREAM_ALARM;
+        final int maxVolume = mAudioManager.getStreamMaxVolume(stream);
+        final int minVolume = 0;
+        if (maxVolume <= minVolume) {
+            return;
+        }
+
+        // Set stream of the session.
+        AudioAttributesCompat attrs = new AudioAttributesCompat.Builder()
+                .setLegacyStreamType(stream)
+                .build();
+        mPlayer.setAudioAttributes(attrs);
+        mSession.updatePlayer(mPlayer, null, null);
+
+        final int originalVolume = mAudioManager.getStreamVolume(stream);
+        final int direction = originalVolume == minVolume
+                ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER;
+        final int targetVolume = originalVolume + direction;
+
+        mController.adjustVolume(direction, AudioManager.FLAG_SHOW_UI);
+        new PollingCheck(WAIT_TIME_MS) {
+            @Override
+            protected boolean check() {
+                return targetVolume == mAudioManager.getStreamVolume(stream);
+            }
+        }.run();
+
+        // Set back to original volume.
+        mAudioManager.setStreamVolume(stream, originalVolume, 0 /* flags */);
+    }
+
+    @Test
     public void testGetPackageName() {
         prepareLooper();
         assertEquals(mContext.getPackageName(), mController.getSessionToken().getPackageName());
@@ -979,7 +1172,7 @@
             testHandler.post(new Runnable() {
                 @Override
                 public void run() {
-                    final int state = MediaPlayerBase.PLAYER_STATE_ERROR;
+                    final int state = MediaPlayerInterface.PLAYER_STATE_ERROR;
                     for (int i = 0; i < 100; i++) {
                         // triggers call from session to controller.
                         player.notifyPlaybackState(state);
@@ -1012,11 +1205,13 @@
                     }
                 });
             }
-            if (sessionThread != null) {
+
+            if (Build.VERSION.SDK_INT >= 18) {
                 sessionThread.quitSafely();
-            }
-            if (testThread != null) {
                 testThread.quitSafely();
+            } else {
+                sessionThread.quit();
+                testThread.quit();
             }
         }
     }
@@ -1065,30 +1260,29 @@
         };
         TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
 
-        mController = createController(TestUtils.getServiceToken(mContext, id));
+        final SessionCommand2 testCommand = new SessionCommand2("testConnectToService", null);
+        final CountDownLatch controllerLatch = new CountDownLatch(1);
+        mController = createController(TestUtils.getServiceToken(mContext, id), true,
+                new ControllerCallback() {
+                    @Override
+                    public void onCustomCommand(MediaController2 controller,
+                            SessionCommand2 command, Bundle args, ResultReceiver receiver) {
+                        if (testCommand.equals(command)) {
+                            controllerLatch.countDown();
+                        }
+                    }
+                }
+        );
         assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
 
-        // Test command from controller to session service
-        // TODO: Re enable when transport control works
-        /*
+        // Test command from controller to session service.
         mController.play();
         assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         assertTrue(mPlayer.mPlayCalled);
-        */
 
-        // Test command from session service to controller
-        // TODO(jaewan): Add equivalent tests again
-        /*
-        final CountDownLatch latch = new CountDownLatch(1);
-        mController.registerPlayerEventCallback((state) -> {
-            assertNotNull(state);
-            assertEquals(PlaybackState.STATE_REWINDING, state.getState());
-            latch.countDown();
-        }, sHandler);
-        mPlayer.notifyPlaybackState(
-                TestUtils.createPlaybackState(PlaybackState.STATE_REWINDING));
-        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-        */
+        // Test command from session service to controller.
+        mSession.sendCustomCommand(testCommand, null);
+        assertTrue(controllerLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
     }
 
     @Test
@@ -1097,15 +1291,11 @@
         testControllerAfterSessionIsClosed(mSession.getToken().getId());
     }
 
-    // TODO(jaewan): Re-enable this test
-    @Ignore
     @Test
     public void testControllerAfterSessionIsClosed_sessionService() throws InterruptedException {
         prepareLooper();
-        /*
-        connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+        testConnectToService(MockMediaSessionService2.ID);
         testControllerAfterSessionIsClosed(MockMediaSessionService2.ID);
-        */
     }
 
     @Test
@@ -1192,14 +1382,12 @@
         testControllerAfterSessionIsClosed(id);
     }
 
-    @Ignore
     @Test
     public void testClose_sessionService() throws InterruptedException {
         prepareLooper();
         testCloseFromService(MockMediaSessionService2.ID);
     }
 
-    @Ignore
     @Test
     public void testClose_libraryService() throws InterruptedException {
         prepareLooper();
diff --git a/media/src/androidTest/java/androidx/media/MediaMetadata2Test.java b/media/src/androidTest/java/androidx/media/MediaMetadata2Test.java
index f000f02..d583e47 100644
--- a/media/src/androidTest/java/androidx/media/MediaMetadata2Test.java
+++ b/media/src/androidTest/java/androidx/media/MediaMetadata2Test.java
@@ -19,7 +19,9 @@
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertTrue;
 
+import android.os.Build;
 import android.os.Bundle;
+import android.support.test.filters.SdkSuppress;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 
@@ -28,6 +30,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class MediaMetadata2Test {
diff --git a/media/src/androidTest/java/androidx/media/MediaPlayer2Test.java b/media/src/androidTest/java/androidx/media/MediaPlayer2Test.java
index f565e8a..0a35ddb 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,10 +37,13 @@
 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.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.util.Log;
 
+import androidx.media.MediaPlayerInterface.PlayerEventCallback;
 import androidx.media.test.R;
 
 import org.junit.After;
@@ -49,14 +53,18 @@
 
 import java.io.BufferedReader;
 import java.io.File;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.util.ArrayDeque;
 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;
 
@@ -81,7 +89,7 @@
 
     @Before
     @Override
-    public void setUp() throws Exception {
+    public void setUp() throws Throwable {
         super.setUp();
         mRecordedFilePath = new File(Environment.getExternalStorageDirectory(),
                 "mediaplayer_record.out").getAbsolutePath();
@@ -97,61 +105,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() {
@@ -172,6 +127,8 @@
         onSetDataSourceCalled.waitForSignal();
     }
 
+    @Test
+    @LargeTest
     public void testPlayAudioFromDataURI() throws Exception {
         final int mp3Duration = 34909;
         final int tolerance = 70;
@@ -220,15 +177,12 @@
                     .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);
+            assertFalse(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
             onPlayCalled.reset();
             mp.play();
             onPlayCalled.waitForSignal();
-            assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            assertTrue(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
 
             /* FIXME: what's API for checking loop state?
             assertFalse(mp.isLooping());
@@ -253,11 +207,11 @@
             // test pause and restart
             mp.pause();
             Thread.sleep(SLEEP_TIME);
-            assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            assertFalse(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
             onPlayCalled.reset();
             mp.play();
             onPlayCalled.waitForSignal();
-            assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            assertTrue(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
 
             // test stop and restart
             mp.reset();
@@ -269,14 +223,14 @@
             mp.prepare();
             onPrepareCalled.waitForSignal();
 
-            assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            assertFalse(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
             onPlayCalled.reset();
             mp.play();
             onPlayCalled.waitForSignal();
-            assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            assertTrue(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
 
             // waiting to complete
-            while (mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+            while (mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING) {
                 Thread.sleep(SLEEP_TIME);
             }
         } finally {
@@ -284,6 +238,8 @@
         }
     }
 
+    @Test
+    @LargeTest
     public void testPlayAudio() throws Exception {
         final int resid = R.raw.testmp3_2;
         final int mp3Duration = 34909;
@@ -323,11 +279,11 @@
                     .build();
             mp.setAudioAttributes(attributes);
 
-            assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            assertFalse(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
             onPlayCalled.reset();
             mp.play();
             onPlayCalled.waitForSignal();
-            assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            assertTrue(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
 
             //assertFalse(mp.isLooping());
             onLoopCurrentCalled.reset();
@@ -348,11 +304,11 @@
             // test pause and restart
             mp.pause();
             Thread.sleep(SLEEP_TIME);
-            assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            assertFalse(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
             onPlayCalled.reset();
             mp.play();
             onPlayCalled.waitForSignal();
-            assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            assertTrue(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
 
             // test stop and restart
             mp.reset();
@@ -367,14 +323,14 @@
             onPrepareCalled.waitForSignal();
             afd.close();
 
-            assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            assertFalse(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
             onPlayCalled.reset();
             mp.play();
             onPlayCalled.waitForSignal();
-            assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            assertTrue(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
 
             // waiting to complete
-            while (mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+            while (mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING) {
                 Thread.sleep(SLEEP_TIME);
             }
         } catch (Exception e) {
@@ -414,7 +370,6 @@
                         .setInternalLegacyStreamType(AudioManager.STREAM_MUSIC)
                         .build();
                 mp.setAudioAttributes(attributes);
-                mp.setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK);
 
                 assertFalse(mp.isPlaying());
                 onPlayCalled.reset();
@@ -445,6 +400,8 @@
     }
     */
 
+    @Test
+    @LargeTest
     public void testPlayAudioLooping() throws Exception {
         final int resid = R.raw.testmp3;
 
@@ -462,8 +419,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
@@ -476,21 +435,21 @@
                     };
             mp.setMediaPlayer2EventCallback(mExecutor, ecb);
 
-            assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            assertFalse(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
             onPlayCalled.reset();
             mp.play();
             onPlayCalled.waitForSignal();
-            assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            assertTrue(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
 
             long duration = mp.getDuration();
             Thread.sleep(duration * 4); // allow for several loops
-            assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            assertTrue(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
             assertEquals("wrong number of completion signals", 0,
                     onCompletionCalled.getNumSignal());
             mp.loopCurrent(false);
 
             // wait for playback to finish
-            while (mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+            while (mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING) {
                 Thread.sleep(SLEEP_TIME);
             }
             assertEquals("wrong number of completion signals", 1,
@@ -500,6 +459,8 @@
         }
     }
 
+    @Test
+    @LargeTest
     public void testPlayMidi() throws Exception {
         final int resid = R.raw.midi8sec;
         final int midiDuration = 8000;
@@ -668,7 +629,7 @@
             mp.play();
             Thread.sleep(SLEEP_TIME);
             assertFalse("player was still playing after " + SLEEP_TIME + " ms",
-                    mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+                    mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
             assertTrue("nothing heard while test ran", listener.heardSound());
             listener.reset();
             mp.seekTo(0, MediaPlayer2.SEEK_PREVIOUS_SYNC);
@@ -730,7 +691,7 @@
          */
         // TODO: uncomment out line below when MediaPlayer2 can seek to requested position.
         // assertEquals(posAfter, posBefore, tolerance);
-        assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+        assertTrue(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
 
         Thread.sleep(SLEEP_TIME);
 
@@ -745,7 +706,7 @@
         posAfter = mPlayer.getCurrentPosition();
         // TODO: uncomment out line below when MediaPlayer2 can seek to requested position.
         // assertEquals(posAfter, posBefore, tolerance);
-        assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+        assertTrue(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
 
         Thread.sleep(SLEEP_TIME);
 
@@ -755,7 +716,7 @@
 
         // TODO: uncomment out line below when MediaPlayer2 can seek to requested position.
         // assertEquals(posAfter, posBefore, tolerance);
-        assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+        assertTrue(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
 
         Thread.sleep(SLEEP_TIME);
     }
@@ -867,7 +828,19 @@
         assertEquals(Integer.parseInt(rotation), angle);
     }
 
+    @Test
+    @LargeTest
+    public void testSkipToNext() throws Exception {
+        testPlaylist(true);
+    }
+
+    @Test
+    @LargeTest
     public void testPlaylist() throws Exception {
+        testPlaylist(false);
+    }
+
+    private void testPlaylist(boolean skip) throws Exception {
         if (!checkLoadResource(
                 R.raw.video_480x360_mp4_h264_1000kbps_30fps_aac_stereo_128kbps_44100hz)) {
             return; // skip
@@ -915,9 +888,18 @@
 
         mPlayer.play();
 
-        mOnCompletionCalled.waitForSignal();
-        onCompletion2Called.waitForSignal();
+        if (skip) {
+            mPlayer.skipToNext();
+            mPlayer.skipToNext();
+        } else {
+            mOnCompletionCalled.waitForSignal();
+            onCompletion2Called.waitForSignal();
+        }
         onCompletion1Called.waitForSignal();
+        if (skip) {
+            assertFalse("first dsd completed", mOnCompletionCalled.isSignalled());
+            assertFalse("second dsd completed", onCompletion2Called.isSignalled());
+        }
 
         mPlayer.reset();
     }
@@ -1035,7 +1017,7 @@
                     playbackRate, pbp.getSpeed(),
                     FLOAT_TOLERANCE + playbackRate * sync.getTolerance());
             assertTrue("MediaPlayer2 should still be playing",
-                    mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+                    mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
 
             long playedMediaDurationMs = mPlayer.getCurrentPosition();
             int diff = Math.abs((int) (playedMediaDurationMs / playbackRate) - playTime);
@@ -1452,14 +1434,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
@@ -1482,21 +1464,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());
 
@@ -1507,7 +1486,7 @@
         mOnPlayCalled.reset();
         mPlayer.play();
         mOnPlayCalled.waitForSignal();
-        assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+        assertTrue(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
 
         // Closed caption tracks are in-band.
         // So, those tracks will be found after processing a number of frames.
@@ -1539,22 +1518,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) {
@@ -1571,6 +1541,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);
@@ -1585,7 +1563,7 @@
         mOnPlayCalled.reset();
         mPlayer.play();
         mOnPlayCalled.waitForSignal();
-        assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+        assertTrue(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
 
         // Closed caption tracks are in-band.
         // So, those tracks will be found after processing a number of frames.
@@ -1644,7 +1622,7 @@
         mOnPlayCalled.reset();
         mPlayer.play();
         mOnPlayCalled.waitForSignal();
-        assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+        assertTrue(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
 
         // The media metadata will be changed while playing since closed caption tracks are in-band
         // and those tracks will be found after processing a number of frames. These tracks will be
@@ -1660,10 +1638,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)
@@ -1704,7 +1749,7 @@
         // sleep long enough that we restart playback at least once, but no more
         Thread.sleep(10000);
         assertTrue("MediaPlayer2 should still be playing",
-                mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+                mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
         mPlayer.reset();
         assertEquals("wrong number of repetitions", 1, mOnCompletionCalled.getNumSignal());
         return 1;
@@ -1763,7 +1808,7 @@
         mOnPlayCalled.reset();
         mPlayer.play();
         mOnPlayCalled.waitForSignal();
-        while (mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+        while (mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING) {
             Log.i("@@@@", "position: " + mPlayer.getCurrentPosition());
             Thread.sleep(500);
         }
@@ -1834,10 +1879,10 @@
         assertFalse(mOnCompletionCalled.isSignalled());
         mPlayer.play();
         mOnPlayCalled.waitForSignal();
-        while (mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+        while (mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING) {
             Thread.sleep(SLEEP_TIME);
         }
-        assertFalse(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+        assertFalse(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
         mOnCompletionCalled.waitForSignal();
         assertFalse(mOnErrorCalled.isSignalled());
         mPlayer.reset();
@@ -1873,32 +1918,34 @@
             mEventCallbacks.add(ecb);
         }
 
-        assertEquals(MediaPlayerBase.BUFFERING_STATE_UNKNOWN, mPlayer.getBufferingState());
-        assertEquals(MediaPlayerBase.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
+        MediaPlayerInterface playerBase = mPlayer.getMediaPlayerInterface();
+        assertEquals(MediaPlayerInterface.BUFFERING_STATE_UNKNOWN, playerBase.getBufferingState());
+        assertEquals(MediaPlayerInterface.PLAYER_STATE_IDLE, playerBase.getPlayerState());
         prepareCompleted.reset();
-        mPlayer.prepare();
+        playerBase.prepare();
         prepareCompleted.waitForSignal();
-        assertEquals(MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
-                mPlayer.getBufferingState());
-        assertEquals(MediaPlayerBase.PLAYER_STATE_PAUSED, mPlayer.getPlayerState());
+        assertEquals(MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+                playerBase.getBufferingState());
+        assertEquals(MediaPlayerInterface.PLAYER_STATE_PAUSED, playerBase.getPlayerState());
+        assertEquals(MediaPlayer2.MEDIAPLAYER2_STATE_PREPARED, mPlayer.getMediaPlayer2State());
 
         playCompleted.reset();
-        mPlayer.play();
+        playerBase.play();
         playCompleted.waitForSignal();
-        assertEquals(MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
-                mPlayer.getBufferingState());
-        assertEquals(MediaPlayerBase.PLAYER_STATE_PLAYING, mPlayer.getPlayerState());
+        assertEquals(MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+                playerBase.getBufferingState());
+        assertEquals(MediaPlayerInterface.PLAYER_STATE_PLAYING, playerBase.getPlayerState());
 
         pauseCompleted.reset();
-        mPlayer.pause();
+        playerBase.pause();
         pauseCompleted.waitForSignal();
-        assertEquals(MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
-                mPlayer.getBufferingState());
-        assertEquals(MediaPlayerBase.PLAYER_STATE_PAUSED, mPlayer.getPlayerState());
+        assertEquals(MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+                playerBase.getBufferingState());
+        assertEquals(MediaPlayerInterface.PLAYER_STATE_PAUSED, playerBase.getPlayerState());
 
-        mPlayer.reset();
-        assertEquals(MediaPlayerBase.BUFFERING_STATE_UNKNOWN, mPlayer.getBufferingState());
-        assertEquals(MediaPlayerBase.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
+        playerBase.reset();
+        assertEquals(MediaPlayerInterface.BUFFERING_STATE_UNKNOWN, playerBase.getBufferingState());
+        assertEquals(MediaPlayerInterface.PLAYER_STATE_IDLE, playerBase.getPlayerState());
     }
 
     @Test
@@ -1909,9 +1956,13 @@
         if (!checkLoadResource(R.raw.testvideo)) {
             return; // skip;
         }
+        final DataSourceDesc dsd2 = createDataSourceDesc(
+                R.raw.video_480x360_mp4_h264_1000kbps_30fps_aac_stereo_128kbps_44100hz);
+        mPlayer.setNextDataSource(dsd2);
 
         mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
 
+        final Monitor onDsdChangedCalled = new Monitor();
         final Monitor onPrepareCalled = new Monitor();
         final Monitor onSeekCompleteCalled = new Monitor();
         final Monitor onPlayerStateChangedCalled = new Monitor();
@@ -1921,69 +1972,79 @@
         final Monitor onPlaybackSpeedChanged = new Monitor();
         final AtomicReference<Float> playbackSpeed = new AtomicReference<>();
 
-        MediaPlayerBase.PlayerEventCallback callback = new MediaPlayerBase.PlayerEventCallback() {
-            // TODO: implement and add test case for onCurrentDataSourceChanged() callback.
+        PlayerEventCallback callback = new PlayerEventCallback() {
             @Override
-            public void onMediaPrepared(MediaPlayerBase mpb, DataSourceDesc dsd) {
+            public void onCurrentDataSourceChanged(MediaPlayerInterface mpb, DataSourceDesc dsd) {
+                onDsdChangedCalled.signal();
+            }
+
+            @Override
+            public void onMediaPrepared(MediaPlayerInterface mpb, DataSourceDesc dsd) {
                 onPrepareCalled.signal();
             }
 
             @Override
-            public void onPlayerStateChanged(MediaPlayerBase mpb, int state) {
+            public void onPlayerStateChanged(MediaPlayerInterface mpb, int state) {
                 playerState.set(state);
                 onPlayerStateChangedCalled.signal();
             }
 
             @Override
-            public void onBufferingStateChanged(MediaPlayerBase mpb, DataSourceDesc dsd,
+            public void onBufferingStateChanged(MediaPlayerInterface mpb, DataSourceDesc dsd,
                     int state) {
                 bufferingState.set(state);
                 onBufferingStateChangedCalled.signal();
             }
 
             @Override
-            public void onPlaybackSpeedChanged(MediaPlayerBase mpb, float speed) {
+            public void onPlaybackSpeedChanged(MediaPlayerInterface mpb, float speed) {
                 playbackSpeed.set(speed);
                 onPlaybackSpeedChanged.signal();
             }
 
             @Override
-            public void onSeekCompleted(MediaPlayerBase mpb, long position) {
+            public void onSeekCompleted(MediaPlayerInterface mpb, long position) {
                 onSeekCompleteCalled.signal();
             }
         };
+        MediaPlayerInterface basePlayer = mPlayer.getMediaPlayerInterface();
         ExecutorService executor = Executors.newFixedThreadPool(1);
-        mPlayer.registerPlayerEventCallback(executor, callback);
+        basePlayer.registerPlayerEventCallback(executor, callback);
 
         onPrepareCalled.reset();
         onPlayerStateChangedCalled.reset();
         onBufferingStateChangedCalled.reset();
-        mPlayer.prepare();
+        basePlayer.prepare();
         do {
             assertTrue(onBufferingStateChangedCalled.waitForSignal(1000));
-        } while (bufferingState.get() != MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_STARVED);
+        } while (bufferingState.get()
+                != MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_STARVED);
 
         assertTrue(onPrepareCalled.waitForSignal(1000));
         do {
             assertTrue(onPlayerStateChangedCalled.waitForSignal(1000));
-        } while (playerState.get() != MediaPlayerBase.PLAYER_STATE_PAUSED);
+        } while (playerState.get() != MediaPlayerInterface.PLAYER_STATE_PAUSED);
         do {
             assertTrue(onBufferingStateChangedCalled.waitForSignal(1000));
-        } while (bufferingState.get() != MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
+        } while (bufferingState.get()
+                != MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
 
         onSeekCompleteCalled.reset();
-        mPlayer.seekTo(mp4Duration >> 1, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+        basePlayer.seekTo(mp4Duration >> 1);
         onSeekCompleteCalled.waitForSignal();
 
         onPlaybackSpeedChanged.reset();
-        mPlayer.setPlaybackSpeed(0.5f);
+        basePlayer.setPlaybackSpeed(0.5f);
         do {
             assertTrue(onPlaybackSpeedChanged.waitForSignal(1000));
         } while (Math.abs(playbackSpeed.get() - 0.5f) > FLOAT_TOLERANCE);
 
-        mPlayer.reset();
+        basePlayer.skipToNext();
+        assertTrue(onDsdChangedCalled.waitForSignal(1000));
 
-        mPlayer.unregisterPlayerEventCallback(callback);
+        basePlayer.reset();
+
+        basePlayer.unregisterPlayerEventCallback(callback);
         executor.shutdown();
     }
 
@@ -2087,12 +2148,12 @@
                 .setDataSource(dataSource)
                 .build());
         playLoadedVideo(null, null, -1);
-        assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+        assertTrue(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
 
         // Test pause and restart.
         mPlayer.pause();
         Thread.sleep(SLEEP_TIME);
-        assertFalse(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+        assertFalse(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
 
         MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
             @Override
@@ -2114,7 +2175,7 @@
         mOnPlayCalled.reset();
         mPlayer.play();
         mOnPlayCalled.waitForSignal();
-        assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+        assertTrue(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
 
         // Test reset.
         mPlayer.reset();
@@ -2131,12 +2192,12 @@
         mOnPlayCalled.reset();
         mPlayer.play();
         mOnPlayCalled.waitForSignal();
-        assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+        assertTrue(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
 
         // Test seek. Note: the seek position is cached and returned as the
         // current position so there's no point in comparing them.
         mPlayer.seekTo(duration - SLEEP_TIME, MediaPlayer2.SEEK_PREVIOUS_SYNC);
-        while (mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+        while (mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING) {
             Thread.sleep(SLEEP_TIME);
         }
     }
@@ -2259,4 +2320,78 @@
         mPlayer.play();
         assertTrue(mOnErrorCalled.waitForSignal());
     }
+
+    @Test
+    @SmallTest
+    public void testClearPendingCommands() throws Exception {
+        final Monitor readAllowed = new Monitor();
+        Media2DataSource dataSource = new Media2DataSource() {
+            @Override
+            public int readAt(long position, byte[] buffer, int offset, int size)
+                    throws IOException {
+                try {
+                    readAllowed.waitForSignal();
+                } catch (InterruptedException e) {
+                    fail();
+                }
+                return -1;
+            }
+
+            @Override
+            public long getSize() throws IOException {
+                return -1;  // Unknown size
+            }
+
+            @Override
+            public void close() throws IOException {}
+        };
+        final ArrayDeque<Integer> commandsCompleted = new ArrayDeque<>();
+        setOnErrorListener();
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                    mOnPrepareCalled.signal();
+                }
+            }
+
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                commandsCompleted.add(what);
+            }
+
+            @Override
+            public void onError(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                mOnErrorCalled.signal();
+            }
+        };
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(ecb);
+        }
+
+        mOnPrepareCalled.reset();
+        mOnErrorCalled.reset();
+
+        mPlayer.setDataSource(new DataSourceDesc.Builder()
+                .setDataSource(dataSource)
+                .build());
+
+        // prepare() will be pending until readAllowed is signaled.
+        mPlayer.prepare();
+
+        mPlayer.play();
+        mPlayer.pause();
+        mPlayer.play();
+        mPlayer.pause();
+        mPlayer.play();
+        mPlayer.seekTo(1000);
+
+        // Cause a failure on the pending prepare operation.
+        readAllowed.signal();
+        mOnErrorCalled.waitForSignal();
+        assertEquals(0, mOnPrepareCalled.getNumSignal());
+        assertEquals(1, commandsCompleted.size());
+        assertEquals(MediaPlayer2.CALL_COMPLETED_SET_DATA_SOURCE,
+                (int) commandsCompleted.peekFirst());
+    }
 }
diff --git a/media/src/androidTest/java/androidx/media/MediaPlayer2TestBase.java b/media/src/androidTest/java/androidx/media/MediaPlayer2TestBase.java
index 215993a..77c5c04 100644
--- a/media/src/androidTest/java/androidx/media/MediaPlayer2TestBase.java
+++ b/media/src/androidTest/java/androidx/media/MediaPlayer2TestBase.java
@@ -15,20 +15,29 @@
  */
 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;
 
@@ -41,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;
@@ -65,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;
@@ -75,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);
@@ -144,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);
@@ -158,7 +170,7 @@
                 return null;
             }
 
-            MediaPlayer2 mp = MediaPlayer2.create();
+            MediaPlayer2 mp = createMediaPlayer2OnUiThread();
 
             final AudioAttributesCompat aa = audioAttributes != null ? audioAttributes :
                     new AudioAttributesCompat.Builder().build();
@@ -204,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;
 
@@ -258,8 +284,23 @@
 
     @Before
     @CallSuper
-    public void setUp() throws Exception {
+    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() {
@@ -344,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);
                     }
                 }
             }
@@ -361,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);
+                    }
+                }
+            }
         });
     }
 
@@ -490,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() {
@@ -559,13 +605,49 @@
         if (playTime == -1) {
             return;
         } else if (playTime == 0) {
-            while (mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+            while (mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING) {
                 Thread.sleep(SLEEP_TIME);
             }
         } else {
             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/androidTest/java/androidx/media/MediaSession2Test.java b/media/src/androidTest/java/androidx/media/MediaSession2Test.java
index 5e7ed0e..adcba6b 100644
--- a/media/src/androidTest/java/androidx/media/MediaSession2Test.java
+++ b/media/src/androidTest/java/androidx/media/MediaSession2Test.java
@@ -65,7 +65,7 @@
 /**
  * Tests {@link MediaSession2}.
  */
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class MediaSession2Test extends MediaSession2TestBase {
@@ -125,7 +125,7 @@
     @Test
     public void testPlayerStateChange() throws Exception {
         prepareLooper();
-        final int targetState = MediaPlayerBase.PLAYER_STATE_PLAYING;
+        final int targetState = MediaPlayerInterface.PLAYER_STATE_PLAYING;
         final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
         sHandler.postAndSync(new Runnable() {
             @Override
@@ -136,7 +136,7 @@
                         .setSessionCallback(sHandlerExecutor, new SessionCallback() {
                             @Override
                             public void onPlayerStateChanged(MediaSession2 session,
-                                    MediaPlayerBase player, int state) {
+                                    MediaPlayerInterface player, int state) {
                                 assertEquals(targetState, state);
                                 latchForSessionCallback.countDown();
                             }
@@ -166,7 +166,7 @@
         final List<MediaItem2> playlist = TestUtils.createPlaylist(5);
 
         final MediaItem2 targetItem = playlist.get(3);
-        final int targetBufferingState = MediaPlayerBase.BUFFERING_STATE_BUFFERING_COMPLETE;
+        final int targetBufferingState = MediaPlayerInterface.BUFFERING_STATE_BUFFERING_COMPLETE;
         final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
         sHandler.postAndSync(new Runnable() {
             @Override
@@ -179,7 +179,7 @@
                         .setSessionCallback(sHandlerExecutor, new SessionCallback() {
                             @Override
                             public void onBufferingStateChanged(MediaSession2 session,
-                                    MediaPlayerBase player, MediaItem2 item, int state) {
+                                    MediaPlayerInterface player, MediaItem2 item, int state) {
                                 assertEquals(targetItem, item);
                                 assertEquals(targetBufferingState, state);
                                 latchForSessionCallback.countDown();
@@ -200,13 +200,39 @@
                     }
                 });
 
-        mPlayer.notifyBufferingState(targetItem, targetBufferingState);
+        mPlayer.notifyBufferingStateChanged(targetItem.getDataSourceDesc(), targetBufferingState);
         assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
         assertTrue(latchForControllerCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
         assertEquals(targetBufferingState, controller.getBufferingState());
     }
 
     @Test
+    public void testSeekCompleted() throws Exception {
+        prepareLooper();
+        final long testPosition = 1001;
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onSeekCompleted(
+                    MediaSession2 session, MediaPlayerInterface mpb, long position) {
+                assertEquals(mPlayer, mpb);
+                assertEquals(testPosition, position);
+                latch.countDown();
+            }
+        };
+
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setPlaylistAgent(mMockAgent)
+                .setId("testSeekCompleted")
+                .setSessionCallback(sHandlerExecutor, callback).build()) {
+            mPlayer.mCurrentPosition = testPosition;
+            mPlayer.notifySeekCompleted(testPosition);
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
     public void testCurrentDataSourceChanged() throws Exception {
         prepareLooper();
         final int listSize = 5;
@@ -214,9 +240,7 @@
         mMockAgent.setPlaylist(list, null);
 
         final MediaItem2 currentItem = list.get(3);
-        mMockAgent.mCurrentMediaItem = currentItem;
-
-        final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+        final CountDownLatch latchForSessionCallback = new CountDownLatch(2);
         try (MediaSession2 session = new MediaSession2.Builder(mContext)
                 .setPlayer(mPlayer)
                 .setPlaylistAgent(mMockAgent)
@@ -224,27 +248,44 @@
                 .setSessionCallback(sHandlerExecutor, new SessionCallback() {
                     @Override
                     public void onCurrentMediaItemChanged(MediaSession2 session,
-                            MediaPlayerBase player, MediaItem2 itemOut) {
-                        assertSame(currentItem, itemOut);
+                            MediaPlayerInterface player, MediaItem2 item) {
+                        switch ((int) latchForSessionCallback.getCount()) {
+                            case 2:
+                                assertEquals(currentItem, item);
+                                break;
+                            case 1:
+                                assertNull(item);
+                        }
                         latchForSessionCallback.countDown();
                     }
                 }).build()) {
 
-            final CountDownLatch latchForControllerCallback = new CountDownLatch(1);
+            final CountDownLatch latchForControllerCallback = new CountDownLatch(2);
             final MediaController2 controller =
                     createController(mSession.getToken(), true, new ControllerCallback() {
                         @Override
                         public void onCurrentMediaItemChanged(MediaController2 controller,
                                 MediaItem2 item) {
-                            assertEquals(currentItem, item);
+                            switch ((int) latchForControllerCallback.getCount()) {
+                                case 2:
+                                    assertEquals(currentItem, item);
+                                    break;
+                                case 1:
+                                    assertNull(item);
+                            }
                             latchForControllerCallback.countDown();
                         }
                     });
 
+            // Player notifies with the unknown dsd. Should be ignored.
+            mPlayer.notifyCurrentDataSourceChanged(TestUtils.createMediaItemWithMetadata()
+                    .getDataSourceDesc());
+            // Known DSD should be notified through the onCurrentMediaItemChanged.
             mPlayer.notifyCurrentDataSourceChanged(currentItem.getDataSourceDesc());
+            // Null DSD becomes null MediaItem2.
+            mPlayer.notifyCurrentDataSourceChanged(null);
             assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
             assertTrue(latchForControllerCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-            assertEquals(currentItem, controller.getCurrentMediaItem());
         }
     }
 
@@ -264,7 +305,7 @@
                 .setId("testMediaPrepared")
                 .setSessionCallback(sHandlerExecutor, new SessionCallback() {
                     @Override
-                    public void onMediaPrepared(MediaSession2 session, MediaPlayerBase player,
+                    public void onMediaPrepared(MediaSession2 session, MediaPlayerInterface player,
                             MediaItem2 itemOut) {
                         assertSame(currentItem, itemOut);
                         latchForSessionCallback.countDown();
@@ -285,7 +326,7 @@
         mMockAgent.setPlaylist(list, null);
 
         final MediaItem2 currentItem = list.get(3);
-        final int buffState = MediaPlayerBase.BUFFERING_STATE_BUFFERING_COMPLETE;
+        final int buffState = MediaPlayerInterface.BUFFERING_STATE_BUFFERING_COMPLETE;
 
         final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
         try (MediaSession2 session = new MediaSession2.Builder(mContext)
@@ -295,7 +336,7 @@
                 .setSessionCallback(sHandlerExecutor, new SessionCallback() {
                     @Override
                     public void onBufferingStateChanged(MediaSession2 session,
-                            MediaPlayerBase player, MediaItem2 itemOut, int stateOut) {
+                            MediaPlayerInterface player, MediaItem2 itemOut, int stateOut) {
                         assertSame(currentItem, itemOut);
                         assertEquals(buffState, stateOut);
                         latchForSessionCallback.countDown();
@@ -325,7 +366,7 @@
                 .setSessionCallback(sHandlerExecutor, new SessionCallback() {
                     @Override
                     public void onPlaybackSpeedChanged(MediaSession2 session,
-                            MediaPlayerBase player, float speedOut) {
+                            MediaPlayerInterface player, float speedOut) {
                         assertEquals(speed, speedOut, 0.0f);
                         latchForSessionCallback.countDown();
                     }
@@ -352,7 +393,7 @@
     @Test
     public void testUpdatePlayer() throws Exception {
         prepareLooper();
-        final int targetState = MediaPlayerBase.PLAYER_STATE_PLAYING;
+        final int targetState = MediaPlayerInterface.PLAYER_STATE_PLAYING;
         final CountDownLatch latch = new CountDownLatch(1);
         sHandler.postAndSync(new Runnable() {
             @Override
@@ -362,7 +403,7 @@
                         .setSessionCallback(sHandlerExecutor, new SessionCallback() {
                             @Override
                             public void onPlayerStateChanged(MediaSession2 session,
-                                    MediaPlayerBase player, int state) {
+                                    MediaPlayerInterface player, int state) {
                                 assertEquals(targetState, state);
                                 latch.countDown();
                             }
@@ -383,12 +424,12 @@
         assertNotNull(mSession.getPlaylistAgent());
         assertNotEquals(agent, mSession.getPlaylistAgent());
 
-        player.notifyPlaybackState(MediaPlayerBase.PLAYER_STATE_PLAYING);
+        player.notifyPlaybackState(MediaPlayerInterface.PLAYER_STATE_PLAYING);
         assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
     }
 
     @Test
-    public void testSetPlayer_playbackInfo() throws Exception {
+    public void testUpdatePlayer_playbackInfo() throws Exception {
         prepareLooper();
         MockPlayer player = new MockPlayer(0);
         final AudioAttributesCompat attrs = new AudioAttributesCompat.Builder()
@@ -482,6 +523,47 @@
     }
 
     @Test
+    public void testGetDuration() throws Exception {
+        prepareLooper();
+        final long testDuration = 9999;
+        mPlayer.mDuration = testDuration;
+        assertEquals(testDuration, mSession.getDuration());
+    }
+
+    @Test
+    public void testSessionCallback_onMediaPrepared() throws Exception {
+        prepareLooper();
+        final long testDuration = 9999;
+        final List<MediaItem2> list = TestUtils.createPlaylist(2);
+        final MediaItem2 testItem = list.get(1);
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        mPlayer.mDuration = testDuration;
+        mMockAgent.setPlaylist(list, null);
+        mMockAgent.mCurrentMediaItem = testItem;
+
+        final SessionCallback sessionCallback = new SessionCallback() {
+            @Override
+            public void onMediaPrepared(MediaSession2 session, MediaPlayerInterface player,
+                    MediaItem2 item) {
+                assertEquals(testItem, item);
+                assertEquals(testDuration,
+                        item.getMetadata().getLong(MediaMetadata2.METADATA_KEY_DURATION));
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setPlaylistAgent(mMockAgent)
+                .setId("testSessionCallback")
+                .setSessionCallback(sHandlerExecutor, sessionCallback)
+                .build()) {
+            mPlayer.notifyMediaPrepared(testItem.getDataSourceDesc());
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
     public void testSetPlaybackSpeed() throws Exception {
         prepareLooper();
         final float speed = 1.5f;
@@ -532,7 +614,7 @@
     @Test
     public void testGetPlayerState() {
         prepareLooper();
-        final int state = MediaPlayerBase.PLAYER_STATE_PLAYING;
+        final int state = MediaPlayerInterface.PLAYER_STATE_PLAYING;
         mPlayer.mLastPlayerState = state;
         assertEquals(state, mSession.getPlayerState());
     }
@@ -540,7 +622,7 @@
     @Test
     public void testGetBufferingState() {
         prepareLooper();
-        final int bufferingState = MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE;
+        final int bufferingState = MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_PLAYABLE;
         mPlayer.mLastBufferingState = bufferingState;
         assertEquals(bufferingState, mSession.getBufferingState());
     }
@@ -744,7 +826,7 @@
 
         mSession.updatePlayer(player, null, null);
         mSession.updatePlayer(mPlayer, null, null);
-        player.notifyPlaybackState(MediaPlayerBase.PLAYER_STATE_PAUSED);
+        player.notifyPlaybackState(MediaPlayerInterface.PLAYER_STATE_PAUSED);
         assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
     }
 
@@ -757,7 +839,7 @@
 
         @Override
         public void unregisterPlayerEventCallback(
-                @NonNull MediaPlayerBase.PlayerEventCallback listener) {
+                @NonNull MediaPlayerInterface.PlayerEventCallback listener) {
             // No-op.
         }
     }
diff --git a/media/src/androidTest/java/androidx/media/MediaSession2TestBase.java b/media/src/androidTest/java/androidx/media/MediaSession2TestBase.java
index 745ef3a..9c9fa8e 100644
--- a/media/src/androidTest/java/androidx/media/MediaSession2TestBase.java
+++ b/media/src/androidTest/java/androidx/media/MediaSession2TestBase.java
@@ -20,6 +20,7 @@
 import static junit.framework.Assert.assertTrue;
 
 import android.content.Context;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.HandlerThread;
 import android.os.Looper;
@@ -121,7 +122,11 @@
             if (sHandler == null) {
                 return;
             }
-            sHandler.getLooper().quitSafely();
+            if (Build.VERSION.SDK_INT >= 18) {
+                sHandler.getLooper().quitSafely();
+            } else {
+                sHandler.getLooper().quit();
+            }
             sHandler = null;
             sHandlerExecutor = null;
         }
diff --git a/media/src/androidTest/java/androidx/media/MediaSession2_PermissionTest.java b/media/src/androidTest/java/androidx/media/MediaSession2_PermissionTest.java
index 3895ea5..0bf93d4 100644
--- a/media/src/androidTest/java/androidx/media/MediaSession2_PermissionTest.java
+++ b/media/src/androidTest/java/androidx/media/MediaSession2_PermissionTest.java
@@ -48,9 +48,11 @@
 import static org.junit.Assert.fail;
 
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Process;
 import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
 import android.support.test.runner.AndroidJUnit4;
 
 import androidx.annotation.NonNull;
@@ -68,6 +70,7 @@
 /**
  * Tests whether {@link MediaSession2} receives commands that hasn't allowed.
  */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
 @RunWith(AndroidJUnit4.class)
 @MediumTest
 public class MediaSession2_PermissionTest extends MediaSession2TestBase {
diff --git a/media/src/androidTest/java/androidx/media/MediaSessionManager_MediaSession2Test.java b/media/src/androidTest/java/androidx/media/MediaSessionManager_MediaSession2Test.java
index 904b768..a0ce092 100644
--- a/media/src/androidTest/java/androidx/media/MediaSessionManager_MediaSession2Test.java
+++ b/media/src/androidTest/java/androidx/media/MediaSessionManager_MediaSession2Test.java
@@ -73,7 +73,7 @@
     public void testGetMediaSession2Tokens_hasMediaController() throws InterruptedException {
         prepareLooper();
         final MockPlayer player = (MockPlayer) mSession.getPlayer();
-        player.notifyPlaybackState(MediaPlayerBase.PLAYER_STATE_IDLE);
+        player.notifyPlaybackState(MediaPlayerInterface.PLAYER_STATE_IDLE);
 
         MediaController2 controller = null;
 //        List<SessionToken2> tokens = mManager.getActiveSessionTokens();
@@ -89,7 +89,7 @@
 //        assertNotNull(controller);
 //
 //        // Test if the found controller is correct one.
-//        assertEquals(MediaPlayerBase.PLAYER_STATE_IDLE, controller.getPlayerState());
+//        assertEquals(MediaPlayerInterface.PLAYER_STATE_IDLE, controller.getPlayerState());
 //        controller.play();
 //
 //        assertTrue(player.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
diff --git a/media/src/androidTest/java/androidx/media/MockPlayer.java b/media/src/androidTest/java/androidx/media/MockPlayer.java
index 49b1a19..e280ed8 100644
--- a/media/src/androidTest/java/androidx/media/MockPlayer.java
+++ b/media/src/androidTest/java/androidx/media/MockPlayer.java
@@ -16,18 +16,17 @@
 
 package androidx.media;
 
-import android.util.ArrayMap;
-
 import androidx.annotation.NonNull;
+import androidx.collection.ArrayMap;
 
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 
 /**
- * A mock implementation of {@link MediaPlayerBase} for testing.
+ * A mock implementation of {@link MediaPlayerInterface} for testing.
  */
-public class MockPlayer extends MediaPlayerBase {
+public class MockPlayer extends MediaPlayerInterface {
     public final CountDownLatch mCountDownLatch;
 
     public boolean mPlayCalled;
@@ -42,6 +41,7 @@
     public float mPlaybackSpeed = 1.0f;
     public @PlayerState int mLastPlayerState;
     public @BuffState int mLastBufferingState;
+    public long mDuration;
 
     public ArrayMap<PlayerEventCallback, Executor> mCallbacks = new ArrayMap<>();
 
@@ -128,6 +128,11 @@
     }
 
     @Override
+    public long getDuration() {
+        return mDuration;
+    }
+
+    @Override
     public void registerPlayerEventCallback(@NonNull Executor executor,
             @NonNull PlayerEventCallback callback) {
         if (callback == null || executor == null) {
@@ -155,21 +160,6 @@
         }
     }
 
-    public void notifyBufferingState(final MediaItem2 item, final int bufferingState) {
-        mLastBufferingState = bufferingState;
-        for (int i = 0; i < mCallbacks.size(); i++) {
-            final PlayerEventCallback callback = mCallbacks.keyAt(i);
-            final Executor executor = mCallbacks.valueAt(i);
-            executor.execute(new Runnable() {
-                @Override
-                public void run() {
-                    callback.onBufferingStateChanged(
-                            MockPlayer.this, item.getDataSourceDesc(), bufferingState);
-                }
-            });
-        }
-    }
-
     public void notifyCurrentDataSourceChanged(final DataSourceDesc dsd) {
         for (int i = 0; i < mCallbacks.size(); i++) {
             final PlayerEventCallback callback = mCallbacks.keyAt(i);
@@ -223,6 +213,19 @@
         }
     }
 
+    public void notifySeekCompleted(final long position) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onSeekCompleted(MockPlayer.this, position);
+                }
+            });
+        }
+    }
+
     public void notifyError(int what) {
         for (int i = 0; i < mCallbacks.size(); i++) {
             final PlayerEventCallback callback = mCallbacks.keyAt(i);
diff --git a/media/src/androidTest/java/androidx/media/SessionToken2Test.java b/media/src/androidTest/java/androidx/media/SessionToken2Test.java
index 22881a8..bf96eb9 100644
--- a/media/src/androidTest/java/androidx/media/SessionToken2Test.java
+++ b/media/src/androidTest/java/androidx/media/SessionToken2Test.java
@@ -20,8 +20,10 @@
 
 import android.content.ComponentName;
 import android.content.Context;
+import android.os.Build;
 import android.os.Process;
 import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SdkSuppress;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 
@@ -32,6 +34,7 @@
 /**
  * Tests {@link SessionToken2}.
  */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class SessionToken2Test {
diff --git a/media/src/androidTest/java/androidx/media/TestUtils.java b/media/src/androidTest/java/androidx/media/TestUtils.java
index 1e3ba9b..2b02b75 100644
--- a/media/src/androidTest/java/androidx/media/TestUtils.java
+++ b/media/src/androidTest/java/androidx/media/TestUtils.java
@@ -25,10 +25,11 @@
 import android.os.Handler;
 import android.os.Looper;
 
+import androidx.core.util.ObjectsCompat;
+
 import java.io.FileDescriptor;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -68,18 +69,29 @@
      *     incorrect if any bundle contains a bundle.
      */
     public static boolean equals(Bundle a, Bundle b) {
+        return contains(a, b) && contains(b, a);
+    }
+
+    /**
+     * Checks whether a Bundle contains another bundle.
+     *
+     * @param a a bundle
+     * @param b another bundle
+     * @return {@code true} if a contains b. {@code false} otherwise. This may be incorrect if any
+     *      bundle contains a bundle.
+     */
+    public static boolean contains(Bundle a, Bundle b) {
         if (a == b) {
             return true;
         }
         if (a == null || b == null) {
+            return b == null;
+        }
+        if (!a.keySet().containsAll(b.keySet())) {
             return false;
         }
-        if (!a.keySet().containsAll(b.keySet())
-                || !b.keySet().containsAll(a.keySet())) {
-            return false;
-        }
-        for (String key : a.keySet()) {
-            if (!Objects.equals(a.get(key), b.get(key))) {
+        for (String key : b.keySet()) {
+            if (!ObjectsCompat.equals(a.get(key), b.get(key))) {
                 return false;
             }
         }
@@ -91,7 +103,7 @@
      * <p>
      * Caller's method name will be used for prefix of each media item's media id.
      *
-     * @param size lits size
+     * @param size list size
      * @return the newly created playlist
      */
     public static List<MediaItem2> createPlaylist(int size) {
@@ -100,11 +112,7 @@
         for (int i = 0; i < size; i++) {
             list.add(new MediaItem2.Builder(MediaItem2.FLAG_PLAYABLE)
                     .setMediaId(caller + "_item_" + (size + 1))
-                    .setDataSourceDesc(
-                            new DataSourceDesc.Builder()
-                                    .setDataSource(new FileDescriptor())
-                                    .build())
-                    .build());
+                    .setDataSourceDesc(createDSD()).build());
         }
         return list;
     }
@@ -117,7 +125,7 @@
      */
     public static MediaItem2 createMediaItemWithMetadata() {
         return new MediaItem2.Builder(MediaItem2.FLAG_PLAYABLE)
-                .setMetadata(createMetadata()).build();
+                .setMetadata(createMetadata()).setDataSourceDesc(createDSD()).build();
     }
 
     /**
@@ -133,6 +141,21 @@
                 .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId).build();
     }
 
+    private static DataSourceDesc createDSD() {
+        return new DataSourceDesc.Builder().setDataSource(new FileDescriptor()).build();
+    }
+
+    /**
+     * Create a bundle for testing purpose.
+     *
+     * @return the newly created bundle.
+     */
+    public static Bundle createTestBundle() {
+        Bundle bundle = new Bundle();
+        bundle.putString("test_key", "test_value");
+        return bundle;
+    }
+
     /**
      * Handler that always waits until the Runnable finishes.
      */
diff --git a/media/src/main/java/android/support/v4/media/MediaBrowserCompat.java b/media/src/main/java/android/support/v4/media/MediaBrowserCompat.java
index a0e839b..aa039c3 100644
--- a/media/src/main/java/android/support/v4/media/MediaBrowserCompat.java
+++ b/media/src/main/java/android/support/v4/media/MediaBrowserCompat.java
@@ -15,6 +15,7 @@
  */
 package android.support.v4.media;
 
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION;
 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_CONNECT;
@@ -32,6 +33,7 @@
 import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID;
 import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST;
 import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN;
+import static androidx.media.MediaBrowserProtocol.DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS;
 import static androidx.media.MediaBrowserProtocol.DATA_OPTIONS;
 import static androidx.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
 import static androidx.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
@@ -428,6 +430,20 @@
     }
 
     /**
+     * Gets the options which is passed to {@link MediaBrowserServiceCompat#notifyChildrenChanged(
+     * String, Bundle)} call that triggered {@link SubscriptionCallback#onChildrenLoaded}.
+     * This should be called inside of {@link SubscriptionCallback#onChildrenLoaded}.
+     *
+     * @return A bundle which is passed to {@link MediaBrowserServiceCompat#notifyChildrenChanged(
+     *         String, Bundle)}
+     * @hide
+     */
+    @RestrictTo(LIBRARY)
+    public @Nullable Bundle getNotifyChildrenChangedOptions() {
+        return mImpl.getNotifyChildrenChangedOptions();
+    }
+
+    /**
      * A class with information on a single media item for use in browsing/searching media.
      * MediaItems are application dependent so we cannot guarantee that they contain the
      * right values.
@@ -951,13 +967,15 @@
         void search(@NonNull String query, Bundle extras, @NonNull SearchCallback callback);
         void sendCustomAction(@NonNull String action, Bundle extras,
                 @Nullable CustomActionCallback callback);
+        @Nullable Bundle getNotifyChildrenChangedOptions();
     }
 
     interface MediaBrowserServiceCallbackImpl {
         void onServiceConnected(Messenger callback, String root, MediaSessionCompat.Token session,
                 Bundle extra);
         void onConnectionFailed(Messenger callback);
-        void onLoadChildren(Messenger callback, String parentId, List list, Bundle options);
+        void onLoadChildren(Messenger callback, String parentId, List list, Bundle options,
+                Bundle notifyChildrenChangedOptions);
     }
 
     static class MediaBrowserImplBase
@@ -982,6 +1000,7 @@
         private String mRootId;
         private MediaSessionCompat.Token mMediaSessionToken;
         private Bundle mExtras;
+        private Bundle mNotifyChildrenChangedOptions;
 
         public MediaBrowserImplBase(Context context, ComponentName serviceComponent,
                 ConnectionCallback callback, Bundle rootHints) {
@@ -1372,7 +1391,7 @@
 
         @Override
         public void onLoadChildren(final Messenger callback, final String parentId,
-                final List list, final Bundle options) {
+                final List list, final Bundle options, final Bundle notifyChildrenChangedOptions) {
             // Check that there hasn't been a disconnect or a different ServiceConnection.
             if (!isCurrent(callback, "onLoadChildren")) {
                 return;
@@ -1398,18 +1417,27 @@
                     if (list == null) {
                         subscriptionCallback.onError(parentId);
                     } else {
+                        mNotifyChildrenChangedOptions = notifyChildrenChangedOptions;
                         subscriptionCallback.onChildrenLoaded(parentId, list);
+                        mNotifyChildrenChangedOptions = null;
                     }
                 } else {
                     if (list == null) {
                         subscriptionCallback.onError(parentId, options);
                     } else {
+                        mNotifyChildrenChangedOptions = notifyChildrenChangedOptions;
                         subscriptionCallback.onChildrenLoaded(parentId, list, options);
+                        mNotifyChildrenChangedOptions = null;
                     }
                 }
             }
         }
 
+        @Override
+        public Bundle getNotifyChildrenChangedOptions() {
+            return mNotifyChildrenChangedOptions;
+        }
+
         /**
          * For debugging.
          */
@@ -1589,6 +1617,7 @@
         protected ServiceBinderWrapper mServiceBinderWrapper;
         protected Messenger mCallbacksMessenger;
         private MediaSessionCompat.Token mMediaSessionToken;
+        private Bundle mNotifyChildrenChangedOptions;
 
         MediaBrowserImplApi21(Context context, ComponentName serviceComponent,
                 ConnectionCallback callback, Bundle rootHints) {
@@ -1862,7 +1891,7 @@
                 mCallbacksMessenger = new Messenger(mHandler);
                 mHandler.setCallbacksMessenger(mCallbacksMessenger);
                 try {
-                    mServiceBinderWrapper.registerCallbackMessenger(mCallbacksMessenger);
+                    mServiceBinderWrapper.registerCallbackMessenger(mContext, mCallbacksMessenger);
                 } catch (RemoteException e) {
                     Log.i(TAG, "Remote error registering client messenger." );
                 }
@@ -1901,7 +1930,8 @@
 
         @Override
         @SuppressWarnings("ReferenceEquality")
-        public void onLoadChildren(Messenger callback, String parentId, List list, Bundle options) {
+        public void onLoadChildren(Messenger callback, String parentId, List list, Bundle options,
+                Bundle notifyChildrenChangedOptions) {
             if (mCallbacksMessenger != callback) {
                 return;
             }
@@ -1922,17 +1952,26 @@
                     if (list == null) {
                         subscriptionCallback.onError(parentId);
                     } else {
+                        mNotifyChildrenChangedOptions = notifyChildrenChangedOptions;
                         subscriptionCallback.onChildrenLoaded(parentId, list);
+                        mNotifyChildrenChangedOptions = null;
                     }
                 } else {
                     if (list == null) {
                         subscriptionCallback.onError(parentId, options);
                     } else {
+                        mNotifyChildrenChangedOptions = notifyChildrenChangedOptions;
                         subscriptionCallback.onChildrenLoaded(parentId, list, options);
+                        mNotifyChildrenChangedOptions = null;
                     }
                 }
             }
         }
+
+        @Override
+        public Bundle getNotifyChildrenChangedOptions() {
+            return mNotifyChildrenChangedOptions;
+        }
     }
 
     @RequiresApi(23)
@@ -2077,7 +2116,8 @@
                         serviceCallback.onLoadChildren(callbacksMessenger,
                                 data.getString(DATA_MEDIA_ITEM_ID),
                                 data.getParcelableArrayList(DATA_MEDIA_ITEM_LIST),
-                                data.getBundle(DATA_OPTIONS));
+                                data.getBundle(DATA_OPTIONS),
+                                data.getBundle(DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS));
                         break;
                     default:
                         Log.w(TAG, "Unhandled message: " + msg
@@ -2147,8 +2187,10 @@
             sendRequest(CLIENT_MSG_GET_MEDIA_ITEM, data, callbacksMessenger);
         }
 
-        void registerCallbackMessenger(Messenger callbackMessenger) throws RemoteException {
+        void registerCallbackMessenger(Context context, Messenger callbackMessenger)
+                throws RemoteException {
             Bundle data = new Bundle();
+            data.putString(DATA_PACKAGE_NAME, context.getPackageName());
             data.putBundle(DATA_ROOT_HINTS, mRootHints);
             sendRequest(CLIENT_MSG_REGISTER_CALLBACK_MESSENGER, data, callbackMessenger);
         }
diff --git a/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java b/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
index 1d78fc8..dd498be 100644
--- a/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
+++ b/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
@@ -18,6 +18,7 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.media.MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER;
 
 import android.app.Activity;
 import android.app.PendingIntent;
@@ -31,8 +32,10 @@
 import android.media.MediaMetadataRetriever;
 import android.media.Rating;
 import android.media.RemoteControlClient;
+import android.media.session.MediaSession;
 import android.net.Uri;
 import android.os.BadParcelableException;
+import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
@@ -55,9 +58,12 @@
 import android.view.ViewConfiguration;
 
 import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.core.app.BundleCompat;
+import androidx.media.MediaSessionManager;
+import androidx.media.MediaSessionManager.RemoteUserInfo;
 import androidx.media.VolumeProviderCompat;
 import androidx.media.session.MediaButtonReceiver;
 
@@ -383,6 +389,11 @@
     // Maximum size of the bitmap in dp.
     private static final int MAX_BITMAP_SIZE_IN_DP = 320;
 
+    private static final String DATA_CALLING_PACKAGE = "data_calling_pkg";
+    private static final String DATA_CALLING_PID = "data_calling_pid";
+    private static final String DATA_CALLING_UID = "data_calling_uid";
+    private static final String DATA_EXTRAS = "data_extras";
+
     // Maximum size of the bitmap in px. It shouldn't be changed.
     static int sMaxBitmapSize;
 
@@ -449,7 +460,11 @@
             mbrIntent = PendingIntent.getBroadcast(context,
                     0/* requestCode, ignored */, mediaButtonIntent, 0/* flags */);
         }
-        if (android.os.Build.VERSION.SDK_INT >= 21) {
+        if (android.os.Build.VERSION.SDK_INT >= 28) {
+            mImpl = new MediaSessionImplApi28(context, tag);
+            // Set default callback to respond to controllers' extra binder requests.
+            setCallback(new Callback() {});
+        } else if (android.os.Build.VERSION.SDK_INT >= 21) {
             mImpl = new MediaSessionImplApi21(context, tag);
             // Set default callback to respond to controllers' extra binder requests.
             setCallback(new Callback() {});
@@ -802,6 +817,18 @@
     }
 
     /**
+     * Gets the controller information who sent the current request.
+     * <p>
+     * Note: This is only valid while in a request callback, such as {@link Callback#onPlay}.
+     *
+     * @throws IllegalStateException If this method is called outside of {@link Callback} methods.
+     * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo)
+     */
+    public final @NonNull RemoteUserInfo getCurrentControllerInfo() {
+        return mImpl.getCurrentControllerInfo();
+    }
+
+    /**
      * Returns the name of the package that sent the last media button, transport control, or
      * command from controllers and the system. This is only valid while in a request callback, such
      * as {@link Callback#onPlay}. This method is not available and returns null on pre-N devices.
@@ -1822,6 +1849,7 @@
         Object getRemoteControlClient();
 
         String getCallingPackage();
+        RemoteUserInfo getCurrentControllerInfo();
     }
 
     static class MediaSessionImplBase implements MediaSessionImpl {
@@ -1918,30 +1946,19 @@
             }
         }
 
-        void postToHandler(int what) {
-            postToHandler(what, null);
-        }
-
-        void postToHandler(int what, int arg1) {
-            postToHandler(what, null, arg1);
-        }
-
-        void postToHandler(int what, Object obj) {
-            postToHandler(what, obj, null);
-        }
-
-        void postToHandler(int what, Object obj, int arg1) {
+        void postToHandler(int what, int arg1, int arg2, Object obj, Bundle extras) {
             synchronized (mLock) {
                 if (mHandler != null) {
-                    mHandler.post(what, obj, arg1);
-                }
-            }
-        }
-
-        void postToHandler(int what, Object obj, Bundle extras) {
-            synchronized (mLock) {
-                if (mHandler != null) {
-                    mHandler.post(what, obj, extras);
+                    Message msg = mHandler.obtainMessage(what, arg1, arg2, obj);
+                    Bundle data = new Bundle();
+                    data.putString(DATA_CALLING_PACKAGE, LEGACY_CONTROLLER);
+                    data.putInt(DATA_CALLING_PID, Binder.getCallingPid());
+                    data.putInt(DATA_CALLING_UID, Binder.getCallingUid());
+                    if (extras != null) {
+                        data.putBundle(DATA_EXTRAS, extras);
+                    }
+                    msg.setData(data);
+                    msg.sendToTarget();
                 }
             }
         }
@@ -1959,6 +1976,7 @@
             if (mVolumeProvider != null) {
                 mVolumeProvider.setCallback(null);
             }
+            mLocalStream = stream;
             mVolumeType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL;
             ParcelableVolumeInfo info = new ParcelableVolumeInfo(mVolumeType, mLocalStream,
                     VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE,
@@ -2282,6 +2300,16 @@
             sendExtras(extras);
         }
 
+        @Override
+        public RemoteUserInfo getCurrentControllerInfo() {
+            synchronized (mLock) {
+                if (mHandler != null) {
+                    return mHandler.getRemoteUserInfo();
+                }
+            }
+            return null;
+        }
+
         // Registers/unregisters components as needed.
         boolean update() {
             boolean registeredRcc = false;
@@ -2792,6 +2820,26 @@
             public boolean isTransportControlEnabled() {
                 return (mFlags & FLAG_HANDLES_TRANSPORT_CONTROLS) != 0;
             }
+
+            void postToHandler(int what) {
+                MediaSessionImplBase.this.postToHandler(what, 0, 0, null, null);
+            }
+
+            void postToHandler(int what, int arg1) {
+                MediaSessionImplBase.this.postToHandler(what, arg1, 0, null, null);
+            }
+
+            void postToHandler(int what, Object obj) {
+                MediaSessionImplBase.this.postToHandler(what, 0, 0, obj, null);
+            }
+
+            void postToHandler(int what, Object obj, int arg1) {
+                MediaSessionImplBase.this.postToHandler(what, arg1, 0, obj, null);
+            }
+
+            void postToHandler(int what, Object obj, Bundle extras) {
+                MediaSessionImplBase.this.postToHandler(what, 0, 0, obj, extras);
+            }
         }
 
         private static final class Command {
@@ -2843,138 +2891,133 @@
             private static final int KEYCODE_MEDIA_PAUSE = 127;
             private static final int KEYCODE_MEDIA_PLAY = 126;
 
+            private RemoteUserInfo mRemoteUserInfo;
+
             public MessageHandler(Looper looper) {
                 super(looper);
             }
 
-            public void post(int what, Object obj, Bundle bundle) {
-                Message msg = obtainMessage(what, obj);
-                msg.setData(bundle);
-                msg.sendToTarget();
-            }
-
-            public void post(int what, Object obj) {
-                obtainMessage(what, obj).sendToTarget();
-            }
-
-            public void post(int what) {
-                post(what, null);
-            }
-
-            public void post(int what, Object obj, int arg1) {
-                obtainMessage(what, arg1, 0, obj).sendToTarget();
-            }
-
             @Override
             public void handleMessage(Message msg) {
                 MediaSessionCompat.Callback cb = mCallback;
                 if (cb == null) {
                     return;
                 }
-                switch (msg.what) {
-                    case MSG_COMMAND:
-                        Command cmd = (Command) msg.obj;
-                        cb.onCommand(cmd.command, cmd.extras, cmd.stub);
-                        break;
-                    case MSG_MEDIA_BUTTON:
-                        KeyEvent keyEvent = (KeyEvent) msg.obj;
-                        Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
-                        intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
-                        // Let the Callback handle events first before using the default behavior
-                        if (!cb.onMediaButtonEvent(intent)) {
-                            onMediaButtonEvent(keyEvent, cb);
-                        }
-                        break;
-                    case MSG_PREPARE:
-                        cb.onPrepare();
-                        break;
-                    case MSG_PREPARE_MEDIA_ID:
-                        cb.onPrepareFromMediaId((String) msg.obj, msg.getData());
-                        break;
-                    case MSG_PREPARE_SEARCH:
-                        cb.onPrepareFromSearch((String) msg.obj, msg.getData());
-                        break;
-                    case MSG_PREPARE_URI:
-                        cb.onPrepareFromUri((Uri) msg.obj, msg.getData());
-                        break;
-                    case MSG_PLAY:
-                        cb.onPlay();
-                        break;
-                    case MSG_PLAY_MEDIA_ID:
-                        cb.onPlayFromMediaId((String) msg.obj, msg.getData());
-                        break;
-                    case MSG_PLAY_SEARCH:
-                        cb.onPlayFromSearch((String) msg.obj, msg.getData());
-                        break;
-                    case MSG_PLAY_URI:
-                        cb.onPlayFromUri((Uri) msg.obj, msg.getData());
-                        break;
-                    case MSG_SKIP_TO_ITEM:
-                        cb.onSkipToQueueItem((Long) msg.obj);
-                        break;
-                    case MSG_PAUSE:
-                        cb.onPause();
-                        break;
-                    case MSG_STOP:
-                        cb.onStop();
-                        break;
-                    case MSG_NEXT:
-                        cb.onSkipToNext();
-                        break;
-                    case MSG_PREVIOUS:
-                        cb.onSkipToPrevious();
-                        break;
-                    case MSG_FAST_FORWARD:
-                        cb.onFastForward();
-                        break;
-                    case MSG_REWIND:
-                        cb.onRewind();
-                        break;
-                    case MSG_SEEK_TO:
-                        cb.onSeekTo((Long) msg.obj);
-                        break;
-                    case MSG_RATE:
-                        cb.onSetRating((RatingCompat) msg.obj);
-                        break;
-                    case MSG_RATE_EXTRA:
-                        cb.onSetRating((RatingCompat) msg.obj, msg.getData());
-                        break;
-                    case MSG_CUSTOM_ACTION:
-                        cb.onCustomAction((String) msg.obj, msg.getData());
-                        break;
-                    case MSG_ADD_QUEUE_ITEM:
-                        cb.onAddQueueItem((MediaDescriptionCompat) msg.obj);
-                        break;
-                    case MSG_ADD_QUEUE_ITEM_AT:
-                        cb.onAddQueueItem((MediaDescriptionCompat) msg.obj, msg.arg1);
-                        break;
-                    case MSG_REMOVE_QUEUE_ITEM:
-                        cb.onRemoveQueueItem((MediaDescriptionCompat) msg.obj);
-                        break;
-                    case MSG_REMOVE_QUEUE_ITEM_AT:
-                        if (mQueue != null) {
-                            QueueItem item = (msg.arg1 >= 0 && msg.arg1 < mQueue.size())
-                                    ? mQueue.get(msg.arg1) : null;
-                            if (item != null) {
-                                cb.onRemoveQueueItem(item.getDescription());
+
+                Bundle data = msg.getData();
+                mRemoteUserInfo = new RemoteUserInfo(data.getString(DATA_CALLING_PACKAGE),
+                        data.getInt(DATA_CALLING_PID), data.getInt(DATA_CALLING_UID));
+                data = data.getBundle(DATA_EXTRAS);
+
+                try {
+                    switch (msg.what) {
+                        case MSG_COMMAND:
+                            Command cmd = (Command) msg.obj;
+                            cb.onCommand(cmd.command, cmd.extras, cmd.stub);
+                            break;
+                        case MSG_MEDIA_BUTTON:
+                            KeyEvent keyEvent = (KeyEvent) msg.obj;
+                            Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+                            intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
+                            // Let the Callback handle events first before using the default
+                            // behavior
+                            if (!cb.onMediaButtonEvent(intent)) {
+                                onMediaButtonEvent(keyEvent, cb);
                             }
-                        }
-                        break;
-                    case MSG_ADJUST_VOLUME:
-                        adjustVolume(msg.arg1, 0);
-                        break;
-                    case MSG_SET_VOLUME:
-                        setVolumeTo(msg.arg1, 0);
-                        break;
-                    case MSG_SET_CAPTIONING_ENABLED:
-                        cb.onSetCaptioningEnabled((boolean) msg.obj);
-                        break;
-                    case MSG_SET_REPEAT_MODE:
-                        cb.onSetRepeatMode(msg.arg1);
-                        break;
-                    case MSG_SET_SHUFFLE_MODE:
-                        cb.onSetShuffleMode(msg.arg1);
-                        break;
+                            break;
+                        case MSG_PREPARE:
+                            cb.onPrepare();
+                            break;
+                        case MSG_PREPARE_MEDIA_ID:
+                            cb.onPrepareFromMediaId((String) msg.obj, data);
+                            break;
+                        case MSG_PREPARE_SEARCH:
+                            cb.onPrepareFromSearch((String) msg.obj, data);
+                            break;
+                        case MSG_PREPARE_URI:
+                            cb.onPrepareFromUri((Uri) msg.obj, data);
+                            break;
+                        case MSG_PLAY:
+                            cb.onPlay();
+                            break;
+                        case MSG_PLAY_MEDIA_ID:
+                            cb.onPlayFromMediaId((String) msg.obj, data);
+                            break;
+                        case MSG_PLAY_SEARCH:
+                            cb.onPlayFromSearch((String) msg.obj, data);
+                            break;
+                        case MSG_PLAY_URI:
+                            cb.onPlayFromUri((Uri) msg.obj, data);
+                            break;
+                        case MSG_SKIP_TO_ITEM:
+                            cb.onSkipToQueueItem((Long) msg.obj);
+                            break;
+                        case MSG_PAUSE:
+                            cb.onPause();
+                            break;
+                        case MSG_STOP:
+                            cb.onStop();
+                            break;
+                        case MSG_NEXT:
+                            cb.onSkipToNext();
+                            break;
+                        case MSG_PREVIOUS:
+                            cb.onSkipToPrevious();
+                            break;
+                        case MSG_FAST_FORWARD:
+                            cb.onFastForward();
+                            break;
+                        case MSG_REWIND:
+                            cb.onRewind();
+                            break;
+                        case MSG_SEEK_TO:
+                            cb.onSeekTo((Long) msg.obj);
+                            break;
+                        case MSG_RATE:
+                            cb.onSetRating((RatingCompat) msg.obj);
+                            break;
+                        case MSG_RATE_EXTRA:
+                            cb.onSetRating((RatingCompat) msg.obj, data);
+                            break;
+                        case MSG_CUSTOM_ACTION:
+                            cb.onCustomAction((String) msg.obj, data);
+                            break;
+                        case MSG_ADD_QUEUE_ITEM:
+                            cb.onAddQueueItem((MediaDescriptionCompat) msg.obj);
+                            break;
+                        case MSG_ADD_QUEUE_ITEM_AT:
+                            cb.onAddQueueItem((MediaDescriptionCompat) msg.obj, msg.arg1);
+                            break;
+                        case MSG_REMOVE_QUEUE_ITEM:
+                            cb.onRemoveQueueItem((MediaDescriptionCompat) msg.obj);
+                            break;
+                        case MSG_REMOVE_QUEUE_ITEM_AT:
+                            if (mQueue != null) {
+                                QueueItem item = (msg.arg1 >= 0 && msg.arg1 < mQueue.size())
+                                        ? mQueue.get(msg.arg1) : null;
+                                if (item != null) {
+                                    cb.onRemoveQueueItem(item.getDescription());
+                                }
+                            }
+                            break;
+                        case MSG_ADJUST_VOLUME:
+                            adjustVolume(msg.arg1, 0);
+                            break;
+                        case MSG_SET_VOLUME:
+                            setVolumeTo(msg.arg1, 0);
+                            break;
+                        case MSG_SET_CAPTIONING_ENABLED:
+                            cb.onSetCaptioningEnabled((boolean) msg.obj);
+                            break;
+                        case MSG_SET_REPEAT_MODE:
+                            cb.onSetRepeatMode(msg.arg1);
+                            break;
+                        case MSG_SET_SHUFFLE_MODE:
+                            cb.onSetShuffleMode(msg.arg1);
+                            break;
+                    }
+                } finally {
+                    mRemoteUserInfo = null;
                 }
             }
 
@@ -3028,6 +3071,10 @@
                         break;
                 }
             }
+
+            RemoteUserInfo getRemoteUserInfo() {
+                return mRemoteUserInfo;
+            }
         }
     }
 
@@ -3050,7 +3097,8 @@
                         new RemoteControlClient.OnPlaybackPositionUpdateListener() {
                             @Override
                             public void onPlaybackPositionUpdate(long newPositionMs) {
-                                postToHandler(MessageHandler.MSG_SEEK_TO, newPositionMs);
+                                postToHandler(
+                                        MessageHandler.MSG_SEEK_TO, -1, -1, newPositionMs, null);
                             }
                         };
                 mRcc.setPlaybackPositionUpdateListener(listener);
@@ -3135,8 +3183,8 @@
                             public void onMetadataUpdate(int key, Object newValue) {
                                 if (key == MediaMetadataEditor.RATING_KEY_BY_USER
                                         && newValue instanceof Rating) {
-                                    postToHandler(MessageHandler.MSG_RATE,
-                                            RatingCompat.fromRating(newValue));
+                                    postToHandler(MessageHandler.MSG_RATE, -1, -1,
+                                            RatingCompat.fromRating(newValue), null);
                                 }
                             }
                         };
@@ -3410,6 +3458,11 @@
             }
         }
 
+        @Override
+        public RemoteUserInfo getCurrentControllerInfo() {
+            return null;
+        }
+
         class ExtraSession extends IMediaSession.Stub {
             @Override
             public void sendCommand(String command, Bundle args, ResultReceiverWrapper cb) {
@@ -3703,4 +3756,25 @@
             }
         }
     }
+
+    @RequiresApi(28)
+    static class MediaSessionImplApi28 extends MediaSessionImplApi21 {
+        private MediaSession mSession;
+
+        MediaSessionImplApi28(Context context, String tag) {
+            super(context, tag);
+        }
+
+        MediaSessionImplApi28(Object mediaSession) {
+            super(mediaSession);
+            mSession = (MediaSession) mediaSession;
+        }
+
+        @Override
+        public final @NonNull RemoteUserInfo getCurrentControllerInfo() {
+            android.media.session.MediaSessionManager.RemoteUserInfo info =
+                    mSession.getCurrentControllerInfo();
+            return new RemoteUserInfo(info.getPackageName(), info.getPid(), info.getUid());
+        }
+    }
 }
diff --git a/media/src/main/java/androidx/media/AudioAttributesCompat.java b/media/src/main/java/androidx/media/AudioAttributesCompat.java
index 1fbb7be..69ef117 100644
--- a/media/src/main/java/androidx/media/AudioAttributesCompat.java
+++ b/media/src/main/java/androidx/media/AudioAttributesCompat.java
@@ -21,6 +21,7 @@
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.os.Build;
+import android.os.Bundle;
 import android.util.SparseIntArray;
 
 import androidx.annotation.IntDef;
@@ -228,6 +229,16 @@
     private static final int FLAG_ALL_PUBLIC =
             (FLAG_AUDIBILITY_ENFORCED | FLAG_HW_AV_SYNC | FLAG_LOW_LATENCY);
 
+    /** Keys to convert to (or create from) Bundle. */
+    private static final String AUDIO_ATTRIBUTES_FRAMEWORKS =
+            "androidx.media.audio_attrs.FRAMEWORKS";
+    private static final String AUDIO_ATTRIBUTES_USAGE = "androidx.media.audio_attrs.USAGE";
+    private static final String AUDIO_ATTRIBUTES_CONTENT_TYPE =
+            "androidx.media.audio_attrs.CONTENT_TYPE";
+    private static final String AUDIO_ATTRIBUTES_FLAGS = "androidx.media.audio_attrs.FLAGS";
+    private static final String AUDIO_ATTRIBUTES_LEGACY_STREAM_TYPE =
+            "androidx.media.audio_attrs.LEGACY_STREAM_TYPE";
+
     int mUsage = USAGE_UNKNOWN;
     int mContentType = CONTENT_TYPE_UNKNOWN;
     int mFlags = 0x0;
@@ -377,6 +388,56 @@
     }
 
     /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public @NonNull Bundle toBundle() {
+        Bundle bundle = new Bundle();
+        if (Build.VERSION.SDK_INT >= 21) {
+            bundle.putParcelable(AUDIO_ATTRIBUTES_FRAMEWORKS, mAudioAttributesWrapper.unwrap());
+        } else {
+            bundle.putInt(AUDIO_ATTRIBUTES_USAGE, mUsage);
+            bundle.putInt(AUDIO_ATTRIBUTES_CONTENT_TYPE, mContentType);
+            bundle.putInt(AUDIO_ATTRIBUTES_FLAGS, mFlags);
+            if (mLegacyStream != null) {
+                bundle.putInt(AUDIO_ATTRIBUTES_LEGACY_STREAM_TYPE, mLegacyStream);
+            }
+        }
+        return bundle;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static AudioAttributesCompat fromBundle(Bundle bundle) {
+        if (bundle == null) {
+            return null;
+        }
+
+        if (Build.VERSION.SDK_INT >= 21) {
+            AudioAttributes frameworkAttrs = (AudioAttributes)
+                    bundle.getParcelable(AUDIO_ATTRIBUTES_FRAMEWORKS);
+            return frameworkAttrs == null ? null : AudioAttributesCompat.wrap(frameworkAttrs);
+        } else {
+            int usage = bundle.getInt(AUDIO_ATTRIBUTES_USAGE, USAGE_UNKNOWN);
+            int contentType = bundle.getInt(AUDIO_ATTRIBUTES_CONTENT_TYPE, CONTENT_TYPE_UNKNOWN);
+            int flags = bundle.getInt(AUDIO_ATTRIBUTES_FLAGS, 0);
+
+            // Here, we do not use builder in order to 'copy' the exact state of the original one.
+            // Builder class guesses the usage based on other value (contentType/legacyStream), and
+            // overwrites it. So using builder cannot ensure the equality.
+            AudioAttributesCompat attr = new AudioAttributesCompat();
+            attr.mUsage = usage;
+            attr.mContentType = contentType;
+            attr.mFlags = flags;
+            attr.mLegacyStream = bundle.containsKey(AUDIO_ATTRIBUTES_LEGACY_STREAM_TYPE)
+                    ? bundle.getInt(AUDIO_ATTRIBUTES_LEGACY_STREAM_TYPE) : null;
+            return attr;
+        }
+    }
+
+    /**
      * Builder class for {@link AudioAttributesCompat} objects.
      *
      * <p>example:
diff --git a/media/src/main/java/androidx/media/Media2DataSource.java b/media/src/main/java/androidx/media/Media2DataSource.java
index 4990259..3855d56 100644
--- a/media/src/main/java/androidx/media/Media2DataSource.java
+++ b/media/src/main/java/androidx/media/Media2DataSource.java
@@ -17,15 +17,10 @@
 
 package androidx.media;
 
-import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import androidx.annotation.RestrictTo;
-
 import java.io.Closeable;
 import java.io.IOException;
 
 /**
- * @hide
  * For supplying media data to the framework. Implement this if your app has
  * special requirements for the way media data is obtained.
  *
@@ -36,7 +31,6 @@
  * Media2DataSource from another thread while it's being used by the framework.</p>
  *
  */
-@RestrictTo(LIBRARY_GROUP)
 public abstract class Media2DataSource implements Closeable {
     /**
      * Called to request data from the given position.
diff --git a/media/src/main/java/androidx/media/MediaBrowser2.java b/media/src/main/java/androidx/media/MediaBrowser2.java
index 6ef7fcf..beadf1f 100644
--- a/media/src/main/java/androidx/media/MediaBrowser2.java
+++ b/media/src/main/java/androidx/media/MediaBrowser2.java
@@ -17,13 +17,18 @@
 package androidx.media;
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
+import static androidx.media.MediaConstants2.ARGUMENT_PAGE;
+import static androidx.media.MediaConstants2.ARGUMENT_PAGE_SIZE;
 
 import android.content.Context;
+import android.os.BadParcelableException;
 import android.os.Bundle;
 import android.support.v4.media.MediaBrowserCompat;
 import android.support.v4.media.MediaBrowserCompat.ItemCallback;
 import android.support.v4.media.MediaBrowserCompat.MediaItem;
 import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback;
+import android.util.Log;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
@@ -38,11 +43,12 @@
 import java.util.concurrent.Executor;
 
 /**
- * @hide
  * Browses media content offered by a {@link MediaLibraryService2}.
  */
-@RestrictTo(LIBRARY_GROUP)
 public class MediaBrowser2 extends MediaController2 {
+    static final String TAG = "MediaBrowser2";
+    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
     /**
      * @hide
      */
@@ -50,11 +56,10 @@
     public static final String EXTRA_ITEM_COUNT = "android.media.browse.extra.ITEM_COUNT";
 
     /**
-     * Key for Bundle version of {@link MediaSession2.ControllerInfo}.
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    public static final String EXTRA_TARGET = "android.media.browse.extra.TARGET";
+    public static final String MEDIA_BROWSER2_SUBSCRIBE = "androidx.media.MEDIA_BROWSER2_SUBSCRIBE";
 
     private final Object mLock = new Object();
     @GuardedBy("mLock")
@@ -171,7 +176,7 @@
                 browser.disconnect();
             }
             mBrowserCompats.clear();
-            // TODO: Ensure that ControllerCallback#onDisconnected() is called by super.close().
+            // Ensure that ControllerCallback#onDisconnected() is called by super.close().
             super.close();
         }
     }
@@ -195,13 +200,20 @@
                 }
             });
         } else {
-            MediaBrowserCompat newBrowser = new MediaBrowserCompat(getContext(),
-                    getSessionToken().getComponentName(), new GetLibraryRootCallback(extras),
-                    extras);
-            newBrowser.connect();
-            synchronized (mLock) {
-                mBrowserCompats.put(extras, newBrowser);
-            }
+            getCallbackExecutor().execute(new Runnable() {
+                @Override
+                public void run() {
+                    // Do this on the callback executor to set the looper of MediaBrowserCompat's
+                    // callback handler to this looper.
+                    MediaBrowserCompat newBrowser = new MediaBrowserCompat(getContext(),
+                            getSessionToken().getComponentName(),
+                            new GetLibraryRootCallback(extras), extras);
+                    synchronized (mLock) {
+                        mBrowserCompats.put(extras, newBrowser);
+                    }
+                    newBrowser.connect();
+                }
+            });
         }
     }
 
@@ -218,15 +230,9 @@
         if (parentId == null) {
             throw new IllegalArgumentException("parentId shouldn't be null");
         }
-        // TODO: Document this behavior
-        Bundle option;
-        if (extras != null && (extras.containsKey(MediaBrowserCompat.EXTRA_PAGE)
-                || extras.containsKey(MediaBrowserCompat.EXTRA_PAGE_SIZE))) {
-            option = new Bundle(extras);
-            option.remove(MediaBrowserCompat.EXTRA_PAGE);
-            option.remove(MediaBrowserCompat.EXTRA_PAGE_SIZE);
-        } else {
-            option = extras;
+        MediaBrowserCompat browser = getBrowserCompat();
+        if (browser == null) {
+            return;
         }
         SubscribeCallback callback = new SubscribeCallback();
         synchronized (mLock) {
@@ -237,11 +243,11 @@
             }
             list.add(callback);
         }
-        // TODO: Revisit using default browser is OK. Here's my concern.
-        //       Assume that MediaBrowser2 is connected with the MediaBrowserServiceCompat.
-        //       Since MediaBrowserServiceCompat can call MediaBrowserServiceCompat#
-        //       getBrowserRootHints(), the service may refuse calls from MediaBrowser2
-        getBrowserCompat().subscribe(parentId, option, callback);
+
+        Bundle options = new Bundle();
+        options.putBundle(ARGUMENT_EXTRAS, extras);
+        options.putBoolean(MEDIA_BROWSER2_SUBSCRIBE, true);
+        browser.subscribe(parentId, options, callback);
     }
 
     /**
@@ -257,6 +263,10 @@
         if (parentId == null) {
             throw new IllegalArgumentException("parentId shouldn't be null");
         }
+        MediaBrowserCompat browser = getBrowserCompat();
+        if (browser == null) {
+            return;
+        }
         // Note: don't use MediaBrowserCompat#unsubscribe(String) here, to keep the subscription
         // callback for getChildren.
         synchronized (mLock) {
@@ -264,7 +274,6 @@
             if (list == null) {
                 return;
             }
-            MediaBrowserCompat browser = getBrowserCompat();
             for (int i = 0; i < list.size(); i++) {
                 browser.unsubscribe(parentId, list.get(i));
             }
@@ -288,12 +297,15 @@
         if (page < 1 || pageSize < 1) {
             throw new IllegalArgumentException("Neither page nor pageSize should be less than 1");
         }
-        Bundle options = new Bundle(extras);
+        MediaBrowserCompat browser = getBrowserCompat();
+        if (browser == null) {
+            return;
+        }
+
+        Bundle options = MediaUtils2.createBundle(extras);
         options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
         options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
-        // TODO: Revisit using default browser is OK. See TODO in subscribe
-        getBrowserCompat().subscribe(parentId, options,
-                new GetChildrenCallback(parentId, page, pageSize));
+        browser.subscribe(parentId, options, new GetChildrenCallback(parentId, page, pageSize));
     }
 
     /**
@@ -303,8 +315,11 @@
      * @param mediaId media id for specifying the item
      */
     public void getItem(@NonNull final String mediaId) {
-        // TODO: Revisit using default browser is OK. See TODO in subscribe
-        getBrowserCompat().getItem(mediaId, new ItemCallback() {
+        MediaBrowserCompat browser = getBrowserCompat();
+        if (browser == null) {
+            return;
+        }
+        browser.getItem(mediaId, new ItemCallback() {
             @Override
             public void onItemLoaded(final MediaItem item) {
                 getCallbackExecutor().execute(new Runnable() {
@@ -338,7 +353,28 @@
      * @param extras extra bundle
      */
     public void search(@NonNull String query, @Nullable Bundle extras) {
-        // TODO: Implement
+        MediaBrowserCompat browser = getBrowserCompat();
+        if (browser == null) {
+            return;
+        }
+        browser.search(query, extras, new MediaBrowserCompat.SearchCallback() {
+            @Override
+            public void onSearchResult(final String query, final Bundle extras,
+                    final List<MediaItem> items) {
+                getCallbackExecutor().execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCallback().onSearchResultChanged(
+                                MediaBrowser2.this, query, items.size(), extras);
+                    }
+                });
+            }
+
+            @Override
+            public void onError(final String query, final Bundle extras) {
+                // Currently no way to tell failures in MediaBrowser2#search().
+            }
+        });
     }
 
     /**
@@ -351,9 +387,40 @@
      * @param pageSize page size. Should be greater or equal to {@code 1}
      * @param extras extra bundle
      */
-    public void getSearchResult(@NonNull String query, int page, int pageSize,
-            @Nullable Bundle extras) {
-        // TODO: Implement
+    public void getSearchResult(final @NonNull String query, final int page, final int pageSize,
+            final @Nullable Bundle extras) {
+        MediaBrowserCompat browser = getBrowserCompat();
+        if (browser == null) {
+            return;
+        }
+        Bundle options = MediaUtils2.createBundle(extras);
+        options.putInt(ARGUMENT_PAGE, page);
+        options.putInt(ARGUMENT_PAGE_SIZE, pageSize);
+        browser.search(query, options, new MediaBrowserCompat.SearchCallback() {
+            @Override
+            public void onSearchResult(final String query, final Bundle extrasSent,
+                    final List<MediaItem> items) {
+                getCallbackExecutor().execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        List<MediaItem2> item2List = MediaUtils2.toMediaItem2List(items);
+                        getCallback().onGetSearchResultDone(
+                                MediaBrowser2.this, query, page, pageSize, item2List, extras);
+                    }
+                });
+            }
+
+            @Override
+            public void onError(final String query, final Bundle extrasSent) {
+                getCallbackExecutor().execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCallback().onGetSearchResultDone(
+                                MediaBrowser2.this, query, page, pageSize, null, extras);
+                    }
+                });
+            }
+        });
     }
 
     @Override
@@ -367,6 +434,20 @@
         }
     }
 
+    private Bundle getExtrasWithoutPagination(Bundle extras) {
+        if (extras == null) {
+            return null;
+        }
+        extras.setClassLoader(getContext().getClassLoader());
+        try {
+            extras.remove(MediaBrowserCompat.EXTRA_PAGE);
+            extras.remove(MediaBrowserCompat.EXTRA_PAGE_SIZE);
+        } catch (BadParcelableException e) {
+            // Pass through...
+        }
+        return extras;
+    }
+
     private class GetLibraryRootCallback extends MediaBrowserCompat.ConnectionCallback {
         private final Bundle mExtras;
 
@@ -433,11 +514,14 @@
                 // Currently no way to tell failures in MediaBrowser2#subscribe().
                 return;
             }
+
+            final Bundle notifyChildrenChangedOptions =
+                    getBrowserCompat().getNotifyChildrenChangedOptions();
             getCallbackExecutor().execute(new Runnable() {
                 @Override
                 public void run() {
                     getCallback().onChildrenChanged(MediaBrowser2.this, parentId, itemCount,
-                            options);
+                            notifyChildrenChangedOptions);
                 }
             });
         }
@@ -472,7 +556,7 @@
 
         @Override
         public void onChildrenLoaded(final String parentId, List<MediaItem> children,
-                final Bundle options) {
+                Bundle options) {
             final List<MediaItem2> items;
             if (children == null) {
                 items = null;
@@ -482,12 +566,17 @@
                     items.add(MediaUtils2.createMediaItem2(children.get(i)));
                 }
             }
+            final Bundle extras = getExtrasWithoutPagination(options);
             getCallbackExecutor().execute(new Runnable() {
                 @Override
                 public void run() {
+                    MediaBrowserCompat browser = getBrowserCompat();
+                    if (browser == null) {
+                        return;
+                    }
                     getCallback().onGetChildrenDone(MediaBrowser2.this, parentId, mPage, mPageSize,
-                            items, options);
-                    getBrowserCompat().unsubscribe(mParentId, GetChildrenCallback.this);
+                            items, extras);
+                    browser.unsubscribe(mParentId, GetChildrenCallback.this);
                 }
             });
         }
diff --git a/media/src/main/java/androidx/media/MediaBrowserCompatUtils.java b/media/src/main/java/androidx/media/MediaBrowserCompatUtils.java
index c553256..b9491b6 100644
--- a/media/src/main/java/androidx/media/MediaBrowserCompatUtils.java
+++ b/media/src/main/java/androidx/media/MediaBrowserCompatUtils.java
@@ -70,12 +70,8 @@
             endIndex2 = startIndex2 + pageSize2 - 1;
         }
 
-        if (startIndex1 <= startIndex2 && startIndex2 <= endIndex1) {
-            return true;
-        } else if (startIndex1 <= endIndex2 && endIndex2 <= endIndex1) {
-            return true;
-        }
-        return false;
+        // For better readability, leaving the exclamation mark here.
+        return !(endIndex1 < startIndex2 || endIndex2 < startIndex1);
     }
 
     private MediaBrowserCompatUtils() {
diff --git a/media/src/main/java/androidx/media/MediaBrowserProtocol.java b/media/src/main/java/androidx/media/MediaBrowserProtocol.java
index 5c85880..eb5b449 100644
--- a/media/src/main/java/androidx/media/MediaBrowserProtocol.java
+++ b/media/src/main/java/androidx/media/MediaBrowserProtocol.java
@@ -15,6 +15,7 @@
  */
 package androidx.media;
 
+import android.os.Bundle;
 import android.support.v4.media.MediaBrowserCompat;
 
 import androidx.annotation.RestrictTo;
@@ -29,10 +30,13 @@
 
     public static final String DATA_CALLBACK_TOKEN = "data_callback_token";
     public static final String DATA_CALLING_UID = "data_calling_uid";
+    public static final String DATA_CALLING_PID = "data_calling_pid";
     public static final String DATA_MEDIA_ITEM_ID = "data_media_item_id";
     public static final String DATA_MEDIA_ITEM_LIST = "data_media_item_list";
     public static final String DATA_MEDIA_SESSION_TOKEN = "data_media_session_token";
     public static final String DATA_OPTIONS = "data_options";
+    public static final String DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS =
+            "data_notify_children_changed_options";
     public static final String DATA_PACKAGE_NAME = "data_package_name";
     public static final String DATA_RESULT_RECEIVER = "data_result_receiver";
     public static final String DATA_ROOT_HINTS = "data_root_hints";
@@ -94,6 +98,9 @@
      *     DATA_MEDIA_ITEM_LIST : An array list for the media item children
      *     DATA_OPTIONS : A bundle of service-specific arguments sent from the media browse to
      *                    the media browser service
+     *     DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS : A bundle of service-specific arguments sent from
+     *                    the media browser service to the media browser by calling
+     *                    {@link MediaBrowserServiceCompat#notifyChildrenChanged(String, Bundle)}
      */
     public static final int SERVICE_MSG_ON_LOAD_CHILDREN = 3;
 
diff --git a/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java b/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
index 8f24837..5da2287 100644
--- a/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
+++ b/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
@@ -27,12 +27,14 @@
 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_SEND_CUSTOM_ACTION;
 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER;
 import static androidx.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN;
+import static androidx.media.MediaBrowserProtocol.DATA_CALLING_PID;
 import static androidx.media.MediaBrowserProtocol.DATA_CALLING_UID;
 import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION;
 import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION_EXTRAS;
 import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID;
 import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST;
 import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN;
+import static androidx.media.MediaBrowserProtocol.DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS;
 import static androidx.media.MediaBrowserProtocol.DATA_OPTIONS;
 import static androidx.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
 import static androidx.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
@@ -61,6 +63,7 @@
 import android.os.Messenger;
 import android.os.Parcel;
 import android.os.RemoteException;
+import android.service.media.MediaBrowserService;
 import android.support.v4.media.MediaBrowserCompat;
 import android.support.v4.media.session.IMediaSession;
 import android.support.v4.media.session.MediaSessionCompat;
@@ -76,6 +79,7 @@
 import androidx.collection.ArrayMap;
 import androidx.core.app.BundleCompat;
 import androidx.core.util.Pair;
+import androidx.media.MediaSessionManager.RemoteUserInfo;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -167,9 +171,10 @@
     /** @hide */
     @RestrictTo(LIBRARY)
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef(flag=true, value = { RESULT_FLAG_OPTION_NOT_HANDLED,
-            RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED, RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED })
-    private @interface ResultFlags { }
+    @IntDef(flag = true, value = {RESULT_FLAG_OPTION_NOT_HANDLED,
+            RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED, RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED})
+    private @interface ResultFlags {
+    }
 
     final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>();
     ConnectionRecord mCurConnection;
@@ -182,6 +187,8 @@
         void setSessionToken(MediaSessionCompat.Token token);
         void notifyChildrenChanged(final String parentId, final Bundle options);
         Bundle getBrowserRootHints();
+        RemoteUserInfo getCurrentBrowserInfo();
+        List<RemoteUserInfo> getSubscribingBrowsers(String parentId);
     }
 
     class MediaBrowserServiceImplBase implements MediaBrowserServiceImpl {
@@ -206,7 +213,7 @@
                 @Override
                 public void run() {
                     Iterator<ConnectionRecord> iter = mConnections.values().iterator();
-                    while (iter.hasNext()){
+                    while (iter.hasNext()) {
                         ConnectionRecord connection = iter.next();
                         try {
                             connection.callbacks.onConnect(connection.root.getRootId(), token,
@@ -233,7 +240,8 @@
                             for (Pair<IBinder, Bundle> callback : callbackList) {
                                 if (MediaBrowserCompatUtils.hasDuplicatedItems(
                                         options, callback.second)) {
-                                    performLoadChildren(parentId, connection, callback.second);
+                                    performLoadChildren(parentId, connection, callback.second,
+                                            options);
                                 }
                             }
                         }
@@ -246,10 +254,33 @@
         public Bundle getBrowserRootHints() {
             if (mCurConnection == null) {
                 throw new IllegalStateException("This should be called inside of onLoadChildren,"
-                        + " onLoadItem or onSearch methods");
+                        + " onLoadItem, onSearch, or onCustomAction methods");
             }
             return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
         }
+
+        @Override
+        public RemoteUserInfo getCurrentBrowserInfo() {
+            if (mCurConnection == null) {
+                throw new IllegalStateException("This should be called inside of onLoadChildren,"
+                        + " onLoadItem, onSearch, or onCustomAction methods");
+            }
+            return mCurConnection.browserInfo;
+        }
+
+        @Override
+        public List<RemoteUserInfo> getSubscribingBrowsers(String parentId) {
+            List<RemoteUserInfo> result = new ArrayList<>();
+            for (IBinder binder : mConnections.keySet()) {
+                ConnectionRecord connection = mConnections.get(binder);
+                List<Pair<IBinder, Bundle>> callbackList =
+                        connection.subscriptions.get(parentId);
+                if (callbackList != null) {
+                    result.add(connection.browserInfo);
+                }
+            }
+            return result;
+        }
     }
 
     @RequiresApi(21)
@@ -298,19 +329,6 @@
         }
 
         @Override
-        public Bundle getBrowserRootHints() {
-            if (mMessenger == null) {
-                // TODO: Handle getBrowserRootHints when connected with framework MediaBrowser.
-                return null;
-            }
-            if (mCurConnection == null) {
-                throw new IllegalStateException("This should be called inside of onLoadChildren,"
-                        + " onLoadItem or onSearch methods");
-            }
-            return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
-        }
-
-        @Override
         public MediaBrowserServiceCompatApi21.BrowserRoot onGetRoot(
                 String clientPackageName, int clientUid, Bundle rootHints) {
             Bundle rootExtras = null;
@@ -328,8 +346,13 @@
                     mRootExtrasList.add(rootExtras);
                 }
             }
+            // We aren't sure whether this connection request would be accepted.
+            // Temporarily set mCurConnection just to make getCurrentBrowserInfo() working.
+            mCurConnection = new ConnectionRecord(clientPackageName, -1, clientUid, rootHints,
+                    null);
             BrowserRoot root = MediaBrowserServiceCompat.this.onGetRoot(
                     clientPackageName, clientUid, rootHints);
+            mCurConnection = null;
             if (root == null) {
                 return null;
             }
@@ -369,6 +392,20 @@
             MediaBrowserServiceCompat.this.onLoadChildren(parentId, result);
         }
 
+        @Override
+        public List<RemoteUserInfo> getSubscribingBrowsers(String parentId) {
+            List<RemoteUserInfo> result = new ArrayList<>();
+            for (IBinder binder : mConnections.keySet()) {
+                ConnectionRecord connection = mConnections.get(binder);
+                List<Pair<IBinder, Bundle>> callbackList =
+                        connection.subscriptions.get(parentId);
+                if (callbackList != null) {
+                    result.add(connection.browserInfo);
+                }
+            }
+            return result;
+        }
+
         void notifyChildrenChangedForFramework(final String parentId, final Bundle options) {
             MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId);
         }
@@ -385,7 +422,8 @@
                             for (Pair<IBinder, Bundle> callback : callbackList) {
                                 if (MediaBrowserCompatUtils.hasDuplicatedItems(
                                         options, callback.second)) {
-                                    performLoadChildren(parentId, connection, callback.second);
+                                    performLoadChildren(parentId, connection, callback.second,
+                                            options);
                                 }
                             }
                         }
@@ -393,6 +431,28 @@
                 }
             });
         }
+
+        @Override
+        public Bundle getBrowserRootHints() {
+            if (mMessenger == null) {
+                // TODO: Handle getBrowserRootHints when connected with framework MediaBrowser.
+                return null;
+            }
+            if (mCurConnection == null) {
+                throw new IllegalStateException("This should be called inside of onGetRoot,"
+                        + " onLoadChildren, onLoadItem, onSearch, or onCustomAction methods");
+            }
+            return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
+        }
+
+        @Override
+        public RemoteUserInfo getCurrentBrowserInfo() {
+            if (mCurConnection == null) {
+                throw new IllegalStateException("This should be called inside of onGetRoot,"
+                        + " onLoadChildren, onLoadItem, onSearch, or onCustomAction methods");
+            }
+            return mCurConnection.browserInfo;
+        }
     }
 
     @RequiresApi(23)
@@ -469,7 +529,7 @@
 
         @Override
         public Bundle getBrowserRootHints() {
-            // If EXTRA_MESSENGER_BINDER is used, mCurConnection is not null.
+            // mCurConnection is not null when EXTRA_MESSENGER_BINDER is used.
             if (mCurConnection != null) {
                 return mCurConnection.rootHints == null ? null
                         : new Bundle(mCurConnection.rootHints);
@@ -488,6 +548,21 @@
         }
     }
 
+    @RequiresApi(28)
+    class MediaBrowserServiceImplApi28 extends MediaBrowserServiceImplApi26 {
+        @Override
+        public RemoteUserInfo getCurrentBrowserInfo() {
+            // mCurConnection is not null when EXTRA_MESSENGER_BINDER is used.
+            if (mCurConnection != null) {
+                return mCurConnection.browserInfo;
+            }
+            android.media.session.MediaSessionManager.RemoteUserInfo userInfoObj =
+                    ((MediaBrowserService) mServiceObj).getCurrentBrowserInfo();
+            return new RemoteUserInfo(
+                    userInfoObj.getPackageName(), userInfoObj.getPid(), userInfoObj.getUid());
+        }
+    }
+
     private final class ServiceHandler extends Handler {
         private final ServiceBinderImpl mServiceBinderImpl = new ServiceBinderImpl();
 
@@ -500,7 +575,8 @@
             switch (msg.what) {
                 case CLIENT_MSG_CONNECT:
                     mServiceBinderImpl.connect(data.getString(DATA_PACKAGE_NAME),
-                            data.getInt(DATA_CALLING_UID), data.getBundle(DATA_ROOT_HINTS),
+                            data.getInt(DATA_CALLING_PID), data.getInt(DATA_CALLING_UID),
+                            data.getBundle(DATA_ROOT_HINTS),
                             new ServiceCallbacksCompat(msg.replyTo));
                     break;
                 case CLIENT_MSG_DISCONNECT:
@@ -524,7 +600,8 @@
                     break;
                 case CLIENT_MSG_REGISTER_CALLBACK_MESSENGER:
                     mServiceBinderImpl.registerCallbacks(new ServiceCallbacksCompat(msg.replyTo),
-                            data.getBundle(DATA_ROOT_HINTS));
+                            data.getString(DATA_PACKAGE_NAME), data.getInt(DATA_CALLING_PID),
+                            data.getInt(DATA_CALLING_UID), data.getBundle(DATA_ROOT_HINTS));
                     break;
                 case CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER:
                     mServiceBinderImpl.unregisterCallbacks(new ServiceCallbacksCompat(msg.replyTo));
@@ -555,6 +632,7 @@
             Bundle data = msg.getData();
             data.setClassLoader(MediaBrowserCompat.class.getClassLoader());
             data.putInt(DATA_CALLING_UID, Binder.getCallingUid());
+            data.putInt(DATA_CALLING_PID, Binder.getCallingPid());
             return super.sendMessageAtTime(msg, uptimeMillis);
         }
 
@@ -571,13 +649,23 @@
      * All the info about a connection.
      */
     private class ConnectionRecord implements IBinder.DeathRecipient {
-        String pkg;
-        Bundle rootHints;
-        ServiceCallbacks callbacks;
-        BrowserRoot root;
-        HashMap<String, List<Pair<IBinder, Bundle>>> subscriptions = new HashMap<>();
+        public final String pkg;
+        public final int pid;
+        public final int uid;
+        public final RemoteUserInfo browserInfo;
+        public final Bundle rootHints;
+        public final ServiceCallbacks callbacks;
+        public final HashMap<String, List<Pair<IBinder, Bundle>>> subscriptions = new HashMap<>();
+        public BrowserRoot root;
 
-        ConnectionRecord() {
+        ConnectionRecord(String pkg, int pid, int uid, Bundle rootHints,
+                ServiceCallbacks callback) {
+            this.pkg = pkg;
+            this.pid = pid;
+            this.uid = uid;
+            this.browserInfo = new RemoteUserInfo(pkg, pid, uid);
+            this.rootHints = rootHints;
+            this.callbacks = callback;
         }
 
         @Override
@@ -740,7 +828,7 @@
         ServiceBinderImpl() {
         }
 
-        public void connect(final String pkg, final int uid, final Bundle rootHints,
+        public void connect(final String pkg, final int pid, final int uid, final Bundle rootHints,
                 final ServiceCallbacks callbacks) {
 
             if (!isValidPackage(pkg, uid)) {
@@ -756,13 +844,11 @@
                     // Clear out the old subscriptions. We are getting new ones.
                     mConnections.remove(b);
 
-                    final ConnectionRecord connection = new ConnectionRecord();
-                    connection.pkg = pkg;
-                    connection.rootHints = rootHints;
-                    connection.callbacks = callbacks;
-
-                    connection.root =
-                            MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints);
+                    final ConnectionRecord connection = new ConnectionRecord(pkg, pid, uid,
+                            rootHints, callbacks);
+                    mCurConnection = connection;
+                    connection.root = MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints);
+                    mCurConnection = null;
 
                     // If they didn't return something, don't allow this client.
                     if (connection.root == null) {
@@ -872,7 +958,8 @@
         }
 
         // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used.
-        public void registerCallbacks(final ServiceCallbacks callbacks, final Bundle rootHints) {
+        public void registerCallbacks(final ServiceCallbacks callbacks, final String pkg,
+                final int pid, final int uid, final Bundle rootHints) {
             mHandler.postOrRun(new Runnable() {
                 @Override
                 public void run() {
@@ -880,9 +967,8 @@
                     // Clear out the old subscriptions. We are getting new ones.
                     mConnections.remove(b);
 
-                    final ConnectionRecord connection = new ConnectionRecord();
-                    connection.callbacks = callbacks;
-                    connection.rootHints = rootHints;
+                    final ConnectionRecord connection = new ConnectionRecord(pkg, pid, uid,
+                            rootHints, callbacks);
                     mConnections.put(b, connection);
                     try {
                         b.linkToDeath(connection, 0);
@@ -956,8 +1042,8 @@
         void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
                 throws RemoteException;
         void onConnectFailed() throws RemoteException;
-        void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options)
-                throws RemoteException;
+        void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options,
+                Bundle notifyChildrenChangedOptions) throws RemoteException;
     }
 
     private static class ServiceCallbacksCompat implements ServiceCallbacks {
@@ -993,10 +1079,11 @@
 
         @Override
         public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list,
-                Bundle options) throws RemoteException {
+                Bundle options, Bundle notifyChildrenChangedOptions) throws RemoteException {
             Bundle data = new Bundle();
             data.putString(DATA_MEDIA_ITEM_ID, mediaId);
             data.putBundle(DATA_OPTIONS, options);
+            data.putBundle(DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS, notifyChildrenChangedOptions);
             if (list != null) {
                 data.putParcelableArrayList(DATA_MEDIA_ITEM_LIST,
                         list instanceof ArrayList ? (ArrayList) list : new ArrayList<>(list));
@@ -1031,7 +1118,9 @@
     @Override
     public void onCreate() {
         super.onCreate();
-        if (Build.VERSION.SDK_INT >= 26) {
+        if (Build.VERSION.SDK_INT >= 28) {
+            mImpl = new MediaBrowserServiceImplApi28();
+        } else if (Build.VERSION.SDK_INT >= 26) {
             mImpl = new MediaBrowserServiceImplApi26();
         } else if (Build.VERSION.SDK_INT >= 23) {
             mImpl = new MediaBrowserServiceImplApi23();
@@ -1253,6 +1342,17 @@
     }
 
     /**
+     * Gets the browser information who sent the current request.
+     *
+     * @throws IllegalStateException If this method is called outside of {@link #onGetRoot} or
+     *             {@link #onLoadChildren} or {@link #onLoadItem}.
+     * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo)
+     */
+    public final @NonNull RemoteUserInfo getCurrentBrowserInfo() {
+        return mImpl.getCurrentBrowserInfo();
+    }
+
+    /**
      * Notifies all connected media browsers that the children of
      * the specified parent id have changed in some way.
      * This will cause browsers to fetch subscribed content again.
@@ -1289,6 +1389,18 @@
     }
 
     /**
+     * Gets {@link RemoteUserInfo} of all browsers which are subscribing to the given parentId.
+     * @hide
+     */
+    @RestrictTo(LIBRARY)
+    public @NonNull List<RemoteUserInfo> getSubscribingBrowsers(@NonNull String parentId) {
+        if (parentId == null) {
+            throw new IllegalArgumentException("parentId cannot be null in getSubscribingBrowsers");
+        }
+        return mImpl.getSubscribingBrowsers(parentId);
+    }
+
+    /**
      * Return whether the given package is one of the ones that is owned by the uid.
      */
     boolean isValidPackage(String pkg, int uid) {
@@ -1325,7 +1437,7 @@
         callbackList.add(new Pair<>(token, options));
         connection.subscriptions.put(id, callbackList);
         // send the results
-        performLoadChildren(id, connection, options);
+        performLoadChildren(id, connection, options, null);
     }
 
     /**
@@ -1358,7 +1470,7 @@
      * Callers must make sure that this connection is still connected.
      */
     void performLoadChildren(final String parentId, final ConnectionRecord connection,
-            final Bundle options) {
+            final Bundle subscribeOptions, final Bundle notifyChildrenChangedOptions) {
         final Result<List<MediaBrowserCompat.MediaItem>> result
                 = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
             @Override
@@ -1373,9 +1485,10 @@
 
                 List<MediaBrowserCompat.MediaItem> filteredList =
                         (getFlags() & RESULT_FLAG_OPTION_NOT_HANDLED) != 0
-                                ? applyOptions(list, options) : list;
+                                ? applyOptions(list, subscribeOptions) : list;
                 try {
-                    connection.callbacks.onLoadChildren(parentId, filteredList, options);
+                    connection.callbacks.onLoadChildren(parentId, filteredList, subscribeOptions,
+                            notifyChildrenChangedOptions);
                 } catch (RemoteException ex) {
                     // The other side is in the process of crashing.
                     Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId
@@ -1385,10 +1498,10 @@
         };
 
         mCurConnection = connection;
-        if (options == null) {
+        if (subscribeOptions == null) {
             onLoadChildren(parentId, result);
         } else {
-            onLoadChildren(parentId, result, options);
+            onLoadChildren(parentId, result, subscribeOptions);
         }
         mCurConnection = null;
 
diff --git a/media/src/main/java/androidx/media/MediaConstants2.java b/media/src/main/java/androidx/media/MediaConstants2.java
index 68a9a19..1fec6db 100644
--- a/media/src/main/java/androidx/media/MediaConstants2.java
+++ b/media/src/main/java/androidx/media/MediaConstants2.java
@@ -33,8 +33,10 @@
             "androidx.media.session.event.ON_PLAYBACK_INFO_CHANGED";
     static final String SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED =
             "androidx.media.session.event.ON_PLAYBACK_SPEED_CHANGED";
-    static final String SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED =
+    static final String SESSION_EVENT_ON_BUFFERING_STATE_CHANGED =
             "androidx.media.session.event.ON_BUFFERING_STATE_CHANGED";
+    static final String SESSION_EVENT_ON_SEEK_COMPLETED =
+            "androidx.media.session.event.ON_SEEK_COMPLETED";
     static final String SESSION_EVENT_ON_REPEAT_MODE_CHANGED =
             "androidx.media.session.event.ON_REPEAT_MODE_CHANGED";
     static final String SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED =
@@ -45,6 +47,10 @@
             "androidx.media.session.event.ON_PLAYLIST_METADATA_CHANGED";
     static final String SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED =
             "androidx.media.session.event.ON_ALLOWED_COMMANDS_CHANGED";
+    static final String SESSION_EVENT_ON_CHILDREN_CHANGED =
+            "androidx.media.session.event.ON_CHILDREN_CHANGED";
+    static final String SESSION_EVENT_ON_SEARCH_RESULT_CHANGED =
+            "androidx.media.session.event.ON_SEARCH_RESULT_CHANGED";
     static final String SESSION_EVENT_SEND_CUSTOM_COMMAND =
             "androidx.media.session.event.SEND_CUSTOM_COMMAND";
     static final String SESSION_EVENT_SET_CUSTOM_LAYOUT =
@@ -89,6 +95,9 @@
     static final String ARGUMENT_COMMAND_BUTTONS = "androidx.media.argument.COMMAND_BUTTONS";
     static final String ARGUMENT_ROUTE_BUNDLE = "androidx.media.argument.ROUTE_BUNDLE";
     static final String ARGUMENT_PLAYBACK_INFO = "androidx.media.argument.PLAYBACK_INFO";
+    static final String ARGUMENT_ITEM_COUNT = "androidx.media.argument.ITEM_COUNT";
+    static final String ARGUMENT_PAGE = "androidx.media.argument.PAGE";
+    static final String ARGUMENT_PAGE_SIZE = "androidx.media.argument.PAGE_SIZE";
 
     static final String ARGUMENT_ICONTROLLER_CALLBACK =
             "androidx.media.argument.ICONTROLLER_CALLBACK";
diff --git a/media/src/main/java/androidx/media/MediaController2.java b/media/src/main/java/androidx/media/MediaController2.java
index 1da552d..5d81cc2 100644
--- a/media/src/main/java/androidx/media/MediaController2.java
+++ b/media/src/main/java/androidx/media/MediaController2.java
@@ -16,93 +16,7 @@
 
 package androidx.media;
 
-import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION;
-
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import static androidx.media.MediaConstants2.ARGUMENT_ALLOWED_COMMANDS;
-import static androidx.media.MediaConstants2.ARGUMENT_ARGUMENTS;
-import static androidx.media.MediaConstants2.ARGUMENT_BUFFERING_STATE;
-import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_BUTTONS;
-import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_CODE;
-import static androidx.media.MediaConstants2.ARGUMENT_CUSTOM_COMMAND;
-import static androidx.media.MediaConstants2.ARGUMENT_ERROR_CODE;
-import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
-import static androidx.media.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK;
-import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ID;
-import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ITEM;
-import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME;
-import static androidx.media.MediaConstants2.ARGUMENT_PID;
-import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_INFO;
-import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_SPEED;
-import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_STATE_COMPAT;
-import static androidx.media.MediaConstants2.ARGUMENT_PLAYER_STATE;
-import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST;
-import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_INDEX;
-import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_METADATA;
-import static androidx.media.MediaConstants2.ARGUMENT_QUERY;
-import static androidx.media.MediaConstants2.ARGUMENT_RATING;
-import static androidx.media.MediaConstants2.ARGUMENT_REPEAT_MODE;
-import static androidx.media.MediaConstants2.ARGUMENT_RESULT_RECEIVER;
-import static androidx.media.MediaConstants2.ARGUMENT_ROUTE_BUNDLE;
-import static androidx.media.MediaConstants2.ARGUMENT_SEEK_POSITION;
-import static androidx.media.MediaConstants2.ARGUMENT_SHUFFLE_MODE;
-import static androidx.media.MediaConstants2.ARGUMENT_UID;
-import static androidx.media.MediaConstants2.ARGUMENT_URI;
-import static androidx.media.MediaConstants2.ARGUMENT_VOLUME;
-import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_DIRECTION;
-import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_FLAGS;
-import static androidx.media.MediaConstants2.CONNECT_RESULT_CONNECTED;
-import static androidx.media.MediaConstants2.CONNECT_RESULT_DISCONNECTED;
-import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_COMMAND_CODE;
-import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_CUSTOM_COMMAND;
-import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_CONNECT;
-import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_DISCONNECT;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYER_STATE_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_REPEAT_MODE_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ROUTES_INFO_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_SEND_CUSTOM_COMMAND;
-import static androidx.media.MediaConstants2.SESSION_EVENT_SET_CUSTOM_LAYOUT;
-import static androidx.media.MediaPlayerBase.BUFFERING_STATE_UNKNOWN;
-import static androidx.media.MediaPlayerBase.UNKNOWN_TIME;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_RESET;
-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_REMOVE_ITEM;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_REWIND;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SELECT_ROUTE;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SET_RATING;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO;
-import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME;
-import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME;
 
 import android.annotation.TargetApi;
 import android.app.PendingIntent;
@@ -111,25 +25,14 @@
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.Process;
-import android.os.RemoteException;
 import android.os.ResultReceiver;
-import android.os.SystemClock;
 import android.support.v4.media.MediaBrowserCompat;
-import android.support.v4.media.MediaMetadataCompat;
-import android.support.v4.media.session.MediaControllerCompat;
-import android.support.v4.media.session.MediaSessionCompat;
-import android.support.v4.media.session.PlaybackStateCompat;
-import android.util.Log;
 
-import androidx.annotation.GuardedBy;
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
 import androidx.media.MediaPlaylistAgent.RepeatMode;
 import androidx.media.MediaPlaylistAgent.ShuffleMode;
 import androidx.media.MediaSession2.CommandButton;
@@ -142,8 +45,9 @@
 import java.util.concurrent.Executor;
 
 /**
- * Allows an app to interact with an active {@link MediaSession2} in any status. Media buttons and
- * other commands can be sent to the session.
+ * 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.
  * <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,9 +55,16 @@
  * 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>
  * MediaController2 objects are thread-safe.
  * <p>
  * @see MediaSession2
+ * @see MediaSessionService2
  */
 @TargetApi(Build.VERSION_CODES.KITKAT)
 public class MediaController2 implements AutoCloseable {
@@ -176,6 +87,657 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface VolumeFlags {}
 
+    private final SupportLibraryImpl mImpl;
+    // For testing.
+    Long mTimeDiff;
+
+    /**
+     * Create a {@link MediaController2} from the {@link SessionToken2}.
+     * This connects to the session and may wake up the service if it's not available.
+     *
+     * @param context Context
+     * @param token token to connect to
+     * @param executor executor to run callbacks on.
+     * @param callback controller callback to receive changes in
+     */
+    public MediaController2(@NonNull Context context, @NonNull SessionToken2 token,
+            @NonNull Executor executor, @NonNull ControllerCallback callback) {
+        mImpl = new MediaController2ImplBase(context, token, executor, callback);
+        mImpl.setInstance(this);
+    }
+
+    /**
+     * Release this object, and disconnect from the session. After this, callbacks wouldn't be
+     * received.
+     */
+    @Override
+    public void close() {
+        try {
+            mImpl.close();
+        } catch (Exception e) {
+            // Should not be here.
+        }
+    }
+
+    /**
+     * @return token
+     */
+    public @NonNull SessionToken2 getSessionToken() {
+        return mImpl.getSessionToken();
+    }
+
+    /**
+     * Returns whether this class is connected to active {@link MediaSession2} or not.
+     */
+    public boolean isConnected() {
+        return mImpl.isConnected();
+    }
+
+    /**
+     * Requests that the player starts or resumes playback.
+     */
+    public void play() {
+        mImpl.play();
+    }
+
+    /**
+     * Requests that the player pauses playback.
+     */
+    public void pause() {
+        mImpl.pause();
+    }
+
+    /**
+     * Requests that the player be reset to its uninitialized state.
+     */
+    public void reset() {
+        mImpl.reset();
+    }
+
+    /**
+     * Request that the player prepare its playback. In other words, other sessions can continue
+     * to play during the preparation of this session. This method can be used to speed up the
+     * start of the playback. Once the preparation is done, the session will change its playback
+     * state to {@link MediaPlayerInterface#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be
+     * called to start playback.
+     */
+    public void prepare() {
+        mImpl.prepare();
+    }
+
+    /**
+     * Start fast forwarding. If playback is already fast forwarding this
+     * may increase the rate.
+     */
+    public void fastForward() {
+        mImpl.fastForward();
+    }
+
+    /**
+     * Start rewinding. If playback is already rewinding this may increase
+     * the rate.
+     */
+    public void rewind() {
+        mImpl.rewind();
+    }
+
+    /**
+     * Move to a new location in the media stream.
+     *
+     * @param pos Position to move to, in milliseconds.
+     */
+    public void seekTo(long pos) {
+        mImpl.seekTo(pos);
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public void skipForward() {
+        // To match with KEYCODE_MEDIA_SKIP_FORWARD
+        mImpl.skipForward();
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public void skipBackward() {
+        // To match with KEYCODE_MEDIA_SKIP_BACKWARD
+        mImpl.skipBackward();
+    }
+
+    /**
+     * Request that the player start playback for a specific media id.
+     *
+     * @param mediaId The id of the requested media.
+     * @param extras Optional extras that can include extra information about the media item
+     *               to be played.
+     */
+    public void playFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
+        mImpl.playFromMediaId(mediaId, extras);
+    }
+
+    /**
+     * Request that the player start playback for a specific search query.
+     *
+     * @param query The search query. Should not be an empty string.
+     * @param extras Optional extras that can include extra information about the query.
+     */
+    public void playFromSearch(@NonNull String query, @Nullable Bundle extras) {
+        mImpl.playFromSearch(query, extras);
+    }
+
+    /**
+     * Request that the player start playback for a specific {@link Uri}.
+     *
+     * @param uri The URI of the requested media.
+     * @param extras Optional extras that can include extra information about the media item
+     *               to be played.
+     */
+    public void playFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
+        mImpl.playFromUri(uri, extras);
+    }
+
+    /**
+     * Request that the player prepare playback for a specific media id. In other words, other
+     * sessions can continue to play during the preparation of this session. This method can be
+     * used to speed up the start of the playback. Once the preparation is done, the session
+     * will change its playback state to {@link MediaPlayerInterface#PLAYER_STATE_PAUSED}.
+     * Afterwards, {@link #play} can be called to start playback. If the preparation is not needed,
+     * {@link #playFromMediaId} can be directly called without this method.
+     *
+     * @param mediaId The id of the requested media.
+     * @param extras Optional extras that can include extra information about the media item
+     *               to be prepared.
+     */
+    public void prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
+        mImpl.prepareFromMediaId(mediaId, extras);
+    }
+
+    /**
+     * Request that the player prepare playback for a specific search query.
+     * In other words, other sessions can continue to play during the preparation of this session.
+     * This method can be used to speed up the start of the playback.
+     * Once the preparation is done, the session will change its playback state to
+     * {@link MediaPlayerInterface#PLAYER_STATE_PAUSED}. Afterwards,
+     * {@link #play} can be called to start playback. If the preparation is not needed,
+     * {@link #playFromSearch} can be directly called without this method.
+     *
+     * @param query The search query. Should not be an empty string.
+     * @param extras Optional extras that can include extra information about the query.
+     */
+    public void prepareFromSearch(@NonNull String query, @Nullable Bundle extras) {
+        mImpl.prepareFromSearch(query, extras);
+    }
+
+    /**
+     * Request that the player prepare playback for a specific {@link Uri}. In other words,
+     * other sessions can continue to play during the preparation of this session. This method
+     * can be used to speed up the start of the playback. Once the preparation is done, the
+     * session will change its playback state to {@link MediaPlayerInterface#PLAYER_STATE_PAUSED}.
+     * Afterwards, {@link #play} can be called to start playback. If the preparation is not needed,
+     * {@link #playFromUri} can be directly called without this method.
+     *
+     * @param uri The URI of the requested media.
+     * @param extras Optional extras that can include extra information about the media item
+     *               to be prepared.
+     */
+    public void prepareFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
+        mImpl.prepareFromUri(uri, extras);
+    }
+
+    /**
+     * Set the volume of the output this session is playing on. The command will be ignored if it
+     * does not support {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}.
+     * <p>
+     * If the session is local playback, this changes the device's volume with the stream that
+     * session's player is using. Flags will be specified for the {@link AudioManager}.
+     * <p>
+     * If the session is remote player (i.e. session has set volume provider), its volume provider
+     * will receive this request instead.
+     *
+     * @see #getPlaybackInfo()
+     * @param value The value to set it to, between 0 and the reported max.
+     * @param flags flags from {@link AudioManager} to include with the volume request for local
+     *              playback
+     */
+    public void setVolumeTo(int value, @VolumeFlags int flags) {
+        mImpl.setVolumeTo(value, flags);
+    }
+
+    /**
+     * 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}.
+     * <p>
+     * If the session is local playback, this changes the device's volume with the stream that
+     * session's player is using. Flags will be specified for the {@link AudioManager}.
+     * <p>
+     * If the session is remote player (i.e. session has set volume provider), its volume provider
+     * will receive this request instead.
+     *
+     * @see #getPlaybackInfo()
+     * @param direction The direction to adjust the volume in.
+     * @param flags flags from {@link AudioManager} to include with the volume request for local
+     *              playback
+     */
+    public void adjustVolume(@VolumeDirection int direction, @VolumeFlags int flags) {
+        mImpl.adjustVolume(direction, flags);
+    }
+
+    /**
+     * Get an intent for launching UI associated with this session if one exists.
+     *
+     * @return A {@link PendingIntent} to launch UI or null.
+     */
+    public @Nullable PendingIntent getSessionActivity() {
+        return mImpl.getSessionActivity();
+    }
+
+    /**
+     * Get the lastly cached player state from
+     * {@link ControllerCallback#onPlayerStateChanged(MediaController2, int)}.
+     *
+     * @return player state
+     */
+    public int getPlayerState() {
+        return mImpl.getPlayerState();
+    }
+
+    /**
+     * Gets the duration of the current media item, or {@link MediaPlayerInterface#UNKNOWN_TIME} if
+     * unknown.
+     * @return the duration in ms, or {@link MediaPlayerInterface#UNKNOWN_TIME}.
+     */
+    public long getDuration() {
+        return mImpl.getDuration();
+    }
+
+    /**
+     * Gets the current playback position.
+     * <p>
+     * This returns the calculated value of the position, based on the difference between the
+     * update time and current time.
+     *
+     * @return position
+     */
+    public long getCurrentPosition() {
+        return mImpl.getCurrentPosition();
+    }
+
+    /**
+     * Get the lastly cached playback speed from
+     * {@link ControllerCallback#onPlaybackSpeedChanged(MediaController2, float)}.
+     *
+     * @return speed the lastly cached playback speed, or 0.0f if unknown.
+     */
+    public float getPlaybackSpeed() {
+        return mImpl.getPlaybackSpeed();
+    }
+
+    /**
+     * Set the playback speed.
+     */
+    public void setPlaybackSpeed(float speed) {
+        mImpl.setPlaybackSpeed(speed);
+    }
+
+    /**
+     * Gets the current buffering state of the player.
+     * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+     * buffered.
+     * @return the buffering state.
+     */
+    public @MediaPlayerInterface.BuffState int getBufferingState() {
+        return mImpl.getBufferingState();
+    }
+
+    /**
+     * Gets the lastly cached buffered position from the session when
+     * {@link ControllerCallback#onBufferingStateChanged(MediaController2, MediaItem2, int)} is
+     * called.
+     *
+     * @return buffering position in millis, or {@link MediaPlayerInterface#UNKNOWN_TIME} if
+     * unknown.
+     */
+    public long getBufferedPosition() {
+        return mImpl.getBufferedPosition();
+    }
+
+    /**
+     * Get the current playback info for this session.
+     *
+     * @return The current playback info or null.
+     */
+    public @Nullable PlaybackInfo getPlaybackInfo() {
+        return mImpl.getPlaybackInfo();
+    }
+
+    /**
+     * Rate the media. This will cause the rating to be set for the current user.
+     * The rating style must follow the user rating style from the session.
+     * You can get the rating style from the session through the
+     * {@link MediaMetadata2#getRating(String)} with the key
+     * {@link MediaMetadata2#METADATA_KEY_USER_RATING}.
+     * <p>
+     * If the user rating was {@code null}, the media item does not accept setting user rating.
+     *
+     * @param mediaId The id of the media
+     * @param rating The rating to set
+     */
+    public void setRating(@NonNull String mediaId, @NonNull Rating2 rating) {
+        mImpl.setRating(mediaId, rating);
+    }
+
+    /**
+     * Send custom command to the session
+     *
+     * @param command custom command
+     * @param args optional argument
+     * @param cb optional result receiver
+     */
+    public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args,
+            @Nullable ResultReceiver cb) {
+        mImpl.sendCustomCommand(command, args, cb);
+    }
+
+    /**
+     * Returns the cached playlist from {@link ControllerCallback#onPlaylistChanged}.
+     * <p>
+     * This list may differ with the list that was specified with
+     * {@link #setPlaylist(List, MediaMetadata2)} depending on the {@link MediaPlaylistAgent}
+     * 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 playlist hasn't set nor controller doesn't have
+     *      enough permission.
+     * @see SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST
+     */
+    public @Nullable List<MediaItem2> getPlaylist() {
+        return mImpl.getPlaylist();
+    }
+
+    /**
+     * Sets the playlist.
+     * <p>
+     * Even when the playlist is successfully set, use the playlist returned from
+     * {@link #getPlaylist()} for playlist APIs such as {@link #skipToPlaylistItem(MediaItem2)}.
+     * Otherwise the session in the remote process can't distinguish between media items.
+     *
+     * @param list playlist
+     * @param metadata metadata of the playlist
+     * @see #getPlaylist()
+     * @see ControllerCallback#onPlaylistChanged
+     */
+    public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+        mImpl.setPlaylist(list, metadata);
+    }
+
+    /**
+     * Updates the playlist metadata
+     *
+     * @param metadata metadata of the playlist
+     */
+    public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+        mImpl.updatePlaylistMetadata(metadata);
+    }
+
+    /**
+     * Gets the lastly cached playlist playlist metadata either from
+     * {@link ControllerCallback#onPlaylistMetadataChanged or
+     * {@link ControllerCallback#onPlaylistChanged}.
+     *
+     * @return metadata metadata of the playlist, or null if none is set
+     */
+    public @Nullable MediaMetadata2 getPlaylistMetadata() {
+        return mImpl.getPlaylistMetadata();
+    }
+
+    /**
+     * Adds the media item to the playlist at position index. Index equals or greater than
+     * 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,
+     * the current index of the playlist will be incremented correspondingly.
+     *
+     * @param index the index you want to add
+     * @param item the media item you want to add
+     */
+    public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+        mImpl.addPlaylistItem(index, item);
+    }
+
+    /**
+     * Removes the media item at index in the playlist.
+     *<p>
+     * If the item is the currently playing item of the playlist, current playback
+     * will be stopped and playback moves to next source in the list.
+     *
+     * @param item the media item you want to add
+     */
+    public void removePlaylistItem(@NonNull MediaItem2 item) {
+        mImpl.removePlaylistItem(item);
+    }
+
+    /**
+     * Replace the media item at index in the playlist. This can be also used to update metadata of
+     * an item.
+     *
+     * @param index the index of the item to replace
+     * @param item the new item
+     */
+    public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+        mImpl.replacePlaylistItem(index, item);
+    }
+
+    /**
+     * Get the lastly cached current item from
+     * {@link ControllerCallback#onCurrentMediaItemChanged(MediaController2, MediaItem2)}.
+     *
+     * @return the currently playing item, or null if unknown.
+     */
+    public MediaItem2 getCurrentMediaItem() {
+        return mImpl.getCurrentMediaItem();
+    }
+
+    /**
+     * Skips to the previous item in the playlist.
+     * <p>
+     * This calls {@link MediaPlaylistAgent#skipToPreviousItem()}.
+     */
+    public void skipToPreviousItem() {
+        mImpl.skipToPreviousItem();
+    }
+
+    /**
+     * Skips to the next item in the playlist.
+     * <p>
+     * This calls {@link MediaPlaylistAgent#skipToNextItem()}.
+     */
+    public void skipToNextItem() {
+        mImpl.skipToNextItem();
+    }
+
+    /**
+     * Skips to the item in the playlist.
+     * <p>
+     * This calls {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
+     *
+     * @param item The item in the playlist you want to play
+     */
+    public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+        mImpl.skipToPlaylistItem(item);
+    }
+
+    /**
+     * Gets the cached repeat mode from the {@link ControllerCallback#onRepeatModeChanged}.
+     *
+     * @return repeat mode
+     * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+     * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+     */
+    public @RepeatMode int getRepeatMode() {
+        return mImpl.getRepeatMode();
+    }
+
+    /**
+     * Sets the repeat mode.
+     *
+     * @param repeatMode repeat mode
+     * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+     * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+     */
+    public void setRepeatMode(@RepeatMode int repeatMode) {
+        mImpl.setRepeatMode(repeatMode);
+    }
+
+    /**
+     * Gets the cached shuffle mode from the {@link ControllerCallback#onShuffleModeChanged}.
+     *
+     * @return The shuffle mode
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+     */
+    public @ShuffleMode int getShuffleMode() {
+        return mImpl.getShuffleMode();
+    }
+
+    /**
+     * Sets the shuffle mode.
+     *
+     * @param shuffleMode The shuffle mode
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+     */
+    public void setShuffleMode(@ShuffleMode int shuffleMode) {
+        mImpl.setShuffleMode(shuffleMode);
+    }
+
+    /**
+     * Queries for information about the routes currently known.
+     */
+    public void subscribeRoutesInfo() {
+        mImpl.subscribeRoutesInfo();
+    }
+
+    /**
+     * Unsubscribes for changes to the routes.
+     * <p>
+     * The {@link ControllerCallback#onRoutesInfoChanged callback} will no longer be invoked for
+     * the routes once this method returns.
+     * </p>
+     */
+    public void unsubscribeRoutesInfo() {
+        mImpl.unsubscribeRoutesInfo();
+    }
+
+    /**
+     * Selects the specified route.
+     *
+     * @param route The route to select.
+     */
+    public void selectRoute(@NonNull Bundle route) {
+        mImpl.selectRoute(route);
+    }
+
+    @NonNull Context getContext() {
+        return mImpl.getContext();
+    }
+
+    @NonNull ControllerCallback getCallback() {
+        return mImpl.getCallback();
+    }
+
+    @NonNull Executor getCallbackExecutor() {
+        return mImpl.getCallbackExecutor();
+    }
+
+    @Nullable MediaBrowserCompat getBrowserCompat() {
+        return mImpl.getBrowserCompat();
+    }
+
+    /**
+     * Sets the time diff forcefully when calculating current position.
+     * @param timeDiff {@code null} for reset.
+     */
+    @VisibleForTesting
+    void setTimeDiff(Long timeDiff) {
+        mTimeDiff = timeDiff;
+    }
+
+    interface SupportLibraryImpl extends AutoCloseable {
+        void setInstance(MediaController2 controller);
+        SessionToken2 getSessionToken();
+        boolean isConnected();
+        void play();
+        void pause();
+        void reset();
+        void prepare();
+        void fastForward();
+        void rewind();
+        void seekTo(long pos);
+        void skipForward();
+        void skipBackward();
+        void playFromMediaId(@NonNull String mediaId, @Nullable Bundle extras);
+        void playFromSearch(@NonNull String query, @Nullable Bundle extras);
+        void playFromUri(@NonNull Uri uri, @Nullable Bundle extras);
+        void prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras);
+        void prepareFromSearch(@NonNull String query, @Nullable Bundle extras);
+        void prepareFromUri(@NonNull Uri uri, @Nullable Bundle extras);
+        void setVolumeTo(int value, @VolumeFlags int flags);
+        void adjustVolume(@VolumeDirection int direction, @VolumeFlags int flags);
+        @Nullable PendingIntent getSessionActivity();
+        int getPlayerState();
+        long getDuration();
+        long getCurrentPosition();
+        float getPlaybackSpeed();
+        void setPlaybackSpeed(float speed);
+        @MediaPlayerInterface.BuffState int getBufferingState();
+        long getBufferedPosition();
+        @Nullable PlaybackInfo getPlaybackInfo();
+        void setRating(@NonNull String mediaId, @NonNull Rating2 rating);
+        void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args,
+                @Nullable ResultReceiver cb);
+        @Nullable List<MediaItem2> getPlaylist();
+        void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata);
+        void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata);
+        @Nullable MediaMetadata2 getPlaylistMetadata();
+        void addPlaylistItem(int index, @NonNull MediaItem2 item);
+        void removePlaylistItem(@NonNull MediaItem2 item);
+        void replacePlaylistItem(int index, @NonNull MediaItem2 item);
+        MediaItem2 getCurrentMediaItem();
+        void skipToPreviousItem();
+        void skipToNextItem();
+        void skipToPlaylistItem(@NonNull MediaItem2 item);
+        @RepeatMode int getRepeatMode();
+        void setRepeatMode(@RepeatMode int repeatMode);
+        @ShuffleMode int getShuffleMode();
+        void setShuffleMode(@ShuffleMode int shuffleMode);
+        void subscribeRoutesInfo();
+        void unsubscribeRoutesInfo();
+        void selectRoute(@NonNull Bundle route);
+
+        // For MediaBrowser2
+        @NonNull Context getContext();
+        @NonNull ControllerCallback getCallback();
+        @NonNull Executor getCallbackExecutor();
+        @Nullable MediaBrowserCompat getBrowserCompat();
+    }
+
     /**
      * Interface for listening to change in activeness of the {@link MediaSession2}.  It's
      * active if and only if it has set a player.
@@ -273,7 +835,7 @@
          * @param state the new buffering state.
          */
         public void onBufferingStateChanged(@NonNull MediaController2 controller,
-                @NonNull MediaItem2 item, @MediaPlayerBase.BuffState int state) { }
+                @NonNull MediaItem2 item, @MediaPlayerInterface.BuffState int state) { }
 
         /**
          * Called to indicate that seeking is completed.
@@ -304,7 +866,7 @@
          * @see #onBufferingStateChanged(MediaController2, MediaItem2, int)
          */
         public void onCurrentMediaItemChanged(@NonNull MediaController2 controller,
-                @NonNull MediaItem2 item) { }
+                @Nullable MediaItem2 item) { }
 
         /**
          * Called when a playlist is changed.
@@ -461,8 +1023,7 @@
             bundle.putInt(KEY_MAX_VOLUME, mMaxVolume);
             bundle.putInt(KEY_CURRENT_VOLUME, mCurrentVolume);
             if (mAudioAttrsCompat != null) {
-                bundle.putParcelable(KEY_AUDIO_ATTRIBUTES,
-                        MediaUtils2.toAudioAttributesBundle(mAudioAttrsCompat));
+                bundle.putBundle(KEY_AUDIO_ATTRIBUTES, mAudioAttrsCompat.toBundle());
             }
             return bundle;
         }
@@ -480,1291 +1041,10 @@
             final int volumeControl = bundle.getInt(KEY_CONTROL_TYPE);
             final int maxVolume = bundle.getInt(KEY_MAX_VOLUME);
             final int currentVolume = bundle.getInt(KEY_CURRENT_VOLUME);
-            final AudioAttributesCompat attrs = MediaUtils2.fromAudioAttributesBundle(
+            final AudioAttributesCompat attrs = AudioAttributesCompat.fromBundle(
                     bundle.getBundle(KEY_AUDIO_ATTRIBUTES));
             return createPlaybackInfo(volumeType, attrs, volumeControl, maxVolume,
                     currentVolume);
         }
     }
-
-    private final class ControllerCompatCallback extends MediaControllerCompat.Callback {
-        @Override
-        public void onSessionReady() {
-            sendCommand(CONTROLLER_COMMAND_CONNECT, new ResultReceiver(mHandler) {
-                @Override
-                protected void onReceiveResult(int resultCode, Bundle resultData) {
-                    if (!mHandlerThread.isAlive()) {
-                        return;
-                    }
-                    switch (resultCode) {
-                        case CONNECT_RESULT_CONNECTED:
-                            onConnectedNotLocked(resultData);
-                            break;
-                        case CONNECT_RESULT_DISCONNECTED:
-                            mCallback.onDisconnected(MediaController2.this);
-                            close();
-                            break;
-                    }
-                }
-            });
-        }
-
-        @Override
-        public void onSessionDestroyed() {
-            close();
-        }
-
-        @Override
-        public void onPlaybackStateChanged(PlaybackStateCompat state) {
-            synchronized (mLock) {
-                mPlaybackStateCompat = state;
-            }
-        }
-
-        @Override
-        public void onMetadataChanged(MediaMetadataCompat metadata) {
-            synchronized (mLock) {
-                mMediaMetadataCompat = metadata;
-            }
-        }
-
-        @Override
-        public void onSessionEvent(String event, Bundle extras) {
-            switch (event) {
-                case SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED: {
-                    SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
-                            extras.getBundle(ARGUMENT_ALLOWED_COMMANDS));
-                    synchronized (mLock) {
-                        mAllowedCommands = allowedCommands;
-                    }
-                    mCallback.onAllowedCommandsChanged(MediaController2.this, allowedCommands);
-                    break;
-                }
-                case SESSION_EVENT_ON_PLAYER_STATE_CHANGED: {
-                    int playerState = extras.getInt(ARGUMENT_PLAYER_STATE);
-                    synchronized (mLock) {
-                        mPlayerState = playerState;
-                    }
-                    mCallback.onPlayerStateChanged(MediaController2.this, playerState);
-                    break;
-                }
-                case SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED: {
-                    MediaItem2 item = MediaItem2.fromBundle(extras.getBundle(ARGUMENT_MEDIA_ITEM));
-                    if (item == null) {
-                        return;
-                    }
-                    synchronized (mLock) {
-                        mCurrentMediaItem = item;
-                    }
-                    mCallback.onCurrentMediaItemChanged(MediaController2.this, item);
-                    break;
-                }
-                case SESSION_EVENT_ON_ERROR: {
-                    int errorCode = extras.getInt(ARGUMENT_ERROR_CODE);
-                    Bundle errorExtras = extras.getBundle(ARGUMENT_EXTRAS);
-                    mCallback.onError(MediaController2.this, errorCode, errorExtras);
-                    break;
-                }
-                case SESSION_EVENT_ON_ROUTES_INFO_CHANGED: {
-                    List<Bundle> routes = MediaUtils2.toBundleList(
-                            extras.getParcelableArray(ARGUMENT_ROUTE_BUNDLE));
-                    mCallback.onRoutesInfoChanged(MediaController2.this, routes);
-                    break;
-                }
-                case SESSION_EVENT_ON_PLAYLIST_CHANGED: {
-                    MediaMetadata2 playlistMetadata = MediaMetadata2.fromBundle(
-                            extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
-                    List<MediaItem2> playlist = MediaUtils2.fromMediaItem2ParcelableArray(
-                            extras.getParcelableArray(ARGUMENT_PLAYLIST));
-                    synchronized (mLock) {
-                        mPlaylist = playlist;
-                        mPlaylistMetadata = playlistMetadata;
-                    }
-                    mCallback.onPlaylistChanged(MediaController2.this, playlist, playlistMetadata);
-                    break;
-                }
-                case SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED: {
-                    MediaMetadata2 playlistMetadata = MediaMetadata2.fromBundle(
-                            extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
-                    synchronized (mLock) {
-                        mPlaylistMetadata = playlistMetadata;
-                    }
-                    mCallback.onPlaylistMetadataChanged(MediaController2.this, playlistMetadata);
-                    break;
-                }
-                case SESSION_EVENT_ON_REPEAT_MODE_CHANGED: {
-                    int repeatMode = extras.getInt(ARGUMENT_REPEAT_MODE);
-                    synchronized (mLock) {
-                        mRepeatMode = repeatMode;
-                    }
-                    mCallback.onRepeatModeChanged(MediaController2.this, repeatMode);
-                    break;
-                }
-                case SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED: {
-                    int shuffleMode = extras.getInt(ARGUMENT_SHUFFLE_MODE);
-                    synchronized (mLock) {
-                        mShuffleMode = shuffleMode;
-                    }
-                    mCallback.onShuffleModeChanged(MediaController2.this, shuffleMode);
-                    break;
-                }
-                case SESSION_EVENT_SEND_CUSTOM_COMMAND: {
-                    Bundle commandBundle = extras.getBundle(ARGUMENT_CUSTOM_COMMAND);
-                    if (commandBundle == null) {
-                        return;
-                    }
-                    SessionCommand2 command = SessionCommand2.fromBundle(commandBundle);
-                    Bundle args = extras.getBundle(ARGUMENT_ARGUMENTS);
-                    ResultReceiver receiver = extras.getParcelable(ARGUMENT_RESULT_RECEIVER);
-                    mCallback.onCustomCommand(MediaController2.this, command, args, receiver);
-                    break;
-                }
-                case SESSION_EVENT_SET_CUSTOM_LAYOUT: {
-                    List<CommandButton> layout = MediaUtils2.fromCommandButtonParcelableArray(
-                            extras.getParcelableArray(ARGUMENT_COMMAND_BUTTONS));
-                    if (layout == null) {
-                        return;
-                    }
-                    mCallback.onCustomLayoutChanged(MediaController2.this, layout);
-                    break;
-                }
-                case SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED: {
-                    PlaybackInfo info = PlaybackInfo.fromBundle(
-                            extras.getBundle(ARGUMENT_PLAYBACK_INFO));
-                    if (info == null) {
-                        return;
-                    }
-                    synchronized (mLock) {
-                        mPlaybackInfo = info;
-                    }
-                    mCallback.onPlaybackInfoChanged(MediaController2.this, info);
-                    break;
-                }
-                case SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED: {
-                    PlaybackStateCompat state =
-                            extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT);
-                    if (state == null) {
-                        return;
-                    }
-                    synchronized (mLock) {
-                        mPlaybackStateCompat = state;
-                    }
-                    mCallback.onPlaybackSpeedChanged(
-                            MediaController2.this, state.getPlaybackSpeed());
-                    break;
-                }
-                case SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED: {
-                    MediaItem2 item = MediaItem2.fromBundle(extras.getBundle(ARGUMENT_MEDIA_ITEM));
-                    int bufferingState = extras.getInt(ARGUMENT_BUFFERING_STATE);
-                    if (item == null) {
-                        return;
-                    }
-                    synchronized (mLock) {
-                        mBufferingState = bufferingState;
-                    }
-                    mCallback.onBufferingStateChanged(MediaController2.this, item, bufferingState);
-                    break;
-                }
-            }
-        }
-    }
-
-    private static final String TAG = "MediaController2";
-    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.
-    static final Bundle sDefaultRootExtras = new Bundle();
-    static {
-        sDefaultRootExtras.putBoolean(MediaConstants2.ROOT_EXTRA_DEFAULT, true);
-    }
-
-    private final Context mContext;
-    private final Object mLock = new Object();
-
-    private final SessionToken2 mToken;
-    private final ControllerCallback mCallback;
-    private final Executor mCallbackExecutor;
-    private final IBinder.DeathRecipient mDeathRecipient;
-
-    private final HandlerThread mHandlerThread;
-    private final Handler mHandler;
-
-    @GuardedBy("mLock")
-    private MediaBrowserCompat mBrowserCompat;
-    @GuardedBy("mLock")
-    private boolean mIsReleased;
-    @GuardedBy("mLock")
-    private List<MediaItem2> mPlaylist;
-    @GuardedBy("mLock")
-    private MediaMetadata2 mPlaylistMetadata;
-    @GuardedBy("mLock")
-    private @RepeatMode int mRepeatMode;
-    @GuardedBy("mLock")
-    private @ShuffleMode int mShuffleMode;
-    @GuardedBy("mLock")
-    private int mPlayerState;
-    @GuardedBy("mLock")
-    private MediaItem2 mCurrentMediaItem;
-    @GuardedBy("mLock")
-    private int mBufferingState;
-    @GuardedBy("mLock")
-    private PlaybackInfo mPlaybackInfo;
-    @GuardedBy("mLock")
-    private SessionCommandGroup2 mAllowedCommands;
-
-    // Media 1.0 variables
-    @GuardedBy("mLock")
-    private MediaControllerCompat mControllerCompat;
-    @GuardedBy("mLock")
-    private ControllerCompatCallback mControllerCompatCallback;
-    @GuardedBy("mLock")
-    private PlaybackStateCompat mPlaybackStateCompat;
-    @GuardedBy("mLock")
-    private MediaMetadataCompat mMediaMetadataCompat;
-
-    // Assignment should be used with the lock hold, but should be used without a lock to prevent
-    // potential deadlock.
-    @GuardedBy("mLock")
-    private volatile boolean mConnected;
-
-    /**
-     * Create a {@link MediaController2} from the {@link SessionToken2}.
-     * This connects to the session and may wake up the service if it's not available.
-     *
-     * @param context Context
-     * @param token token to connect to
-     * @param executor executor to run callbacks on.
-     * @param callback controller callback to receive changes in
-     */
-    public MediaController2(@NonNull Context context, @NonNull SessionToken2 token,
-            @NonNull Executor executor, @NonNull ControllerCallback callback) {
-        super();
-        if (context == null) {
-            throw new IllegalArgumentException("context shouldn't be null");
-        }
-        if (token == null) {
-            throw new IllegalArgumentException("token shouldn't be null");
-        }
-        if (callback == null) {
-            throw new IllegalArgumentException("callback shouldn't be null");
-        }
-        if (executor == null) {
-            throw new IllegalArgumentException("executor shouldn't be null");
-        }
-        mContext = context;
-        mHandlerThread = new HandlerThread("MediaController2_Thread");
-        mHandlerThread.start();
-        mHandler = new Handler(mHandlerThread.getLooper());
-        mToken = token;
-        mCallback = callback;
-        mCallbackExecutor = executor;
-        mDeathRecipient = new IBinder.DeathRecipient() {
-            @Override
-            public void binderDied() {
-                MediaController2.this.close();
-            }
-        };
-
-        initialize();
-    }
-
-    /**
-     * Release this object, and disconnect from the session. After this, callbacks wouldn't be
-     * received.
-     */
-    @Override
-    public void close() {
-        if (DEBUG) {
-            //Log.d(TAG, "release from " + mToken, new IllegalStateException());
-        }
-        synchronized (mLock) {
-            if (mIsReleased) {
-                // Prevent re-enterance from the ControllerCallback.onDisconnected()
-                return;
-            }
-            mHandler.removeCallbacksAndMessages(null);
-            mHandlerThread.quitSafely();
-
-            mIsReleased = true;
-
-            // Send command before the unregister callback to use mIControllerCallback in the
-            // callback.
-            sendCommand(CONTROLLER_COMMAND_DISCONNECT);
-            if (mControllerCompat != null) {
-                mControllerCompat.unregisterCallback(mControllerCompatCallback);
-            }
-            if (mBrowserCompat != null) {
-                mBrowserCompat.disconnect();
-                mBrowserCompat = null;
-            }
-            if (mControllerCompat != null) {
-                mControllerCompat.unregisterCallback(mControllerCompatCallback);
-                mControllerCompat = null;
-            }
-            mConnected = false;
-        }
-        mCallbackExecutor.execute(new Runnable() {
-            @Override
-            public void run() {
-                mCallback.onDisconnected(MediaController2.this);
-            }
-        });
-    }
-
-    /**
-     * @return token
-     */
-    public @NonNull SessionToken2 getSessionToken() {
-        return mToken;
-    }
-
-    /**
-     * Returns whether this class is connected to active {@link MediaSession2} or not.
-     */
-    public boolean isConnected() {
-        synchronized (mLock) {
-            return mConnected;
-        }
-    }
-
-    /**
-     * Requests that the player starts or resumes playback.
-     */
-    public void play() {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return;
-            }
-            sendCommand(COMMAND_CODE_PLAYBACK_PLAY);
-        }
-    }
-
-    /**
-     * Requests that the player pauses playback.
-     */
-    public void pause() {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return;
-            }
-            sendCommand(COMMAND_CODE_PLAYBACK_PAUSE);
-        }
-    }
-
-    /**
-     * Requests that the player be reset to its uninitialized state.
-     */
-    public void reset() {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return;
-            }
-            sendCommand(COMMAND_CODE_PLAYBACK_RESET);
-        }
-    }
-
-    /**
-     * Request that the player prepare its playback. In other words, other sessions can continue
-     * to play during the preparation of this session. This method can be used to speed up the
-     * start of the playback. Once the preparation is done, the session will change its playback
-     * state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called
-     * to start playback.
-     */
-    public void prepare() {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return;
-            }
-            sendCommand(COMMAND_CODE_PLAYBACK_PREPARE);
-        }
-    }
-
-    /**
-     * Start fast forwarding. If playback is already fast forwarding this
-     * may increase the rate.
-     */
-    public void fastForward() {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return;
-            }
-            sendCommand(COMMAND_CODE_SESSION_FAST_FORWARD);
-        }
-    }
-
-    /**
-     * Start rewinding. If playback is already rewinding this may increase
-     * the rate.
-     */
-    public void rewind() {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return;
-            }
-            sendCommand(COMMAND_CODE_SESSION_REWIND);
-        }
-    }
-
-    /**
-     * Move to a new location in the media stream.
-     *
-     * @param pos Position to move to, in milliseconds.
-     */
-    public void seekTo(long pos) {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return;
-            }
-            Bundle args = new Bundle();
-            args.putLong(ARGUMENT_SEEK_POSITION, pos);
-            sendCommand(COMMAND_CODE_PLAYBACK_SEEK_TO, args);
-        }
-    }
-
-    /**
-     * @hide
-     */
-    @RestrictTo(LIBRARY_GROUP)
-    public void skipForward() {
-        // To match with KEYCODE_MEDIA_SKIP_FORWARD
-    }
-
-    /**
-     * @hide
-     */
-    @RestrictTo(LIBRARY_GROUP)
-    public void skipBackward() {
-        // To match with KEYCODE_MEDIA_SKIP_BACKWARD
-    }
-
-    /**
-     * Request that the player start playback for a specific media id.
-     *
-     * @param mediaId The id of the requested media.
-     * @param extras Optional extras that can include extra information about the media item
-     *               to be played.
-     */
-    public void playFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return;
-            }
-            Bundle args = new Bundle();
-            args.putString(ARGUMENT_MEDIA_ID, mediaId);
-            args.putBundle(ARGUMENT_EXTRAS, extras);
-            sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID, args);
-        }
-    }
-
-    /**
-     * Request that the player start playback for a specific search query.
-     *
-     * @param query The search query. Should not be an empty string.
-     * @param extras Optional extras that can include extra information about the query.
-     */
-    public void playFromSearch(@NonNull String query, @Nullable Bundle extras) {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return;
-            }
-            Bundle args = new Bundle();
-            args.putString(ARGUMENT_QUERY, query);
-            args.putBundle(ARGUMENT_EXTRAS, extras);
-            sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_SEARCH, args);
-        }
-    }
-
-    /**
-     * Request that the player start playback for a specific {@link Uri}.
-     *
-     * @param uri The URI of the requested media.
-     * @param extras Optional extras that can include extra information about the media item
-     *               to be played.
-     */
-    public void playFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return;
-            }
-            Bundle args = new Bundle();
-            args.putParcelable(ARGUMENT_URI, uri);
-            args.putBundle(ARGUMENT_EXTRAS, extras);
-            sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_URI, args);
-        }
-    }
-
-    /**
-     * Request that the player prepare playback for a specific media id. In other words, other
-     * sessions can continue to play during the preparation of this session. This method can be
-     * used to speed up the start of the playback. Once the preparation is done, the session
-     * will change its playback state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards,
-     * {@link #play} can be called to start playback. If the preparation is not needed,
-     * {@link #playFromMediaId} can be directly called without this method.
-     *
-     * @param mediaId The id of the requested media.
-     * @param extras Optional extras that can include extra information about the media item
-     *               to be prepared.
-     */
-    public void prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return;
-            }
-            Bundle args = new Bundle();
-            args.putString(ARGUMENT_MEDIA_ID, mediaId);
-            args.putBundle(ARGUMENT_EXTRAS, extras);
-            sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID, args);
-        }
-    }
-
-    /**
-     * Request that the player prepare playback for a specific search query.
-     * In other words, other sessions can continue to play during the preparation of this session.
-     * This method can be used to speed up the start of the playback.
-     * Once the preparation is done, the session will change its playback state to
-     * {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards,
-     * {@link #play} can be called to start playback. If the preparation is not needed,
-     * {@link #playFromSearch} can be directly called without this method.
-     *
-     * @param query The search query. Should not be an empty string.
-     * @param extras Optional extras that can include extra information about the query.
-     */
-    public void prepareFromSearch(@NonNull String query, @Nullable Bundle extras) {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return;
-            }
-            Bundle args = new Bundle();
-            args.putString(ARGUMENT_QUERY, query);
-            args.putBundle(ARGUMENT_EXTRAS, extras);
-            sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH, args);
-        }
-    }
-
-    /**
-     * Request that the player prepare playback for a specific {@link Uri}. In other words,
-     * other sessions can continue to play during the preparation of this session. This method
-     * can be used to speed up the start of the playback. Once the preparation is done, the
-     * session will change its playback state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}.
-     * Afterwards, {@link #play} can be called to start playback. If the preparation is not needed,
-     * {@link #playFromUri} can be directly called without this method.
-     *
-     * @param uri The URI of the requested media.
-     * @param extras Optional extras that can include extra information about the media item
-     *               to be prepared.
-     */
-    public void prepareFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return;
-            }
-            Bundle args = new Bundle();
-            args.putParcelable(ARGUMENT_URI, uri);
-            args.putBundle(ARGUMENT_EXTRAS, extras);
-            sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_URI, args);
-        }
-    }
-
-    /**
-     * Set the volume of the output this session is playing on. The command will be ignored if it
-     * does not support {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}.
-     * <p>
-     * If the session is local playback, this changes the device's volume with the stream that
-     * session's player is using. Flags will be specified for the {@link AudioManager}.
-     * <p>
-     * If the session is remote player (i.e. session has set volume provider), its volume provider
-     * will receive this request instead.
-     *
-     * @see #getPlaybackInfo()
-     * @param value The value to set it to, between 0 and the reported max.
-     * @param flags flags from {@link AudioManager} to include with the volume request for local
-     *              playback
-     */
-    public void setVolumeTo(int value, @VolumeFlags int flags) {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return;
-            }
-            Bundle args = new Bundle();
-            args.putInt(ARGUMENT_VOLUME, value);
-            args.putInt(ARGUMENT_VOLUME_FLAGS, flags);
-            sendCommand(COMMAND_CODE_VOLUME_SET_VOLUME, args);
-        }
-    }
-
-    /**
-     * 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}.
-     * <p>
-     * If the session is local playback, this changes the device's volume with the stream that
-     * session's player is using. Flags will be specified for the {@link AudioManager}.
-     * <p>
-     * If the session is remote player (i.e. session has set volume provider), its volume provider
-     * will receive this request instead.
-     *
-     * @see #getPlaybackInfo()
-     * @param direction The direction to adjust the volume in.
-     * @param flags flags from {@link AudioManager} to include with the volume request for local
-     *              playback
-     */
-    public void adjustVolume(@VolumeDirection int direction, @VolumeFlags int flags) {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return;
-            }
-            Bundle args = new Bundle();
-            args.putInt(ARGUMENT_VOLUME_DIRECTION, direction);
-            args.putInt(ARGUMENT_VOLUME_FLAGS, flags);
-            sendCommand(COMMAND_CODE_VOLUME_ADJUST_VOLUME, args);
-        }
-    }
-
-    /**
-     * Get an intent for launching UI associated with this session if one exists.
-     *
-     * @return A {@link PendingIntent} to launch UI or null.
-     */
-    public @Nullable PendingIntent getSessionActivity() {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return null;
-            }
-            return mControllerCompat.getSessionActivity();
-        }
-    }
-
-    /**
-     * Get the lastly cached player state from
-     * {@link ControllerCallback#onPlayerStateChanged(MediaController2, int)}.
-     *
-     * @return player state
-     */
-    public int getPlayerState() {
-        synchronized (mLock) {
-            return mPlayerState;
-        }
-    }
-
-    /**
-     * Gets the duration of the current media item, or {@link MediaPlayerBase#UNKNOWN_TIME} if
-     * unknown.
-     * @return the duration in ms, or {@link MediaPlayerBase#UNKNOWN_TIME}.
-     */
-    public long getDuration() {
-        synchronized (mLock) {
-            if (mMediaMetadataCompat != null
-                    && mMediaMetadataCompat.containsKey(METADATA_KEY_DURATION)) {
-                return mMediaMetadataCompat.getLong(METADATA_KEY_DURATION);
-            }
-        }
-        return MediaPlayerBase.UNKNOWN_TIME;
-    }
-
-    /**
-     * Gets the current playback position.
-     * <p>
-     * This returns the calculated value of the position, based on the difference between the
-     * update time and current time.
-     *
-     * @return position
-     */
-    public long getCurrentPosition() {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return UNKNOWN_TIME;
-            }
-            if (mPlaybackStateCompat != null) {
-                long timeDiff = SystemClock.elapsedRealtime()
-                        - mPlaybackStateCompat.getLastPositionUpdateTime();
-                long expectedPosition = mPlaybackStateCompat.getPosition()
-                        + (long) (mPlaybackStateCompat.getPlaybackSpeed() * timeDiff);
-                return Math.max(0, expectedPosition);
-            }
-            return UNKNOWN_TIME;
-        }
-    }
-
-    /**
-     * Get the lastly cached playback speed from
-     * {@link ControllerCallback#onPlaybackSpeedChanged(MediaController2, float)}.
-     *
-     * @return speed the lastly cached playback speed, or 0.0f if unknown.
-     */
-    public float getPlaybackSpeed() {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return 0f;
-            }
-            return (mPlaybackStateCompat == null) ? 0f : mPlaybackStateCompat.getPlaybackSpeed();
-        }
-    }
-
-    /**
-     * Set the playback speed.
-     */
-    public void setPlaybackSpeed(float speed) {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return;
-            }
-            Bundle args = new Bundle();
-            args.putFloat(ARGUMENT_PLAYBACK_SPEED, speed);
-            sendCommand(COMMAND_CODE_PLAYBACK_SET_SPEED, args);
-        }
-    }
-
-    /**
-     * Gets the current buffering state of the player.
-     * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
-     * buffered.
-     * @return the buffering state.
-     */
-    public @MediaPlayerBase.BuffState int getBufferingState() {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return BUFFERING_STATE_UNKNOWN;
-            }
-            return mBufferingState;
-        }
-    }
-
-    /**
-     * Gets the lastly cached buffered position from the session when
-     * {@link ControllerCallback#onBufferingStateChanged(MediaController2, MediaItem2, int)} is
-     * called.
-     *
-     * @return buffering position in millis, or {@link MediaPlayerBase#UNKNOWN_TIME} if unknown.
-     */
-    public long getBufferedPosition() {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return UNKNOWN_TIME;
-            }
-            return (mPlaybackStateCompat == null) ? UNKNOWN_TIME
-                    : mPlaybackStateCompat.getBufferedPosition();
-        }
-    }
-
-    /**
-     * Get the current playback info for this session.
-     *
-     * @return The current playback info or null.
-     */
-    public @Nullable PlaybackInfo getPlaybackInfo() {
-        synchronized (mLock) {
-            return mPlaybackInfo;
-        }
-    }
-
-    /**
-     * Rate the media. This will cause the rating to be set for the current user.
-     * The rating style must follow the user rating style from the session.
-     * You can get the rating style from the session through the
-     * {@link MediaMetadata2#getRating(String)} with the key
-     * {@link MediaMetadata2#METADATA_KEY_USER_RATING}.
-     * <p>
-     * If the user rating was {@code null}, the media item does not accept setting user rating.
-     *
-     * @param mediaId The id of the media
-     * @param rating The rating to set
-     */
-    public void setRating(@NonNull String mediaId, @NonNull Rating2 rating) {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return;
-            }
-            Bundle args = new Bundle();
-            args.putString(ARGUMENT_MEDIA_ID, mediaId);
-            args.putBundle(ARGUMENT_RATING, rating.toBundle());
-            sendCommand(COMMAND_CODE_SESSION_SET_RATING, args);
-        }
-    }
-
-    /**
-     * Send custom command to the session
-     *
-     * @param command custom command
-     * @param args optional argument
-     * @param cb optional result receiver
-     */
-    public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args,
-            @Nullable ResultReceiver cb) {
-        synchronized (mLock) {
-            if (!mConnected) {
-                Log.w(TAG, "Session isn't active", new IllegalStateException());
-                return;
-            }
-            Bundle bundle = new Bundle();
-            bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
-            bundle.putBundle(ARGUMENT_ARGUMENTS, args);
-            sendCommand(CONTROLLER_COMMAND_BY_CUSTOM_COMMAND, bundle, cb);
-        }
-    }
-
-    /**
-     * Returns the cached playlist from {@link ControllerCallback#onPlaylistChanged}.
-     * <p>
-     * This list may differ with the list that was specified with
-     * {@link #setPlaylist(List, MediaMetadata2)} depending on the {@link MediaPlaylistAgent}
-     * 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 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) {
-            return mPlaylist;
-        }
-    }
-
-    /**
-     * Sets the playlist.
-     * <p>
-     * Even when the playlist is successfully set, use the playlist returned from
-     * {@link #getPlaylist()} for playlist APIs such as {@link #skipToPlaylistItem(MediaItem2)}.
-     * Otherwise the session in the remote process can't distinguish between media items.
-     *
-     * @param list playlist
-     * @param metadata metadata of the playlist
-     * @see #getPlaylist()
-     * @see ControllerCallback#onPlaylistChanged
-     */
-    public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
-        if (list == null) {
-            throw new IllegalArgumentException("list shouldn't be null");
-        }
-        Bundle args = new Bundle();
-        args.putParcelableArray(ARGUMENT_PLAYLIST, MediaUtils2.toMediaItem2ParcelableArray(list));
-        args.putBundle(ARGUMENT_PLAYLIST_METADATA, metadata == null ? null : metadata.toBundle());
-        sendCommand(COMMAND_CODE_PLAYLIST_SET_LIST, args);
-    }
-
-    /**
-     * Updates the playlist metadata
-     *
-     * @param metadata metadata of the playlist
-     */
-    public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
-        Bundle args = new Bundle();
-        args.putBundle(ARGUMENT_PLAYLIST_METADATA, metadata == null ? null : metadata.toBundle());
-        sendCommand(COMMAND_CODE_PLAYLIST_SET_LIST_METADATA, args);
-    }
-
-    /**
-     * Gets the lastly cached playlist playlist metadata either from
-     * {@link ControllerCallback#onPlaylistMetadataChanged or
-     * {@link ControllerCallback#onPlaylistChanged}.
-     *
-     * @return metadata metadata of the playlist, or null if none is set
-     */
-    public @Nullable MediaMetadata2 getPlaylistMetadata() {
-        synchronized (mLock) {
-            return mPlaylistMetadata;
-        }
-    }
-
-    /**
-     * Adds the media item to the playlist at position index. Index equals or greater than
-     * 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,
-     * the current index of the playlist will be incremented correspondingly.
-     *
-     * @param index the index you want to add
-     * @param item the media item you want to add
-     */
-    public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
-        Bundle args = new Bundle();
-        args.putInt(ARGUMENT_PLAYLIST_INDEX, index);
-        args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
-        sendCommand(COMMAND_CODE_PLAYLIST_ADD_ITEM, args);
-    }
-
-    /**
-     * Removes the media item at index in the playlist.
-     *<p>
-     * If the item is the currently playing item of the playlist, current playback
-     * will be stopped and playback moves to next source in the list.
-     *
-     * @param item the media item you want to add
-     */
-    public void removePlaylistItem(@NonNull MediaItem2 item) {
-        Bundle args = new Bundle();
-        args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
-        sendCommand(COMMAND_CODE_PLAYLIST_REMOVE_ITEM, args);
-    }
-
-    /**
-     * Replace the media item at index in the playlist. This can be also used to update metadata of
-     * an item.
-     *
-     * @param index the index of the item to replace
-     * @param item the new item
-     */
-    public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
-        Bundle args = new Bundle();
-        args.putInt(ARGUMENT_PLAYLIST_INDEX, index);
-        args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
-        sendCommand(COMMAND_CODE_PLAYLIST_REPLACE_ITEM, args);
-    }
-
-    /**
-     * Get the lastly cached current item from
-     * {@link ControllerCallback#onCurrentMediaItemChanged(MediaController2, MediaItem2)}.
-     *
-     * @return the currently playing item, or null if unknown.
-     */
-    public MediaItem2 getCurrentMediaItem() {
-        synchronized (mLock) {
-            return mCurrentMediaItem;
-        }
-    }
-
-    /**
-     * Skips to the previous item in the playlist.
-     * <p>
-     * This calls {@link MediaPlaylistAgent#skipToPreviousItem()}.
-     */
-    public void skipToPreviousItem() {
-        sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM);
-    }
-
-    /**
-     * Skips to the next item in the playlist.
-     * <p>
-     * This calls {@link MediaPlaylistAgent#skipToNextItem()}.
-     */
-    public void skipToNextItem() {
-        sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM);
-    }
-
-    /**
-     * Skips to the item in the playlist.
-     * <p>
-     * This calls {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
-     *
-     * @param item The item in the playlist you want to play
-     */
-    public void skipToPlaylistItem(@NonNull MediaItem2 item) {
-        Bundle args = new Bundle();
-        args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
-        sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM, args);
-    }
-
-    /**
-     * Gets the cached repeat mode from the {@link ControllerCallback#onRepeatModeChanged}.
-     *
-     * @return repeat mode
-     * @see MediaPlaylistAgent#REPEAT_MODE_NONE
-     * @see MediaPlaylistAgent#REPEAT_MODE_ONE
-     * @see MediaPlaylistAgent#REPEAT_MODE_ALL
-     * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
-     */
-    public @RepeatMode int getRepeatMode() {
-        synchronized (mLock) {
-            return mRepeatMode;
-        }
-    }
-
-    /**
-     * Sets the repeat mode.
-     *
-     * @param repeatMode repeat mode
-     * @see MediaPlaylistAgent#REPEAT_MODE_NONE
-     * @see MediaPlaylistAgent#REPEAT_MODE_ONE
-     * @see MediaPlaylistAgent#REPEAT_MODE_ALL
-     * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
-     */
-    public void setRepeatMode(@RepeatMode int repeatMode) {
-        Bundle args = new Bundle();
-        args.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
-        sendCommand(COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE, args);
-    }
-
-    /**
-     * Gets the cached shuffle mode from the {@link ControllerCallback#onShuffleModeChanged}.
-     *
-     * @return The shuffle mode
-     * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
-     * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
-     * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
-     */
-    public @ShuffleMode int getShuffleMode() {
-        synchronized (mLock) {
-            return mShuffleMode;
-        }
-    }
-
-    /**
-     * Sets the shuffle mode.
-     *
-     * @param shuffleMode The shuffle mode
-     * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
-     * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
-     * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
-     */
-    public void setShuffleMode(@ShuffleMode int shuffleMode) {
-        Bundle args = new Bundle();
-        args.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
-        sendCommand(COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE, args);
-    }
-
-    /**
-     * Queries for information about the routes currently known.
-     */
-    public void subscribeRoutesInfo() {
-        sendCommand(COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO);
-    }
-
-    /**
-     * Unsubscribes for changes to the routes.
-     * <p>
-     * The {@link ControllerCallback#onRoutesInfoChanged callback} will no longer be invoked for
-     * the routes once this method returns.
-     * </p>
-     */
-    public void unsubscribeRoutesInfo() {
-        sendCommand(COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO);
-    }
-
-    /**
-     * Selects the specified route.
-     *
-     * @param route The route to select.
-     */
-    public void selectRoute(@NonNull Bundle route) {
-        if (route == null) {
-            throw new IllegalArgumentException("route shouldn't be null");
-        }
-        Bundle args = new Bundle();
-        args.putBundle(ARGUMENT_ROUTE_BUNDLE, route);
-        sendCommand(COMMAND_CODE_SESSION_SELECT_ROUTE, args);
-    }
-
-    // Should be used without a lock to prevent potential deadlock.
-    void onConnectedNotLocked(Bundle data) {
-        // is enough or should we pass it while connecting?
-        final SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
-                data.getBundle(ARGUMENT_ALLOWED_COMMANDS));
-        final int playerState = data.getInt(ARGUMENT_PLAYER_STATE);
-        final int bufferingState = data.getInt(ARGUMENT_BUFFERING_STATE);
-        final PlaybackStateCompat playbackStateCompat = data.getParcelable(
-                ARGUMENT_PLAYBACK_STATE_COMPAT);
-        final int repeatMode = data.getInt(ARGUMENT_REPEAT_MODE);
-        final int shuffleMode = data.getInt(ARGUMENT_SHUFFLE_MODE);
-        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);
-        }
-        boolean close = false;
-        try {
-            synchronized (mLock) {
-                if (mIsReleased) {
-                    return;
-                }
-                if (mConnected) {
-                    Log.e(TAG, "Cannot be notified about the connection result many times."
-                            + " Probably a bug or malicious app.");
-                    close = true;
-                    return;
-                }
-                mAllowedCommands = allowedCommands;
-                mPlayerState = playerState;
-                mBufferingState = bufferingState;
-                mPlaybackStateCompat = playbackStateCompat;
-                mRepeatMode = repeatMode;
-                mShuffleMode = shuffleMode;
-                mPlaylist = playlist;
-                mCurrentMediaItem = currentMediaItem;
-                mPlaylistMetadata = metadata;
-                mConnected = true;
-                mPlaybackInfo = playbackInfo;
-            }
-            mCallbackExecutor.execute(new Runnable() {
-                @Override
-                public void run() {
-                    // Note: We may trigger ControllerCallbacks with the initial values
-                    // But it's hard to define the order of the controller callbacks
-                    // Only notify about the
-                    mCallback.onConnected(MediaController2.this, allowedCommands);
-                }
-            });
-        } finally {
-            if (close) {
-                // Trick to call release() without holding the lock, to prevent potential deadlock
-                // with the developer's custom lock within the ControllerCallback.onDisconnected().
-                close();
-            }
-        }
-    }
-
-    private void initialize() {
-        if (mToken.getType() == SessionToken2.TYPE_SESSION) {
-            synchronized (mLock) {
-                mBrowserCompat = null;
-            }
-            connectToSession(mToken.getSessionCompatToken());
-        } else {
-            connectToService();
-        }
-    }
-
-    private void connectToSession(MediaSessionCompat.Token sessionCompatToken) {
-        MediaControllerCompat controllerCompat = null;
-        try {
-            controllerCompat = new MediaControllerCompat(mContext, sessionCompatToken);
-        } catch (RemoteException e) {
-            e.printStackTrace();
-        }
-        synchronized (mLock) {
-            mControllerCompat = controllerCompat;
-            mControllerCompatCallback = new ControllerCompatCallback();
-            mControllerCompat.registerCallback(mControllerCompatCallback, mHandler);
-        }
-
-        if (controllerCompat.isSessionReady()) {
-            sendCommand(CONTROLLER_COMMAND_CONNECT, new ResultReceiver(mHandler) {
-                @Override
-                protected void onReceiveResult(int resultCode, Bundle resultData) {
-                    if (!mHandlerThread.isAlive()) {
-                        return;
-                    }
-                    switch (resultCode) {
-                        case CONNECT_RESULT_CONNECTED:
-                            onConnectedNotLocked(resultData);
-                            break;
-                        case CONNECT_RESULT_DISCONNECTED:
-                            mCallback.onDisconnected(MediaController2.this);
-                            close();
-                            break;
-                    }
-                }
-            });
-        }
-    }
-
-    private void connectToService() {
-        synchronized (mLock) {
-            mBrowserCompat = new MediaBrowserCompat(mContext, mToken.getComponentName(),
-                    new ConnectionCallback(), sDefaultRootExtras);
-            mBrowserCompat.connect();
-        }
-    }
-
-    private void sendCommand(int commandCode) {
-        sendCommand(commandCode, null);
-    }
-
-    private void sendCommand(int commandCode, Bundle args) {
-        if (args == null) {
-            args = new Bundle();
-        }
-        args.putInt(ARGUMENT_COMMAND_CODE, commandCode);
-        sendCommand(CONTROLLER_COMMAND_BY_COMMAND_CODE, args, null);
-    }
-
-    private void sendCommand(String command) {
-        sendCommand(command, null, null);
-    }
-
-    private void sendCommand(String command, ResultReceiver receiver) {
-        sendCommand(command, null, receiver);
-    }
-
-    private void sendCommand(String command, Bundle args, ResultReceiver receiver) {
-        if (args == null) {
-            args = new Bundle();
-        }
-        MediaControllerCompat controller;
-        ControllerCompatCallback callback;
-        synchronized (mLock) {
-            controller = mControllerCompat;
-            callback = mControllerCompatCallback;
-        }
-        args.putBinder(ARGUMENT_ICONTROLLER_CALLBACK, callback.getIControllerCallback().asBinder());
-        args.putString(ARGUMENT_PACKAGE_NAME, mContext.getPackageName());
-        args.putInt(ARGUMENT_UID, Process.myUid());
-        args.putInt(ARGUMENT_PID, Process.myPid());
-        controller.sendCommand(command, args, receiver);
-    }
-
-    @NonNull Context getContext() {
-        return mContext;
-    }
-
-    @NonNull ControllerCallback getCallback() {
-        return mCallback;
-    }
-
-    @NonNull Executor getCallbackExecutor() {
-        return mCallbackExecutor;
-    }
-
-    @Nullable MediaBrowserCompat getBrowserCompat() {
-        synchronized (mLock) {
-            return mBrowserCompat;
-        }
-    }
-
-    private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
-        @Override
-        public void onConnected() {
-            MediaBrowserCompat browser = getBrowserCompat();
-            if (browser != null) {
-                connectToSession(browser.getSessionToken());
-            } else if (DEBUG) {
-                Log.d(TAG, "Controller is closed prematually", new IllegalStateException());
-            }
-        }
-
-        @Override
-        public void onConnectionSuspended() {
-            close();
-        }
-
-        @Override
-        public void onConnectionFailed() {
-            close();
-        }
-    }
 }
diff --git a/media/src/main/java/androidx/media/MediaController2ImplBase.java b/media/src/main/java/androidx/media/MediaController2ImplBase.java
new file mode 100644
index 0000000..1bd0009
--- /dev/null
+++ b/media/src/main/java/androidx/media/MediaController2ImplBase.java
@@ -0,0 +1,1283 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION;
+
+import static androidx.media.MediaConstants2.ARGUMENT_ALLOWED_COMMANDS;
+import static androidx.media.MediaConstants2.ARGUMENT_ARGUMENTS;
+import static androidx.media.MediaConstants2.ARGUMENT_BUFFERING_STATE;
+import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_BUTTONS;
+import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_CODE;
+import static androidx.media.MediaConstants2.ARGUMENT_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.ARGUMENT_ERROR_CODE;
+import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
+import static androidx.media.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK;
+import static androidx.media.MediaConstants2.ARGUMENT_ITEM_COUNT;
+import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ID;
+import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ITEM;
+import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME;
+import static androidx.media.MediaConstants2.ARGUMENT_PID;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_INFO;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_SPEED;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_STATE_COMPAT;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYER_STATE;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_INDEX;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_METADATA;
+import static androidx.media.MediaConstants2.ARGUMENT_QUERY;
+import static androidx.media.MediaConstants2.ARGUMENT_RATING;
+import static androidx.media.MediaConstants2.ARGUMENT_REPEAT_MODE;
+import static androidx.media.MediaConstants2.ARGUMENT_RESULT_RECEIVER;
+import static androidx.media.MediaConstants2.ARGUMENT_ROUTE_BUNDLE;
+import static androidx.media.MediaConstants2.ARGUMENT_SEEK_POSITION;
+import static androidx.media.MediaConstants2.ARGUMENT_SHUFFLE_MODE;
+import static androidx.media.MediaConstants2.ARGUMENT_UID;
+import static androidx.media.MediaConstants2.ARGUMENT_URI;
+import static androidx.media.MediaConstants2.ARGUMENT_VOLUME;
+import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_DIRECTION;
+import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_FLAGS;
+import static androidx.media.MediaConstants2.CONNECT_RESULT_CONNECTED;
+import static androidx.media.MediaConstants2.CONNECT_RESULT_DISCONNECTED;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_COMMAND_CODE;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_CONNECT;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_DISCONNECT;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CHILDREN_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYER_STATE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_REPEAT_MODE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ROUTES_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SEARCH_RESULT_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SEEK_COMPLETED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_SEND_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.SESSION_EVENT_SET_CUSTOM_LAYOUT;
+import static androidx.media.MediaPlayerInterface.BUFFERING_STATE_UNKNOWN;
+import static androidx.media.MediaPlayerInterface.UNKNOWN_TIME;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_RESET;
+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_REMOVE_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_REWIND;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SELECT_ROUTE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SET_RATING;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME;
+import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.SystemClock;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.app.BundleCompat;
+import androidx.media.MediaController2.ControllerCallback;
+import androidx.media.MediaController2.PlaybackInfo;
+import androidx.media.MediaController2.VolumeDirection;
+import androidx.media.MediaController2.VolumeFlags;
+import androidx.media.MediaPlaylistAgent.RepeatMode;
+import androidx.media.MediaPlaylistAgent.ShuffleMode;
+import androidx.media.MediaSession2.CommandButton;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+class MediaController2ImplBase implements MediaController2.SupportLibraryImpl {
+
+    private static final String TAG = "MC2ImplBase";
+    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.
+    static final Bundle sDefaultRootExtras = new Bundle();
+    static {
+        sDefaultRootExtras.putBoolean(MediaConstants2.ROOT_EXTRA_DEFAULT, true);
+    }
+
+    private final Context mContext;
+    private final Object mLock = new Object();
+
+    private final SessionToken2 mToken;
+    private final ControllerCallback mCallback;
+    private final Executor mCallbackExecutor;
+    private final IBinder.DeathRecipient mDeathRecipient;
+
+    private final HandlerThread mHandlerThread;
+    private final Handler mHandler;
+
+    private MediaController2 mInstance;
+
+    @GuardedBy("mLock")
+    private MediaBrowserCompat mBrowserCompat;
+    @GuardedBy("mLock")
+    private boolean mIsReleased;
+    @GuardedBy("mLock")
+    private List<MediaItem2> mPlaylist;
+    @GuardedBy("mLock")
+    private MediaMetadata2 mPlaylistMetadata;
+    @GuardedBy("mLock")
+    private @RepeatMode int mRepeatMode;
+    @GuardedBy("mLock")
+    private @ShuffleMode int mShuffleMode;
+    @GuardedBy("mLock")
+    private int mPlayerState;
+    @GuardedBy("mLock")
+    private MediaItem2 mCurrentMediaItem;
+    @GuardedBy("mLock")
+    private int mBufferingState;
+    @GuardedBy("mLock")
+    private PlaybackInfo mPlaybackInfo;
+    @GuardedBy("mLock")
+    private SessionCommandGroup2 mAllowedCommands;
+
+    // Media 1.0 variables
+    @GuardedBy("mLock")
+    private MediaControllerCompat mControllerCompat;
+    @GuardedBy("mLock")
+    private ControllerCompatCallback mControllerCompatCallback;
+    @GuardedBy("mLock")
+    private PlaybackStateCompat mPlaybackStateCompat;
+    @GuardedBy("mLock")
+    private MediaMetadataCompat mMediaMetadataCompat;
+
+    // Assignment should be used with the lock hold, but should be used without a lock to prevent
+    // potential deadlock.
+    @GuardedBy("mLock")
+    private volatile boolean mConnected;
+
+    MediaController2ImplBase(@NonNull Context context, @NonNull SessionToken2 token,
+            @NonNull Executor executor, @NonNull ControllerCallback callback) {
+        super();
+        if (context == null) {
+            throw new IllegalArgumentException("context shouldn't be null");
+        }
+        if (token == null) {
+            throw new IllegalArgumentException("token shouldn't be null");
+        }
+        if (callback == null) {
+            throw new IllegalArgumentException("callback shouldn't be null");
+        }
+        if (executor == null) {
+            throw new IllegalArgumentException("executor shouldn't be null");
+        }
+        mContext = context;
+        mHandlerThread = new HandlerThread("MediaController2_Thread");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mToken = token;
+        mCallback = callback;
+        mCallbackExecutor = executor;
+        mDeathRecipient = new IBinder.DeathRecipient() {
+            @Override
+            public void binderDied() {
+                MediaController2ImplBase.this.close();
+            }
+        };
+
+        initialize();
+    }
+
+    @Override
+    public void setInstance(MediaController2 controller) {
+        mInstance = controller;
+    }
+
+    @Override
+    public void close() {
+        if (DEBUG) {
+            //Log.d(TAG, "release from " + mToken, new IllegalStateException());
+        }
+        synchronized (mLock) {
+            if (mIsReleased) {
+                // Prevent re-enterance from the ControllerCallback.onDisconnected()
+                return;
+            }
+            mHandler.removeCallbacksAndMessages(null);
+
+            if (Build.VERSION.SDK_INT >= 18) {
+                mHandlerThread.quitSafely();
+            } else {
+                mHandlerThread.quit();
+            }
+
+            mIsReleased = true;
+
+            // Send command before the unregister callback to use mIControllerCallback in the
+            // callback.
+            sendCommand(CONTROLLER_COMMAND_DISCONNECT);
+            if (mControllerCompat != null) {
+                mControllerCompat.unregisterCallback(mControllerCompatCallback);
+            }
+            if (mBrowserCompat != null) {
+                mBrowserCompat.disconnect();
+                mBrowserCompat = null;
+            }
+            if (mControllerCompat != null) {
+                mControllerCompat.unregisterCallback(mControllerCompatCallback);
+                mControllerCompat = null;
+            }
+            mConnected = false;
+        }
+        mCallbackExecutor.execute(new Runnable() {
+            @Override
+            public void run() {
+                mCallback.onDisconnected(mInstance);
+            }
+        });
+    }
+
+    @Override
+    public @NonNull SessionToken2 getSessionToken() {
+        return mToken;
+    }
+
+    @Override
+    public boolean isConnected() {
+        synchronized (mLock) {
+            return mConnected;
+        }
+    }
+
+    @Override
+    public void play() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            sendCommand(COMMAND_CODE_PLAYBACK_PLAY);
+        }
+    }
+
+    @Override
+    public void pause() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            sendCommand(COMMAND_CODE_PLAYBACK_PAUSE);
+        }
+    }
+
+    @Override
+    public void reset() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            sendCommand(COMMAND_CODE_PLAYBACK_RESET);
+        }
+    }
+
+    @Override
+    public void prepare() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            sendCommand(COMMAND_CODE_PLAYBACK_PREPARE);
+        }
+    }
+
+    @Override
+    public void fastForward() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            sendCommand(COMMAND_CODE_SESSION_FAST_FORWARD);
+        }
+    }
+
+    @Override
+    public void rewind() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            sendCommand(COMMAND_CODE_SESSION_REWIND);
+        }
+    }
+
+    @Override
+    public void seekTo(long pos) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putLong(ARGUMENT_SEEK_POSITION, pos);
+            sendCommand(COMMAND_CODE_PLAYBACK_SEEK_TO, args);
+        }
+    }
+
+    @Override
+    public void skipForward() {
+        // To match with KEYCODE_MEDIA_SKIP_FORWARD
+    }
+
+    @Override
+    public void skipBackward() {
+        // To match with KEYCODE_MEDIA_SKIP_BACKWARD
+    }
+
+    @Override
+    public void playFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putString(ARGUMENT_MEDIA_ID, mediaId);
+            args.putBundle(ARGUMENT_EXTRAS, extras);
+            sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID, args);
+        }
+    }
+
+    @Override
+    public void playFromSearch(@NonNull String query, @Nullable Bundle extras) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putString(ARGUMENT_QUERY, query);
+            args.putBundle(ARGUMENT_EXTRAS, extras);
+            sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_SEARCH, args);
+        }
+    }
+
+    @Override
+    public void playFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putParcelable(ARGUMENT_URI, uri);
+            args.putBundle(ARGUMENT_EXTRAS, extras);
+            sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_URI, args);
+        }
+    }
+
+    @Override
+    public void prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putString(ARGUMENT_MEDIA_ID, mediaId);
+            args.putBundle(ARGUMENT_EXTRAS, extras);
+            sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID, args);
+        }
+    }
+
+    @Override
+    public void prepareFromSearch(@NonNull String query, @Nullable Bundle extras) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putString(ARGUMENT_QUERY, query);
+            args.putBundle(ARGUMENT_EXTRAS, extras);
+            sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH, args);
+        }
+    }
+
+    @Override
+    public void prepareFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putParcelable(ARGUMENT_URI, uri);
+            args.putBundle(ARGUMENT_EXTRAS, extras);
+            sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_URI, args);
+        }
+    }
+
+    @Override
+    public void setVolumeTo(int value, @VolumeFlags int flags) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putInt(ARGUMENT_VOLUME, value);
+            args.putInt(ARGUMENT_VOLUME_FLAGS, flags);
+            sendCommand(COMMAND_CODE_VOLUME_SET_VOLUME, args);
+        }
+    }
+
+    @Override
+    public void adjustVolume(@VolumeDirection int direction, @VolumeFlags int flags) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putInt(ARGUMENT_VOLUME_DIRECTION, direction);
+            args.putInt(ARGUMENT_VOLUME_FLAGS, flags);
+            sendCommand(COMMAND_CODE_VOLUME_ADJUST_VOLUME, args);
+        }
+    }
+
+    @Override
+    public @Nullable PendingIntent getSessionActivity() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return null;
+            }
+            return mControllerCompat.getSessionActivity();
+        }
+    }
+
+    @Override
+    public int getPlayerState() {
+        synchronized (mLock) {
+            return mPlayerState;
+        }
+    }
+
+    @Override
+    public long getDuration() {
+        synchronized (mLock) {
+            if (mMediaMetadataCompat != null
+                    && mMediaMetadataCompat.containsKey(METADATA_KEY_DURATION)) {
+                return mMediaMetadataCompat.getLong(METADATA_KEY_DURATION);
+            }
+        }
+        return MediaPlayerInterface.UNKNOWN_TIME;
+    }
+
+    @Override
+    public long getCurrentPosition() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return UNKNOWN_TIME;
+            }
+            if (mPlaybackStateCompat != null) {
+                long timeDiff = (mInstance.mTimeDiff != null) ? mInstance.mTimeDiff
+                        : SystemClock.elapsedRealtime()
+                                - mPlaybackStateCompat.getLastPositionUpdateTime();
+                long expectedPosition = mPlaybackStateCompat.getPosition()
+                        + (long) (mPlaybackStateCompat.getPlaybackSpeed() * timeDiff);
+                return Math.max(0, expectedPosition);
+            }
+            return UNKNOWN_TIME;
+        }
+    }
+
+    @Override
+    public float getPlaybackSpeed() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return 0f;
+            }
+            return (mPlaybackStateCompat == null) ? 0f : mPlaybackStateCompat.getPlaybackSpeed();
+        }
+    }
+
+    @Override
+    public void setPlaybackSpeed(float speed) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putFloat(ARGUMENT_PLAYBACK_SPEED, speed);
+            sendCommand(COMMAND_CODE_PLAYBACK_SET_SPEED, args);
+        }
+    }
+
+    @Override
+    public @MediaPlayerInterface.BuffState int getBufferingState() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return BUFFERING_STATE_UNKNOWN;
+            }
+            return mBufferingState;
+        }
+    }
+
+    @Override
+    public long getBufferedPosition() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return UNKNOWN_TIME;
+            }
+            return (mPlaybackStateCompat == null) ? UNKNOWN_TIME
+                    : mPlaybackStateCompat.getBufferedPosition();
+        }
+    }
+
+    @Override
+    public @Nullable PlaybackInfo getPlaybackInfo() {
+        synchronized (mLock) {
+            return mPlaybackInfo;
+        }
+    }
+
+    @Override
+    public void setRating(@NonNull String mediaId, @NonNull Rating2 rating) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putString(ARGUMENT_MEDIA_ID, mediaId);
+            args.putBundle(ARGUMENT_RATING, rating.toBundle());
+            sendCommand(COMMAND_CODE_SESSION_SET_RATING, args);
+        }
+    }
+
+    @Override
+    public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args,
+            @Nullable ResultReceiver cb) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle bundle = new Bundle();
+            bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
+            bundle.putBundle(ARGUMENT_ARGUMENTS, args);
+            sendCommand(CONTROLLER_COMMAND_BY_CUSTOM_COMMAND, bundle, cb);
+        }
+    }
+
+    @Override
+    public @Nullable List<MediaItem2> getPlaylist() {
+        synchronized (mLock) {
+            return mPlaylist;
+        }
+    }
+
+    @Override
+    public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+        if (list == null) {
+            throw new IllegalArgumentException("list shouldn't be null");
+        }
+        Bundle args = new Bundle();
+        args.putParcelableArray(ARGUMENT_PLAYLIST, MediaUtils2.toMediaItem2ParcelableArray(list));
+        args.putBundle(ARGUMENT_PLAYLIST_METADATA, metadata == null ? null : metadata.toBundle());
+        sendCommand(COMMAND_CODE_PLAYLIST_SET_LIST, args);
+    }
+
+    @Override
+    public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+        Bundle args = new Bundle();
+        args.putBundle(ARGUMENT_PLAYLIST_METADATA, metadata == null ? null : metadata.toBundle());
+        sendCommand(COMMAND_CODE_PLAYLIST_SET_LIST_METADATA, args);
+    }
+
+    @Override
+    public @Nullable MediaMetadata2 getPlaylistMetadata() {
+        synchronized (mLock) {
+            return mPlaylistMetadata;
+        }
+    }
+
+    @Override
+    public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+        Bundle args = new Bundle();
+        args.putInt(ARGUMENT_PLAYLIST_INDEX, index);
+        args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+        sendCommand(COMMAND_CODE_PLAYLIST_ADD_ITEM, args);
+    }
+
+    @Override
+    public void removePlaylistItem(@NonNull MediaItem2 item) {
+        Bundle args = new Bundle();
+        args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+        sendCommand(COMMAND_CODE_PLAYLIST_REMOVE_ITEM, args);
+    }
+
+    @Override
+    public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+        Bundle args = new Bundle();
+        args.putInt(ARGUMENT_PLAYLIST_INDEX, index);
+        args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+        sendCommand(COMMAND_CODE_PLAYLIST_REPLACE_ITEM, args);
+    }
+
+    @Override
+    public MediaItem2 getCurrentMediaItem() {
+        synchronized (mLock) {
+            return mCurrentMediaItem;
+        }
+    }
+
+    @Override
+    public void skipToPreviousItem() {
+        sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM);
+    }
+
+    @Override
+    public void skipToNextItem() {
+        sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM);
+    }
+
+    @Override
+    public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+        Bundle args = new Bundle();
+        args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+        sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM, args);
+    }
+
+    @Override
+    public @RepeatMode int getRepeatMode() {
+        synchronized (mLock) {
+            return mRepeatMode;
+        }
+    }
+
+    @Override
+    public void setRepeatMode(@RepeatMode int repeatMode) {
+        Bundle args = new Bundle();
+        args.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
+        sendCommand(COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE, args);
+    }
+
+    @Override
+    public @ShuffleMode int getShuffleMode() {
+        synchronized (mLock) {
+            return mShuffleMode;
+        }
+    }
+
+    @Override
+    public void setShuffleMode(@ShuffleMode int shuffleMode) {
+        Bundle args = new Bundle();
+        args.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
+        sendCommand(COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE, args);
+    }
+
+    @Override
+    public void subscribeRoutesInfo() {
+        sendCommand(COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO);
+    }
+
+    @Override
+    public void unsubscribeRoutesInfo() {
+        sendCommand(COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO);
+    }
+
+    @Override
+    public void selectRoute(@NonNull Bundle route) {
+        if (route == null) {
+            throw new IllegalArgumentException("route shouldn't be null");
+        }
+        Bundle args = new Bundle();
+        args.putBundle(ARGUMENT_ROUTE_BUNDLE, route);
+        sendCommand(COMMAND_CODE_SESSION_SELECT_ROUTE, args);
+    }
+
+    @Override
+    public @NonNull Context getContext() {
+        return mContext;
+    }
+
+    @Override
+    public @NonNull ControllerCallback getCallback() {
+        return mCallback;
+    }
+
+    @Override
+    public @NonNull Executor getCallbackExecutor() {
+        return mCallbackExecutor;
+    }
+
+    @Override
+    public @Nullable MediaBrowserCompat getBrowserCompat() {
+        synchronized (mLock) {
+            return mBrowserCompat;
+        }
+    }
+
+    // Should be used without a lock to prevent potential deadlock.
+    void onConnectedNotLocked(Bundle data) {
+        data.setClassLoader(MediaSession2.class.getClassLoader());
+        // is enough or should we pass it while connecting?
+        final SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
+                data.getBundle(ARGUMENT_ALLOWED_COMMANDS));
+        final int playerState = data.getInt(ARGUMENT_PLAYER_STATE);
+        final int bufferingState = data.getInt(ARGUMENT_BUFFERING_STATE);
+        final PlaybackStateCompat playbackStateCompat = data.getParcelable(
+                ARGUMENT_PLAYBACK_STATE_COMPAT);
+        final int repeatMode = data.getInt(ARGUMENT_REPEAT_MODE);
+        final int shuffleMode = data.getInt(ARGUMENT_SHUFFLE_MODE);
+        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);
+        }
+        boolean close = false;
+        try {
+            synchronized (mLock) {
+                if (mIsReleased) {
+                    return;
+                }
+                if (mConnected) {
+                    Log.e(TAG, "Cannot be notified about the connection result many times."
+                            + " Probably a bug or malicious app.");
+                    close = true;
+                    return;
+                }
+                mAllowedCommands = allowedCommands;
+                mPlayerState = playerState;
+                mBufferingState = bufferingState;
+                mPlaybackStateCompat = playbackStateCompat;
+                mRepeatMode = repeatMode;
+                mShuffleMode = shuffleMode;
+                mPlaylist = playlist;
+                mCurrentMediaItem = currentMediaItem;
+                mPlaylistMetadata = metadata;
+                mConnected = true;
+                mPlaybackInfo = playbackInfo;
+            }
+            mCallbackExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    // Note: We may trigger ControllerCallbacks with the initial values
+                    // But it's hard to define the order of the controller callbacks
+                    // Only notify about the
+                    mCallback.onConnected(mInstance, allowedCommands);
+                }
+            });
+        } finally {
+            if (close) {
+                // Trick to call release() without holding the lock, to prevent potential deadlock
+                // with the developer's custom lock within the ControllerCallback.onDisconnected().
+                close();
+            }
+        }
+    }
+
+    private void initialize() {
+        if (mToken.getType() == SessionToken2.TYPE_SESSION) {
+            synchronized (mLock) {
+                mBrowserCompat = null;
+            }
+            connectToSession(mToken.getSessionCompatToken());
+        } else {
+            connectToService();
+        }
+    }
+
+    private void connectToSession(MediaSessionCompat.Token sessionCompatToken) {
+        MediaControllerCompat controllerCompat = null;
+        try {
+            controllerCompat = new MediaControllerCompat(mContext, sessionCompatToken);
+        } catch (RemoteException e) {
+            e.printStackTrace();
+        }
+        synchronized (mLock) {
+            mControllerCompat = controllerCompat;
+            mControllerCompatCallback = new ControllerCompatCallback();
+            mControllerCompat.registerCallback(mControllerCompatCallback, mHandler);
+        }
+
+        if (controllerCompat.isSessionReady()) {
+            sendCommand(CONTROLLER_COMMAND_CONNECT, new ResultReceiver(mHandler) {
+                @Override
+                protected void onReceiveResult(int resultCode, Bundle resultData) {
+                    if (!mHandlerThread.isAlive()) {
+                        return;
+                    }
+                    switch (resultCode) {
+                        case CONNECT_RESULT_CONNECTED:
+                            onConnectedNotLocked(resultData);
+                            break;
+                        case CONNECT_RESULT_DISCONNECTED:
+                            mCallbackExecutor.execute(new Runnable() {
+                                @Override
+                                public void run() {
+                                    mCallback.onDisconnected(mInstance);
+                                }
+                            });
+                            close();
+                            break;
+                    }
+                }
+            });
+        }
+    }
+
+    private void connectToService() {
+        mCallbackExecutor.execute(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (mLock) {
+                    mBrowserCompat = new MediaBrowserCompat(mContext, mToken.getComponentName(),
+                            new ConnectionCallback(), sDefaultRootExtras);
+                    mBrowserCompat.connect();
+                }
+            }
+        });
+    }
+
+    private void sendCommand(int commandCode) {
+        sendCommand(commandCode, null);
+    }
+
+    private void sendCommand(int commandCode, Bundle args) {
+        if (args == null) {
+            args = new Bundle();
+        }
+        args.putInt(ARGUMENT_COMMAND_CODE, commandCode);
+        sendCommand(CONTROLLER_COMMAND_BY_COMMAND_CODE, args, null);
+    }
+
+    private void sendCommand(String command) {
+        sendCommand(command, null, null);
+    }
+
+    private void sendCommand(String command, ResultReceiver receiver) {
+        sendCommand(command, null, receiver);
+    }
+
+    private void sendCommand(String command, Bundle args, ResultReceiver receiver) {
+        if (args == null) {
+            args = new Bundle();
+        }
+        MediaControllerCompat controller;
+        ControllerCompatCallback callback;
+        synchronized (mLock) {
+            controller = mControllerCompat;
+            callback = mControllerCompatCallback;
+        }
+        BundleCompat.putBinder(args, ARGUMENT_ICONTROLLER_CALLBACK,
+                callback.getIControllerCallback().asBinder());
+        args.putString(ARGUMENT_PACKAGE_NAME, mContext.getPackageName());
+        args.putInt(ARGUMENT_UID, Process.myUid());
+        args.putInt(ARGUMENT_PID, Process.myPid());
+        controller.sendCommand(command, args, receiver);
+    }
+
+    private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
+        @Override
+        public void onConnected() {
+            MediaBrowserCompat browser = getBrowserCompat();
+            if (browser != null) {
+                connectToSession(browser.getSessionToken());
+            } else if (DEBUG) {
+                Log.d(TAG, "Controller is closed prematually", new IllegalStateException());
+            }
+        }
+
+        @Override
+        public void onConnectionSuspended() {
+            close();
+        }
+
+        @Override
+        public void onConnectionFailed() {
+            close();
+        }
+    }
+
+    private final class ControllerCompatCallback extends MediaControllerCompat.Callback {
+        @Override
+        public void onSessionReady() {
+            sendCommand(CONTROLLER_COMMAND_CONNECT, new ResultReceiver(mHandler) {
+                @Override
+                protected void onReceiveResult(int resultCode, Bundle resultData) {
+                    if (!mHandlerThread.isAlive()) {
+                        return;
+                    }
+                    switch (resultCode) {
+                        case CONNECT_RESULT_CONNECTED:
+                            onConnectedNotLocked(resultData);
+                            break;
+                        case CONNECT_RESULT_DISCONNECTED:
+                            mCallbackExecutor.execute(new Runnable() {
+                                @Override
+                                public void run() {
+                                    mCallback.onDisconnected(mInstance);
+                                }
+                            });
+                            close();
+                            break;
+                    }
+                }
+            });
+        }
+
+        @Override
+        public void onSessionDestroyed() {
+            close();
+        }
+
+        @Override
+        public void onPlaybackStateChanged(PlaybackStateCompat state) {
+            synchronized (mLock) {
+                mPlaybackStateCompat = state;
+            }
+        }
+
+        @Override
+        public void onMetadataChanged(MediaMetadataCompat metadata) {
+            synchronized (mLock) {
+                mMediaMetadataCompat = metadata;
+            }
+        }
+
+        @Override
+        public void onSessionEvent(String event, Bundle extras) {
+            if (extras != null) {
+                extras.setClassLoader(MediaSession2.class.getClassLoader());
+            }
+            switch (event) {
+                case SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED: {
+                    final SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
+                            extras.getBundle(ARGUMENT_ALLOWED_COMMANDS));
+                    synchronized (mLock) {
+                        mAllowedCommands = allowedCommands;
+                    }
+                    mCallbackExecutor.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            mCallback.onAllowedCommandsChanged(mInstance, allowedCommands);
+                        }
+                    });
+                    break;
+                }
+                case SESSION_EVENT_ON_PLAYER_STATE_CHANGED: {
+                    final int playerState = extras.getInt(ARGUMENT_PLAYER_STATE);
+                    PlaybackStateCompat state =
+                            extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT);
+                    if (state == null) {
+                        return;
+                    }
+                    synchronized (mLock) {
+                        mPlayerState = playerState;
+                        mPlaybackStateCompat = state;
+                    }
+                    mCallbackExecutor.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            mCallback.onPlayerStateChanged(mInstance, playerState);
+                        }
+                    });
+                    break;
+                }
+                case SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED: {
+                    final MediaItem2 item = MediaItem2.fromBundle(
+                            extras.getBundle(ARGUMENT_MEDIA_ITEM));
+                    synchronized (mLock) {
+                        mCurrentMediaItem = item;
+                    }
+                    mCallbackExecutor.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            mCallback.onCurrentMediaItemChanged(mInstance, item);
+                        }
+                    });
+                    break;
+                }
+                case SESSION_EVENT_ON_ERROR: {
+                    final int errorCode = extras.getInt(ARGUMENT_ERROR_CODE);
+                    final Bundle errorExtras = extras.getBundle(ARGUMENT_EXTRAS);
+                    mCallbackExecutor.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            mCallback.onError(mInstance, errorCode, errorExtras);
+                        }
+                    });
+                    break;
+                }
+                case SESSION_EVENT_ON_ROUTES_INFO_CHANGED: {
+                    final List<Bundle> routes = MediaUtils2.toBundleList(
+                            extras.getParcelableArray(ARGUMENT_ROUTE_BUNDLE));
+                    mCallbackExecutor.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            mCallback.onRoutesInfoChanged(mInstance, routes);
+                        }
+                    });
+                    break;
+                }
+                case SESSION_EVENT_ON_PLAYLIST_CHANGED: {
+                    final MediaMetadata2 playlistMetadata = MediaMetadata2.fromBundle(
+                            extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
+                    final List<MediaItem2> playlist = MediaUtils2.fromMediaItem2ParcelableArray(
+                            extras.getParcelableArray(ARGUMENT_PLAYLIST));
+                    synchronized (mLock) {
+                        mPlaylist = playlist;
+                        mPlaylistMetadata = playlistMetadata;
+                    }
+                    mCallbackExecutor.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            mCallback.onPlaylistChanged(mInstance, playlist, playlistMetadata);
+                        }
+                    });
+                    break;
+                }
+                case SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED: {
+                    final MediaMetadata2 playlistMetadata = MediaMetadata2.fromBundle(
+                            extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
+                    synchronized (mLock) {
+                        mPlaylistMetadata = playlistMetadata;
+                    }
+                    mCallbackExecutor.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            mCallback.onPlaylistMetadataChanged(mInstance, playlistMetadata);
+                        }
+                    });
+                    break;
+                }
+                case SESSION_EVENT_ON_REPEAT_MODE_CHANGED: {
+                    final int repeatMode = extras.getInt(ARGUMENT_REPEAT_MODE);
+                    synchronized (mLock) {
+                        mRepeatMode = repeatMode;
+                    }
+                    mCallbackExecutor.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            mCallback.onRepeatModeChanged(mInstance, repeatMode);
+                        }
+                    });
+                    break;
+                }
+                case SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED: {
+                    final int shuffleMode = extras.getInt(ARGUMENT_SHUFFLE_MODE);
+                    synchronized (mLock) {
+                        mShuffleMode = shuffleMode;
+                    }
+                    mCallbackExecutor.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            mCallback.onShuffleModeChanged(mInstance, shuffleMode);
+                        }
+                    });
+                    break;
+                }
+                case SESSION_EVENT_SEND_CUSTOM_COMMAND: {
+                    Bundle commandBundle = extras.getBundle(ARGUMENT_CUSTOM_COMMAND);
+                    if (commandBundle == null) {
+                        return;
+                    }
+                    final SessionCommand2 command = SessionCommand2.fromBundle(commandBundle);
+                    final Bundle args = extras.getBundle(ARGUMENT_ARGUMENTS);
+                    final ResultReceiver receiver = extras.getParcelable(ARGUMENT_RESULT_RECEIVER);
+                    mCallbackExecutor.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            mCallback.onCustomCommand(mInstance, command, args, receiver);
+                        }
+                    });
+                    break;
+                }
+                case SESSION_EVENT_SET_CUSTOM_LAYOUT: {
+                    final List<CommandButton> layout = MediaUtils2.fromCommandButtonParcelableArray(
+                            extras.getParcelableArray(ARGUMENT_COMMAND_BUTTONS));
+                    if (layout == null) {
+                        return;
+                    }
+                    mCallbackExecutor.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            mCallback.onCustomLayoutChanged(mInstance, layout);
+                        }
+                    });
+                    break;
+                }
+                case SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED: {
+                    final PlaybackInfo info = PlaybackInfo.fromBundle(
+                            extras.getBundle(ARGUMENT_PLAYBACK_INFO));
+                    if (info == null) {
+                        return;
+                    }
+                    synchronized (mLock) {
+                        mPlaybackInfo = info;
+                    }
+                    mCallbackExecutor.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            mCallback.onPlaybackInfoChanged(mInstance, info);
+                        }
+                    });
+                    break;
+                }
+                case SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED: {
+                    final PlaybackStateCompat state =
+                            extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT);
+                    if (state == null) {
+                        return;
+                    }
+                    synchronized (mLock) {
+                        mPlaybackStateCompat = state;
+                    }
+                    mCallbackExecutor.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            mCallback.onPlaybackSpeedChanged(mInstance, state.getPlaybackSpeed());
+                        }
+                    });
+                    break;
+                }
+                case SESSION_EVENT_ON_BUFFERING_STATE_CHANGED: {
+                    final MediaItem2 item = MediaItem2.fromBundle(
+                            extras.getBundle(ARGUMENT_MEDIA_ITEM));
+                    final int bufferingState = extras.getInt(ARGUMENT_BUFFERING_STATE);
+                    PlaybackStateCompat state =
+                            extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT);
+                    if (item == null || state == null) {
+                        return;
+                    }
+                    synchronized (mLock) {
+                        mBufferingState = bufferingState;
+                        mPlaybackStateCompat = state;
+                    }
+                    mCallbackExecutor.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            mCallback.onBufferingStateChanged(mInstance, item, bufferingState);
+                        }
+                    });
+                    break;
+                }
+                case SESSION_EVENT_ON_SEEK_COMPLETED: {
+                    final long position = extras.getLong(ARGUMENT_SEEK_POSITION);
+                    PlaybackStateCompat state =
+                            extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT);
+                    if (state == null) {
+                        return;
+                    }
+                    synchronized (mLock) {
+                        mPlaybackStateCompat = state;
+                    }
+                    mCallbackExecutor.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            mCallback.onSeekCompleted(mInstance, position);
+                        }
+                    });
+                    break;
+                }
+                case SESSION_EVENT_ON_CHILDREN_CHANGED: {
+                    String parentId = extras.getString(ARGUMENT_MEDIA_ID);
+                    if (parentId == null || !(mInstance instanceof MediaBrowser2)) {
+                        return;
+                    }
+                    int itemCount = extras.getInt(ARGUMENT_ITEM_COUNT, -1);
+                    Bundle childrenExtras = extras.getBundle(ARGUMENT_EXTRAS);
+                    ((MediaBrowser2.BrowserCallback) mCallback).onChildrenChanged(
+                            (MediaBrowser2) mInstance, parentId, itemCount, childrenExtras);
+                    break;
+                }
+                case SESSION_EVENT_ON_SEARCH_RESULT_CHANGED: {
+                    final String query = extras.getString(ARGUMENT_QUERY);
+                    if (query == null || !(mInstance instanceof MediaBrowser2)) {
+                        return;
+                    }
+                    final int itemCount = extras.getInt(ARGUMENT_ITEM_COUNT, -1);
+                    final Bundle searchExtras = extras.getBundle(ARGUMENT_EXTRAS);
+                    mCallbackExecutor.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            ((MediaBrowser2.BrowserCallback) mCallback).onSearchResultChanged(
+                                    (MediaBrowser2) mInstance, query, itemCount, searchExtras);
+                        }
+                    });
+                    break;
+                }
+            }
+        }
+    }
+}
diff --git a/media/src/main/java/androidx/media/MediaLibraryService2.java b/media/src/main/java/androidx/media/MediaLibraryService2.java
index edd97c3..9e3bfda 100644
--- a/media/src/main/java/androidx/media/MediaLibraryService2.java
+++ b/media/src/main/java/androidx/media/MediaLibraryService2.java
@@ -19,28 +19,27 @@
 import static android.support.v4.media.MediaBrowserCompat.EXTRA_PAGE;
 import static android.support.v4.media.MediaBrowserCompat.EXTRA_PAGE_SIZE;
 
-import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
+import static androidx.media.MediaConstants2.ARGUMENT_PAGE;
+import static androidx.media.MediaConstants2.ARGUMENT_PAGE_SIZE;
 
 import android.app.PendingIntent;
 import android.content.Intent;
+import android.os.BadParcelableException;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.support.v4.media.MediaBrowserCompat.MediaItem;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
 import androidx.media.MediaLibraryService2.MediaLibrarySession.Builder;
 import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
 import androidx.media.MediaSession2.ControllerInfo;
 
-import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 
 /**
- * @hide
  * Base class for media library services.
  * <p>
  * Media library services enable applications to browse media content provided by an application
@@ -62,7 +61,6 @@
  *
  * @see MediaSessionService2
  */
-@RestrictTo(LIBRARY_GROUP)
 public abstract class MediaLibraryService2 extends MediaSessionService2 {
     /**
      * This is the interface name that a service implementing a session service should say that it
@@ -70,8 +68,6 @@
      */
     public static final String SERVICE_INTERFACE = "android.media.MediaLibraryService2";
 
-    // TODO: Revisit this value.
-
     /**
      * Session for the {@link MediaLibraryService2}. Build this object with
      * {@link Builder} and return in {@link #onCreateSession(String)}.
@@ -170,7 +166,6 @@
              * @param parentId parent id
              * @see SessionCommand2#COMMAND_CODE_LIBRARY_UNSUBSCRIBE
              */
-            // TODO: Make this to be called.
             public void onUnsubscribe(@NonNull MediaLibrarySession session,
                     @NonNull ControllerInfo controller, @NonNull String parentId) {
             }
@@ -217,6 +212,8 @@
          */
         // Override all methods just to show them with the type instead of generics in Javadoc.
         // This workarounds javadoc issue described in the MediaSession2.BuilderBase.
+        // Note: Don't override #setSessionCallback() because the callback can be set by the
+        // constructor.
         public static final class Builder extends MediaSession2.BuilderBase<MediaLibrarySession,
                 Builder, MediaLibrarySessionCallback> {
             private MediaLibrarySessionImplBase.Builder mImpl;
@@ -235,7 +232,7 @@
             }
 
             @Override
-            public @NonNull Builder setPlayer(@NonNull MediaPlayerBase player) {
+            public @NonNull Builder setPlayer(@NonNull MediaPlayerInterface player) {
                 return super.setPlayer(player);
             }
 
@@ -261,12 +258,6 @@
             }
 
             @Override
-            public @NonNull Builder setSessionCallback(@NonNull Executor executor,
-                    @NonNull MediaLibrarySessionCallback callback) {
-                return super.setSessionCallback(executor, callback);
-            }
-
-            @Override
             public @NonNull MediaLibrarySession build() {
                 return super.build();
             }
@@ -291,9 +282,10 @@
          */
         public void notifyChildrenChanged(@NonNull ControllerInfo controller,
                 @NonNull String parentId, int itemCount, @Nullable Bundle extras) {
-            Bundle options = new Bundle(extras);
-            options.putInt(MediaBrowser2.EXTRA_ITEM_COUNT, itemCount);
-            options.putBundle(MediaBrowser2.EXTRA_TARGET, controller.toBundle());
+            List<MediaSessionManager.RemoteUserInfo> subscribingBrowsers =
+                    getServiceCompat().getSubscribingBrowsers(parentId);
+            getImpl().notifyChildrenChanged(controller, parentId, itemCount, extras,
+                    subscribingBrowsers);
         }
 
         /**
@@ -308,9 +300,11 @@
         // This is for the backward compatibility.
         public void notifyChildrenChanged(@NonNull String parentId, int itemCount,
                 @Nullable Bundle extras) {
-            Bundle options = new Bundle(extras);
-            options.putInt(MediaBrowser2.EXTRA_ITEM_COUNT, itemCount);
-            getServiceCompat().notifyChildrenChanged(parentId, options);
+            if (extras == null) {
+                getServiceCompat().notifyChildrenChanged(parentId);
+            } else {
+                getServiceCompat().notifyChildrenChanged(parentId, extras);
+            }
         }
 
         /**
@@ -322,8 +316,8 @@
          * @param extras extra bundle
          */
         public void notifySearchResultChanged(@NonNull ControllerInfo controller,
-                @NonNull String query, int itemCount, @NonNull Bundle extras) {
-            // TODO: Implement
+                @NonNull String query, int itemCount, @Nullable Bundle extras) {
+            getImpl().notifySearchResultChanged(controller, query, itemCount, extras);
         }
 
         private MediaLibraryService2 getService() {
@@ -494,10 +488,7 @@
                 // controller.
                 return sDefaultBrowserRoot;
             }
-            final CountDownLatch latch = new CountDownLatch(1);
-            // TODO: Revisit this when we support caller information.
-            final ControllerInfo info = new ControllerInfo(MediaLibraryService2.this, clientUid, -1,
-                    clientPackageName, null);
+            final ControllerInfo controller = getController();
             MediaLibrarySession session = getLibrarySession();
             // Call onGetLibraryRoot() directly instead of execute on the executor. Here's the
             // reason.
@@ -511,7 +502,7 @@
             // Because of the reason, just call onGetLibraryRoot directly here. onGetLibraryRoot()
             // has documentation that it may be called on the main thread.
             LibraryRoot libraryRoot = session.getCallback().onGetLibraryRoot(
-                    session, info, extras);
+                    session, controller, extras);
             if (libraryRoot == null) {
                 return null;
             }
@@ -526,36 +517,47 @@
         @Override
         public void onLoadChildren(final String parentId, final Result<List<MediaItem>> result,
                 final Bundle options) {
+            result.detach();
             final ControllerInfo controller = getController();
             getLibrarySession().getCallbackExecutor().execute(new Runnable() {
                 @Override
                 public void run() {
-                    int page = options.getInt(EXTRA_PAGE, -1);
-                    int pageSize = options.getInt(EXTRA_PAGE_SIZE, -1);
-                    if (page >= 0 && pageSize >= 0) {
-                        // Requesting the list of children through the pagenation.
-                        List<MediaItem2> children = getLibrarySession().getCallback().onGetChildren(
-                                getLibrarySession(), controller, parentId, page, pageSize, options);
-                        if (children == null) {
-                            result.sendError(null);
-                        } else {
-                            List<MediaItem> list = new ArrayList<>();
-                            for (int i = 0; i < children.size(); i++) {
-                                list.add(MediaUtils2.createMediaItem(children.get(i)));
+                    if (options != null) {
+                        options.setClassLoader(MediaLibraryService2.this.getClassLoader());
+                        try {
+                            int page = options.getInt(EXTRA_PAGE);
+                            int pageSize = options.getInt(EXTRA_PAGE_SIZE);
+                            if (page > 0 && pageSize > 0) {
+                                // Requesting the list of children through pagination.
+                                List<MediaItem2> children = getLibrarySession().getCallback()
+                                        .onGetChildren(getLibrarySession(), controller, parentId,
+                                                page, pageSize, options);
+                                result.sendResult(MediaUtils2.fromMediaItem2List(children));
+                                return;
+                            } else if (options.containsKey(
+                                    MediaBrowser2.MEDIA_BROWSER2_SUBSCRIBE)) {
+                                // This onLoadChildren() was triggered by MediaBrowser2.subscribe().
+                                options.remove(MediaBrowser2.MEDIA_BROWSER2_SUBSCRIBE);
+                                getLibrarySession().getCallback().onSubscribe(getLibrarySession(),
+                                        controller, parentId, options.getBundle(ARGUMENT_EXTRAS));
+                                return;
                             }
-                            result.sendResult(list);
+                        } catch (BadParcelableException e) {
+                            // pass-through.
                         }
-                    } else {
-                        // Only wants to register callbacks
-                        getLibrarySession().getCallback().onSubscribe(getLibrarySession(),
-                                controller, parentId, options);
                     }
+                    List<MediaItem2> children = getLibrarySession().getCallback()
+                            .onGetChildren(getLibrarySession(), controller, parentId,
+                                    1 /* page */, Integer.MAX_VALUE /* pageSize*/,
+                                    null /* extras */);
+                    result.sendResult(MediaUtils2.fromMediaItem2List(children));
                 }
             });
         }
 
         @Override
         public void onLoadItem(final String itemId, final Result<MediaItem> result) {
+            result.detach();
             final ControllerInfo controller = getController();
             getLibrarySession().getCallbackExecutor().execute(new Runnable() {
                 @Override
@@ -563,7 +565,7 @@
                     MediaItem2 item = getLibrarySession().getCallback().onGetItem(
                             getLibrarySession(), controller, itemId);
                     if (item == null) {
-                        result.sendError(null);
+                        result.sendResult(null);
                     } else {
                         result.sendResult(MediaUtils2.createMediaItem(item));
                     }
@@ -572,17 +574,65 @@
         }
 
         @Override
-        public void onSearch(String query, Bundle extras, Result<List<MediaItem>> result) {
-            // TODO: Implement
+        public void onSearch(final String query, final Bundle extras,
+                final Result<List<MediaItem>> result) {
+            result.detach();
+            final ControllerInfo controller = getController();
+            extras.setClassLoader(MediaLibraryService2.this.getClassLoader());
+            try {
+                final int page = extras.getInt(ARGUMENT_PAGE);
+                final int pageSize = extras.getInt(ARGUMENT_PAGE_SIZE);
+                if (!(page > 0 && pageSize > 0)) {
+                    getLibrarySession().getCallbackExecutor().execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            getLibrarySession().getCallback().onSearch(
+                                    getLibrarySession(), controller, query, extras);
+                        }
+                    });
+                } else {
+                    getLibrarySession().getCallbackExecutor().execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            List<MediaItem2> searchResult = getLibrarySession().getCallback()
+                                    .onGetSearchResult(getLibrarySession(), controller, query,
+                                            page, pageSize, extras);
+                            if (searchResult == null) {
+                                result.sendResult(null);
+                                return;
+                            }
+                            result.sendResult(MediaUtils2.fromMediaItem2List(searchResult));
+                        }
+                    });
+                }
+            } catch (BadParcelableException e) {
+                // Do nothing.
+            }
         }
 
         @Override
         public void onCustomAction(String action, Bundle extras, Result<Bundle> result) {
-            // TODO: Implement
+            // No-op. Library session will handle the custom action.
         }
 
         private ControllerInfo getController() {
-            // TODO: Implement, by using getBrowserRootHints() / getCurrentBrowserInfo() / ...
+            MediaLibrarySession session = getLibrarySession();
+            List<ControllerInfo> controllers = session.getConnectedControllers();
+
+            MediaSessionManager.RemoteUserInfo info = getCurrentBrowserInfo();
+            if (info == null) {
+                return null;
+            }
+
+            for (int i = 0; i < controllers.size(); i++) {
+                // Note: This cannot pick the right controller between two controllers in same
+                // process.
+                ControllerInfo controller = controllers.get(i);
+                if (controller.getPackageName().equals(info.getPackageName())
+                        && controller.getUid() == info.getUid()) {
+                    return controller;
+                }
+            }
             return null;
         }
     }
diff --git a/media/src/main/java/androidx/media/MediaLibrarySessionImplBase.java b/media/src/main/java/androidx/media/MediaLibrarySessionImplBase.java
index c21edd3..dfaa98c 100644
--- a/media/src/main/java/androidx/media/MediaLibrarySessionImplBase.java
+++ b/media/src/main/java/androidx/media/MediaLibrarySessionImplBase.java
@@ -31,7 +31,7 @@
 class MediaLibrarySessionImplBase extends MediaSession2ImplBase {
     MediaLibrarySessionImplBase(Context context,
             MediaSessionCompat sessionCompat, String id,
-            MediaPlayerBase player, MediaPlaylistAgent playlistAgent,
+            MediaPlayerInterface player, MediaPlaylistAgent playlistAgent,
             VolumeProviderCompat volumeProvider, PendingIntent sessionActivity,
             Executor callbackExecutor,
             MediaSession2.SessionCallback callback) {
@@ -39,6 +39,11 @@
                 callbackExecutor, callback);
     }
 
+    @Override
+    MediaSession2 createInstance() {
+        return new MediaLibrarySession(this);
+    }
+
     static final class Builder extends MediaSession2ImplBase.BuilderBase<
             MediaLibrarySession, MediaLibrarySession.MediaLibrarySessionCallback> {
         Builder(Context context) {
diff --git a/media/src/main/java/androidx/media/MediaPlayer2.java b/media/src/main/java/androidx/media/MediaPlayer2.java
index 1864d72..4edb946 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,27 +48,16 @@
 import java.util.UUID;
 import java.util.concurrent.Executor;
 
-
 /**
- * @hide
- * MediaPlayer2 class can be used to control playback
- * of audio/video files and streams. An example on how to use the methods in
- * this class can be found in {@link android.widget.VideoView}.
+ * MediaPlayer2 class can be used to control playback of audio/video files and streams.
  *
  * <p>Topics covered here are:
  * <ol>
  * <li><a href="#StateDiagram">State Diagram</a>
- * <li><a href="#Valid_and_Invalid_States">Valid and Invalid States</a>
  * <li><a href="#Permissions">Permissions</a>
  * <li><a href="#Callbacks">Register informational and error callbacks</a>
  * </ol>
  *
- * <div class="special reference">
- * <h3>Developer Guides</h3>
- * <p>For more information about how to use MediaPlayer2, read the
- * <a href="{@docRoot}guide/topics/media/mediaplayer.html">Media Playback</a> developer guide.</p>
- * </div>
- *
  * <a name="StateDiagram"></a>
  * <h3>State Diagram</h3>
  *
@@ -87,18 +77,18 @@
  * <p>From this state diagram, one can see that a MediaPlayer2 object has the
  *    following states:</p>
  * <ul>
- *     <li>When a MediaPlayer2 object is just created using <code>create</code> or
- *         after {@link #reset()} is called, it is in the <em>Idle</em> state; and after
- *         {@link #close()} is called, it is in the <em>End</em> state. Between these
- *         two states is the life cycle of the MediaPlayer2 object.
+ *     <li>When a MediaPlayer2 object is just created using {@link #create()} or
+ *         after {@link #reset()} is called, it is in the <strong>Idle</strong> state; and Once
+ *         {@link #close()} is called, it can no longer be used and there is no way to bring it
+ *         back to any other state.
  *         <ul>
- *         <li> It is a programming error to invoke methods such
- *         as {@link #getCurrentPosition()},
- *         {@link #getDuration()}, {@link #getVideoHeight()},
- *         {@link #getVideoWidth()}, {@link #setAudioAttributes(AudioAttributes)},
- *         {@link #setPlayerVolume(float)}, {@link #pause()}, {@link #play()},
- *         {@link #seekTo(long, int)} or
- *         {@link #prepare()} in the <em>Idle</em> state.
+ *         <li>Calling {@link #setDataSource(DataSourceDesc)} and {@link #prepare()} transfers a
+ *         MediaPlayer2 object in the <strong>Idle</strong> state to the <strong>Paused</strong>
+ *         state. It is good programming practice to register a event callback for
+ *         {@link MediaPlayer2EventCallback#onCallCompleted} and
+ *         look out for {@link #CALL_STATUS_BAD_VALUE} and {@link #CALL_STATUS_ERROR_IO} that may be
+ *         caused from {@link #setDataSource}.
+ *         </li>
  *         <li>It is also recommended that once
  *         a MediaPlayer2 object is no longer being used, call {@link #close()} immediately
  *         so that resources used by the internal player engine associated with the
@@ -106,13 +96,7 @@
  *         singleton resources such as hardware acceleration components and
  *         failure to call {@link #close()} may cause subsequent instances of
  *         MediaPlayer2 objects to fallback to software implementations or fail
- *         altogether. Once the MediaPlayer2
- *         object is in the <em>End</em> state, it can no longer be used and
- *         there is no way to bring it back to any other state. </li>
- *         <li>Furthermore,
- *         the MediaPlayer2 objects created using <code>new</code> is in the
- *         <em>Idle</em> state.
- *         </li>
+ *         altogether. </li>
  *         </ul>
  *         </li>
  *     <li>In general, some playback control operation may fail due to various
@@ -121,71 +105,55 @@
  *         Thus, error reporting and recovery is an important concern under
  *         these circumstances. Sometimes, due to programming errors, invoking a playback
  *         control operation in an invalid state may also occur. Under all these
- *         error conditions, the internal player engine invokes a user supplied
- *         MediaPlayer2EventCallback.onError() method if an MediaPlayer2EventCallback has been
- *         registered beforehand via
- *         {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.
+ *         error conditions, the player goes to <strong>Error</strong> state and invokes a user
+ *         supplied {@link MediaPlayer2EventCallback#onError}} method if an event callback has been
+ *         registered beforehand via {@link #setMediaPlayer2EventCallback}.
  *         <ul>
  *         <li>It is important to note that once an error occurs, the
- *         MediaPlayer2 object enters the <em>Error</em> state (except as noted
- *         above), even if an error listener has not been registered by the application.</li>
- *         <li>In order to reuse a MediaPlayer2 object that is in the <em>
- *         Error</em> state and recover from the error,
- *         {@link #reset()} can be called to restore the object to its <em>Idle</em>
+ *         MediaPlayer2 object enters the <strong>Error</strong> state (except as noted
+ *         above), even if a callback has not been registered by the application.</li>
+ *         <li>In order to reuse a MediaPlayer2 object that is in the <strong>
+ *         Error</strong> state and recover from the error,
+ *         {@link #reset()} can be called to restore the object to its <strong>Idle</strong>
  *         state.</li>
  *         <li>It is good programming practice to have your application
- *         register a OnErrorListener to look out for error notifications from
+ *         register a {@link MediaPlayer2EventCallback} to look out for error callbacks from
  *         the internal player engine.</li>
- *         <li>IllegalStateException is
- *         thrown to prevent programming errors such as calling
- *         {@link #prepare()}, {@link #setDataSource(DataSourceDesc)}
- *         methods in an invalid state. </li>
+ *         <li> {@link MediaPlayer2EventCallback#onCallCompleted} is called with
+ *         {@link #CALL_STATUS_INVALID_OPERATION} on programming errors such as calling
+ *         {@link #prepare()} and {@link #setDataSource(DataSourceDesc)} methods in an invalid
+ *         state. </li>
  *         </ul>
  *         </li>
- *     <li>Calling
- *         {@link #setDataSource(DataSourceDesc)} transfers a
- *         MediaPlayer2 object in the <em>Idle</em> state to the
- *         <em>Initialized</em> state.
- *         <ul>
- *         <li>An IllegalStateException is thrown if
- *         setDataSource() is called in any other state.</li>
- *         <li>It is good programming
- *         practice to always look out for <code>IllegalArgumentException</code>
- *         and <code>IOException</code> that may be thrown from
- *         <code>setDataSource</code>.</li>
- *         </ul>
- *         </li>
- *     <li>A MediaPlayer2 object must first enter the <em>Prepared</em> state
+ *     <li>A MediaPlayer2 object must first enter the <strong>Paused</strong> state
  *         before playback can be started.
  *         <ul>
- *         <li>There are an asynchronous way that the <em>Prepared</em> state can be reached:
- *         a call to {@link #prepare()} (asynchronous) which
- *         first transfers the object to the <em>Preparing</em> state after the
- *         call returns (which occurs almost right way) while the internal
- *         player engine continues working on the rest of preparation work
- *         until the preparation work completes. When the preparation completes,
+ *         <li>The <strong>Paused</strong> state can be reached by calling {@link #prepare()}. Note
+ *         that {@link #prepare()} is asynchronous. When the preparation completes,
  *         the internal player engine then calls a user supplied callback method,
- *         onInfo() of the MediaPlayer2EventCallback interface with {@link #MEDIA_INFO_PREPARED},
- *         if an MediaPlayer2EventCallback is registered beforehand via
+ *         {@link MediaPlayer2EventCallback#onInfo} interface with {@link #MEDIA_INFO_PREPARED},
+ *         if a MediaPlayer2EventCallback is registered beforehand via
  *         {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.</li>
- *         <li>It is important to note that
- *         the <em>Preparing</em> state is a transient state, and the behavior
- *         of calling any method with side effect while a MediaPlayer2 object is
- *         in the <em>Preparing</em> state is undefined.</li>
- *         <li>An IllegalStateException is
- *         thrown if {@link #prepare()} is called in
- *         any other state.</li>
- *         <li>While in the <em>Prepared</em> state, properties
- *         such as audio/sound volume, screenOnWhilePlaying, looping can be
- *         adjusted by invoking the corresponding set methods.</li>
+ *         <li>The player also goes to <strong>Paused</strong> state when {@link #pause()} is called
+ *         to pause the ongoing playback. Note that {@link #pause()} is asynchronous. Once
+ *         {@link #pause()} is processed successfully by the internal media engine,
+ *         <strong>Paused</strong> state will be notified with
+ *         {@link MediaPlayerInterface.PlayerEventCallback#onPlayerStateChanged} callback.
+ *         In addition to the callback, {@link #getMediaPlayer2State()} can also be used to test
+ *         whether the MediaPlayer2 object is in the <strong>Paused</strong> state.
+ *         </li>
+ *         <li>While in the <em>Paused</em> state, properties such as audio/sound volume, looping
+ *         can be adjusted by invoking the corresponding set methods.</li>
  *         </ul>
  *         </li>
- *     <li>To start the playback, {@link #play()} must be called. After
- *         {@link #play()} returns successfully, the MediaPlayer2 object is in the
- *         <em>Started</em> state. {@link #getPlayerState()} can be called to test
- *         whether the MediaPlayer2 object is in the <em>Started</em> state.
+ *     <li>To start the playback, {@link #play()} must be called. Once {@link #play()} is processed
+ *         successfully by the internal media engine, <strong>Playing</strong> state will be
+ *         notified with {@link MediaPlayerInterface.PlayerEventCallback#onPlayerStateChanged}
+ *         callback.
+ *         In addition to the callback, {@link #getMediaPlayer2State()} can be called to test
+ *         whether the MediaPlayer2 object is in the <strong>Started</strong> state.
  *         <ul>
- *         <li>While in the <em>Started</em> state, the internal player engine calls
+ *         <li>While in the <strong>Playing</strong> state, the internal player engine calls
  *         a user supplied callback method MediaPlayer2EventCallback.onInfo() with
  *         {@link #MEDIA_INFO_BUFFERING_UPDATE} if an MediaPlayer2EventCallback has been
  *         registered beforehand via
@@ -193,42 +161,21 @@
  *         This callback allows applications to keep track of the buffering status
  *         while streaming audio/video.</li>
  *         <li>Calling {@link #play()} has not effect
- *         on a MediaPlayer2 object that is already in the <em>Started</em> state.</li>
+ *         on a MediaPlayer2 object that is already in the <strong>Playing</strong> state.</li>
  *         </ul>
  *         </li>
- *     <li>Playback can be paused and stopped, and the current playback position
- *         can be adjusted. Playback can be paused via {@link #pause()}. When the call to
- *         {@link #pause()} returns, the MediaPlayer2 object enters the
- *         <em>Paused</em> state. Note that the transition from the <em>Started</em>
- *         state to the <em>Paused</em> state and vice versa happens
- *         asynchronously in the player engine. It may take some time before
- *         the state is updated in calls to {@link #getPlayerState()}, and it can be
- *         a number of seconds in the case of streamed content.
+ *     <li>The playback position can be adjusted with a call to {@link #seekTo}.
  *         <ul>
- *         <li>Calling {@link #play()} to resume playback for a paused
- *         MediaPlayer2 object, and the resumed playback
- *         position is the same as where it was paused. When the call to
- *         {@link #play()} returns, the paused MediaPlayer2 object goes back to
- *         the <em>Started</em> state.</li>
- *         <li>Calling {@link #pause()} has no effect on
- *         a MediaPlayer2 object that is already in the <em>Paused</em> state.</li>
- *         </ul>
- *         </li>
- *     <li>The playback position can be adjusted with a call to
- *         {@link #seekTo(long, int)}.
- *         <ul>
- *         <li>Although the asynchronuous {@link #seekTo(long, int)}
- *         call returns right away, the actual seek operation may take a while to
+ *         <li>Although the asynchronous {@link #seekTo} call returns right away,
+ *         the actual seek operation may take a while to
  *         finish, especially for audio/video being streamed. When the actual
  *         seek operation completes, the internal player engine calls a user
  *         supplied MediaPlayer2EventCallback.onCallCompleted() with
  *         {@link #CALL_COMPLETED_SEEK_TO}
  *         if an MediaPlayer2EventCallback has been registered beforehand via
  *         {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.</li>
- *         <li>Please
- *         note that {@link #seekTo(long, int)} can also be called in the other states,
- *         such as <em>Prepared</em>, <em>Paused</em> and <em>PlaybackCompleted
- *         </em> state. When {@link #seekTo(long, int)} is called in those states,
+ *         <li>Please note that {@link #seekTo(long, int)} can also be called in
+ *         <strong>Paused</strong> state. When {@link #seekTo(long, int)} is called in those states,
  *         one video frame will be displayed if the stream has video and the requested
  *         position is valid.
  *         </li>
@@ -241,206 +188,20 @@
  *     <li>When the playback reaches the end of stream, the playback completes.
  *         <ul>
  *         <li>If current source is set to loop by {@link #loopCurrent(boolean)},
- *         the MediaPlayer2 object shall remain in the <em>Started</em> state.</li>
+ *         the MediaPlayer2 object shall remain in the <strong>Playing</strong> state.</li>
  *         <li>If the looping mode was set to <var>false
  *         </var>, the player engine calls a user supplied callback method,
- *         MediaPlayer2EventCallback.onCompletion(), if an MediaPlayer2EventCallback is
- *         registered beforehand via
+ *         {@link MediaPlayer2EventCallback#onInfo} with {@link #MEDIA_INFO_PLAYBACK_COMPLETE},
+ *         if an MediaPlayer2EventCallback is registered beforehand via
  *         {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.
- *         The invoke of the callback signals that the object is now in the <em>
- *         PlaybackCompleted</em> state.</li>
- *         <li>While in the <em>PlaybackCompleted</em>
- *         state, calling {@link #play()} can restart the playback from the
- *         beginning of the audio/video source.</li>
+ *         The invoke of the callback signals that the object is now in the <strong>Paused</strong>
+ *         state.</li>
+ *         <li>While in the <strong>Paused</strong> state, calling {@link #play()} can restart the
+ *         playback from the beginning of the audio/video source.</li>
  * </ul>
  *
- *
- * <a name="Valid_and_Invalid_States"></a>
- * <h3>Valid and invalid states</h3>
- *
- * <table border="0" cellspacing="0" cellpadding="0">
- * <tr><td>Method Name </p></td>
- *     <td>Valid Sates </p></td>
- *     <td>Invalid States </p></td>
- *     <td>Comments </p></td></tr>
- * <tr><td>attachAuxEffect </p></td>
- *     <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
- *     <td>{Idle, Error} </p></td>
- *     <td>This method must be called after setDataSource.
- *     Calling it does not change the object state. </p></td></tr>
- * <tr><td>getAudioSessionId </p></td>
- *     <td>any </p></td>
- *     <td>{} </p></td>
- *     <td>This method can be called in any state and calling it does not change
- *         the object state. </p></td></tr>
- * <tr><td>getCurrentPosition </p></td>
- *     <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
- *         PlaybackCompleted} </p></td>
- *     <td>{Error}</p></td>
- *     <td>Successful invoke of this method in a valid state does not change the
- *         state. Calling this method in an invalid state transfers the object
- *         to the <em>Error</em> state. </p></td></tr>
- * <tr><td>getDuration </p></td>
- *     <td>{Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
- *     <td>{Idle, Initialized, Error} </p></td>
- *     <td>Successful invoke of this method in a valid state does not change the
- *         state. Calling this method in an invalid state transfers the object
- *         to the <em>Error</em> state. </p></td></tr>
- * <tr><td>getVideoHeight </p></td>
- *     <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
- *         PlaybackCompleted}</p></td>
- *     <td>{Error}</p></td>
- *     <td>Successful invoke of this method in a valid state does not change the
- *         state. Calling this method in an invalid state transfers the object
- *         to the <em>Error</em> state.  </p></td></tr>
- * <tr><td>getVideoWidth </p></td>
- *     <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
- *         PlaybackCompleted}</p></td>
- *     <td>{Error}</p></td>
- *     <td>Successful invoke of this method in a valid state does not change
- *         the state. Calling this method in an invalid state transfers the
- *         object to the <em>Error</em> state. </p></td></tr>
- * <tr><td>getPlayerState </p></td>
- *     <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
- *          PlaybackCompleted}</p></td>
- *     <td>{Error}</p></td>
- *     <td>Successful invoke of this method in a valid state does not change
- *         the state. Calling this method in an invalid state transfers the
- *         object to the <em>Error</em> state. </p></td></tr>
- * <tr><td>pause </p></td>
- *     <td>{Started, Paused, PlaybackCompleted}</p></td>
- *     <td>{Idle, Initialized, Prepared, Stopped, Error}</p></td>
- *     <td>Successful invoke of this method in a valid state transfers the
- *         object to the <em>Paused</em> state. Calling this method in an
- *         invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
- * <tr><td>prepare </p></td>
- *     <td>{Initialized, Stopped} </p></td>
- *     <td>{Idle, Prepared, Started, Paused, PlaybackCompleted, Error} </p></td>
- *     <td>Successful invoke of this method in a valid state transfers the
- *         object to the <em>Preparing</em> state. Calling this method in an
- *         invalid state throws an IllegalStateException.</p></td></tr>
- * <tr><td>release </p></td>
- *     <td>any </p></td>
- *     <td>{} </p></td>
- *     <td>After {@link #close()}, the object is no longer available. </p></td></tr>
- * <tr><td>reset </p></td>
- *     <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
- *         PlaybackCompleted, Error}</p></td>
- *     <td>{}</p></td>
- *     <td>After {@link #reset()}, the object is like being just created.</p></td></tr>
- * <tr><td>seekTo </p></td>
- *     <td>{Prepared, Started, Paused, PlaybackCompleted} </p></td>
- *     <td>{Idle, Initialized, Stopped, Error}</p></td>
- *     <td>Successful invoke of this method in a valid state does not change
- *         the state. Calling this method in an invalid state transfers the
- *         object to the <em>Error</em> state. </p></td></tr>
- * <tr><td>setAudioAttributes </p></td>
- *     <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
- *          PlaybackCompleted}</p></td>
- *     <td>{Error}</p></td>
- *     <td>Successful invoke of this method does not change the state. In order for the
- *         target audio attributes type to become effective, this method must be called before
- *         prepare().</p></td></tr>
- * <tr><td>setAudioSessionId </p></td>
- *     <td>{Idle} </p></td>
- *     <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
- *          Error} </p></td>
- *     <td>This method must be called in idle state as the audio session ID must be known before
- *         calling setDataSource. Calling it does not change the object
- *         state. </p></td></tr>
- * <tr><td>setAudioStreamType (deprecated)</p></td>
- *     <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
- *          PlaybackCompleted}</p></td>
- *     <td>{Error}</p></td>
- *     <td>Successful invoke of this method does not change the state. In order for the
- *         target audio stream type to become effective, this method must be called before
- *         prepare().</p></td></tr>
- * <tr><td>setAuxEffectSendLevel </p></td>
- *     <td>any</p></td>
- *     <td>{} </p></td>
- *     <td>Calling this method does not change the object state. </p></td></tr>
- * <tr><td>setDataSource </p></td>
- *     <td>{Idle} </p></td>
- *     <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
- *          Error} </p></td>
- *     <td>Successful invoke of this method in a valid state transfers the
- *         object to the <em>Initialized</em> state. Calling this method in an
- *         invalid state throws an IllegalStateException.</p></td></tr>
- * <tr><td>setDisplay </p></td>
- *     <td>any </p></td>
- *     <td>{} </p></td>
- *     <td>This method can be called in any state and calling it does not change
- *         the object state. </p></td></tr>
- * <tr><td>setSurface </p></td>
- *     <td>any </p></td>
- *     <td>{} </p></td>
- *     <td>This method can be called in any state and calling it does not change
- *         the object state. </p></td></tr>
- * <tr><td>loopCurrent </p></td>
- *     <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
- *         PlaybackCompleted}</p></td>
- *     <td>{Error}</p></td>
- *     <td>Successful invoke of this method in a valid state does not change
- *         the state. Calling this method in an
- *         invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
- * <tr><td>isLooping </p></td>
- *     <td>any </p></td>
- *     <td>{} </p></td>
- *     <td>This method can be called in any state and calling it does not change
- *         the object state. </p></td></tr>
- * <tr><td>setDrmEventCallback </p></td>
- *     <td>any </p></td>
- *     <td>{} </p></td>
- *     <td>This method can be called in any state and calling it does not change
- *         the object state. </p></td></tr>
- * <tr><td>setMediaPlayer2EventCallback </p></td>
- *     <td>any </p></td>
- *     <td>{} </p></td>
- *     <td>This method can be called in any state and calling it does not change
- *         the object state. </p></td></tr>
- * <tr><td>setPlaybackParams</p></td>
- *     <td>{Initialized, Prepared, Started, Paused, PlaybackCompleted, Error}</p></td>
- *     <td>{Idle, Stopped} </p></td>
- *     <td>This method will change state in some cases, depending on when it's called.
- *         </p></td></tr>
- * <tr><td>setPlayerVolume </p></td>
- *     <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
- *          PlaybackCompleted}</p></td>
- *     <td>{Error}</p></td>
- *     <td>Successful invoke of this method does not change the state.
- * <tr><td>play </p></td>
- *     <td>{Prepared, Started, Paused, PlaybackCompleted}</p></td>
- *     <td>{Idle, Initialized, Stopped, Error}</p></td>
- *     <td>Successful invoke of this method in a valid state transfers the
- *         object to the <em>Started</em> state. Calling this method in an
- *         invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
- * <tr><td>stop </p></td>
- *     <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
- *     <td>{Idle, Initialized, Error}</p></td>
- *     <td>Successful invoke of this method in a valid state transfers the
- *         object to the <em>Stopped</em> state. Calling this method in an
- *         invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
- * <tr><td>getTrackInfo </p></td>
- *     <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
- *     <td>{Idle, Initialized, Error}</p></td>
- *     <td>Successful invoke of this method does not change the state.</p></td></tr>
- * <tr><td>selectTrack </p></td>
- *     <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
- *     <td>{Idle, Initialized, Error}</p></td>
- *     <td>Successful invoke of this method does not change the state.</p></td></tr>
- * <tr><td>deselectTrack </p></td>
- *     <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
- *     <td>{Idle, Initialized, Error}</p></td>
- *     <td>Successful invoke of this method does not change the state.</p></td></tr>
- *
- * </table>
- *
  * <a name="Permissions"></a>
  * <h3>Permissions</h3>
- * <p>One may need to declare a corresponding WAKE_LOCK permission {@link
- * android.R.styleable#AndroidManifestUsesPermission &lt;uses-permission&gt;}
- * element.
- *
  * <p>This class requires the {@link android.Manifest.permission#INTERNET} permission
  * when used with network-based content.
  *
@@ -460,8 +221,7 @@
  *
  */
 @TargetApi(Build.VERSION_CODES.P)
-@RestrictTo(LIBRARY_GROUP)
-public abstract class MediaPlayer2 extends MediaPlayerBase {
+public abstract class MediaPlayer2 {
     /**
      * Create a MediaPlayer2 object.
      *
@@ -478,6 +238,12 @@
     public MediaPlayer2() { }
 
     /**
+     * Returns a {@link MediaPlayerInterface} implementation which runs based on
+     * this MediaPlayer2 instance.
+     */
+    public abstract MediaPlayerInterface getMediaPlayerInterface();
+
+    /**
      * Releases the resources held by this {@code MediaPlayer2} object.
      *
      * It is considered good practice to call this method when you're
@@ -495,13 +261,8 @@
      * of the same codec are supported, some performance degradation
      * may be expected when unnecessary multiple instances are used
      * at the same time.
-     *
-     * {@code close()} may be safely called after a prior {@code close()}.
-     * This class implements the Java {@code AutoCloseable} interface and
-     * may be used with try-with-resources.
      */
     // This is a synchronous call.
-    @Override
     public abstract void close();
 
     /**
@@ -513,7 +274,6 @@
      *
      */
     // This is an asynchronous call.
-    @Override
     public abstract void play();
 
     /**
@@ -524,21 +284,18 @@
      *
      */
     // This is an asynchronous call.
-    @Override
     public abstract void prepare();
 
     /**
      * Pauses playback. Call play() to resume.
      */
     // This is an asynchronous call.
-    @Override
     public abstract void pause();
 
     /**
      * Tries to play next data source if applicable.
      */
     // This is an asynchronous call.
-    @Override
     public abstract void skipToNext();
 
     /**
@@ -548,7 +305,6 @@
      * @param msec the offset in milliseconds from the start to seek to
      */
     // This is an asynchronous call.
-    @Override
     public void seekTo(long msec) {
         seekTo(msec, SEEK_PREVIOUS_SYNC /* mode */);
     }
@@ -558,7 +314,6 @@
      *
      * @return the current position in milliseconds
      */
-    @Override
     public abstract long getCurrentPosition();
 
     /**
@@ -567,7 +322,6 @@
      * @return the duration in milliseconds, if no duration is available
      *         (for example, if streaming live content), -1 is returned.
      */
-    @Override
     public abstract long getDuration();
 
     /**
@@ -579,25 +333,14 @@
      *
      * @return the current buffered media source position in milliseconds
      */
-    @Override
     public abstract long getBufferedPosition();
 
     /**
-     * Gets the current player state.
+     * Gets the current MediaPlayer2 state.
      *
-     * @return the current player state.
+     * @return the current MediaPlayer2 state.
      */
-    @Override
-    public abstract @PlayerState int getPlayerState();
-
-    /**
-     * Gets the current buffering state of the player.
-     * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
-     * buffered.
-     * @return the buffering state, one of the following:
-     */
-    @Override
-    public abstract @BuffState int getBufferingState();
+    public abstract @MediaPlayer2State int getMediaPlayer2State();
 
     /**
      * Sets the audio attributes for this MediaPlayer2.
@@ -607,14 +350,12 @@
      * @param attributes a non-null set of audio attributes
      */
     // This is an asynchronous call.
-    @Override
     public abstract void setAudioAttributes(@NonNull AudioAttributesCompat attributes);
 
     /**
      * Gets the audio attributes for this MediaPlayer2.
      * @return attributes a set of audio attributes
      */
-    @Override
     public abstract @Nullable AudioAttributesCompat getAudioAttributes();
 
     /**
@@ -623,7 +364,6 @@
      * @param dsd the descriptor of data source you want to play
      */
     // This is an asynchronous call.
-    @Override
     public abstract void setDataSource(@NonNull DataSourceDesc dsd);
 
     /**
@@ -633,7 +373,6 @@
      * @param dsd the descriptor of data source you want to play after current one
      */
     // This is an asynchronous call.
-    @Override
     public abstract void setNextDataSource(@NonNull DataSourceDesc dsd);
 
     /**
@@ -642,7 +381,6 @@
      * @param dsds the list of data sources you want to play after current one
      */
     // This is an asynchronous call.
-    @Override
     public abstract void setNextDataSources(@NonNull List<DataSourceDesc> dsds);
 
     /**
@@ -650,7 +388,6 @@
      *
      * @return the current DataSourceDesc
      */
-    @Override
     public abstract @NonNull DataSourceDesc getCurrentDataSource();
 
     /**
@@ -658,7 +395,6 @@
      * @param loop true if the current data source is meant to loop.
      */
     // This is an asynchronous call.
-    @Override
     public abstract void loopCurrent(boolean loop);
 
     /**
@@ -671,7 +407,6 @@
      * @param speed the desired playback speed
      */
     // This is an asynchronous call.
-    @Override
     public abstract void setPlaybackSpeed(float speed);
 
     /**
@@ -679,7 +414,6 @@
      * Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}.
      * @return the actual playback speed
      */
-    @Override
     public float getPlaybackSpeed() {
         return 1.0f;
     }
@@ -690,7 +424,6 @@
      * {@link #setPlaybackSpeed(float)}.
      * @return true if reverse playback is supported.
      */
-    @Override
     public boolean isReversePlaybackSupported() {
         return false;
     }
@@ -705,7 +438,6 @@
      * @param volume a value between 0.0f and {@link #getMaxPlayerVolume()}.
      */
     // This is an asynchronous call.
-    @Override
     public abstract void setPlayerVolume(float volume);
 
     /**
@@ -713,36 +445,16 @@
      * Note that it does not take into account the associated stream volume.
      * @return the player volume.
      */
-    @Override
     public abstract float getPlayerVolume();
 
     /**
      * @return the maximum volume that can be used in {@link #setPlayerVolume(float)}.
      */
-    @Override
     public float getMaxPlayerVolume() {
         return 1.0f;
     }
 
     /**
-     * Adds a callback to be notified of events for this player.
-     * @param e the {@link Executor} to be used for the events.
-     * @param cb the callback to receive the events.
-     */
-    // This is a synchronous call.
-    @Override
-    public abstract void registerPlayerEventCallback(@NonNull Executor e,
-                                                     @NonNull PlayerEventCallback cb);
-
-    /**
-     * Removes a previously registered callback for player events
-     * @param cb the callback to remove
-     */
-    // This is a synchronous call.
-    @Override
-    public abstract void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb);
-
-    /**
      * Insert a task in the command queue to help the client to identify whether a batch
      * of commands has been finished. When this command is processed, a notification
      * {@code MediaPlayer2EventCallback.onCommandLabelReached} will be fired with the
@@ -832,10 +544,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();
 
     /**
@@ -930,7 +639,8 @@
     /**
      * Moves the media to specified time position by considering the given mode.
      * <p>
-     * When seekTo is finished, the user will be notified via OnSeekComplete supplied by the user.
+     * When seekTo is finished, the user will be notified via
+     * {@link MediaPlayer2EventCallback#onInfo} with {@link #CALL_COMPLETED_SEEK_TO}.
      * There is at most one active seekTo processed at any time. If there is a to-be-completed
      * seekTo, new seekTo requests will be queued in such a way that only the last request
      * is kept. When current seekTo is completed, the queued request will be processed if
@@ -948,7 +658,7 @@
     public abstract void seekTo(long msec, @SeekMode int mode);
 
     /**
-     * Get current playback position as a {@link MediaTimestamp}.
+     * Gets current playback position as a {@link MediaTimestamp}.
      * <p>
      * The MediaTimestamp represents how the media time correlates to the system time in
      * a linear fashion using an anchor and a clock rate. During regular playback, the media
@@ -974,7 +684,6 @@
      * data source and calling prepare().
      */
     // This is a synchronous call.
-    @Override
     public abstract void reset();
 
     /**
@@ -1109,8 +818,9 @@
     /**
      * Selects a track.
      * <p>
-     * If a MediaPlayer2 is in invalid state, it throws an IllegalStateException exception.
-     * If a MediaPlayer2 is in <em>Started</em> state, the selected track is presented immediately.
+     * If a MediaPlayer2 is in invalid state, {@link #CALL_STATUS_INVALID_OPERATION} will be
+     * reported with {@link MediaPlayer2EventCallback#onCallCompleted}.
+     * If a MediaPlayer2 is in <em>Playing</em> state, the selected track is presented immediately.
      * If a MediaPlayer2 is not in Started state, it just marks the track to be played.
      * </p>
      * <p>
@@ -1124,13 +834,10 @@
      * </p>
      * <p>
      * Currently, only timed text tracks or audio tracks can be selected via this method.
-     * In addition, the support for selecting an audio track at runtime is pretty limited
-     * in that an audio track can only be selected in the <em>Prepared</em> state.
      * </p>
      * @param index the index of the track to be selected. The valid range of the index
      * is 0..total number of track - 1. The total number of tracks as well as the type of
      * each individual track can be found by calling {@link #getTrackInfo()} method.
-     * @throws IllegalStateException if called in an invalid state.
      *
      * @see MediaPlayer2#getTrackInfo
      */
@@ -1138,7 +845,7 @@
     public abstract void selectTrack(int index);
 
     /**
-     * Deselect a track.
+     * Deselects a track.
      * <p>
      * Currently, the track must be a timed text track and no audio or video tracks can be
      * deselected. If the timed text track identified by index has not been
@@ -1147,7 +854,6 @@
      * @param index the index of the track to be deselected. The valid range of the index
      * is 0..total number of tracks - 1. The total number of tracks as well as the type of
      * each individual track can be found by calling {@link #getTrackInfo()} method.
-     * @throws IllegalStateException if called in an invalid state.
      *
      * @see MediaPlayer2#getTrackInfo
      */
@@ -1229,13 +935,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 +966,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) { }
     }
 
     /**
@@ -1271,6 +992,46 @@
     // This is a synchronous call.
     public abstract void clearMediaPlayer2EventCallback();
 
+    /**
+     * MediaPlayer2 has not been prepared or just has been reset.
+     * In this state, MediaPlayer2 doesn't fetch data.
+     */
+    public static final int MEDIAPLAYER2_STATE_IDLE = 1001;
+
+    /**
+     * MediaPlayer2 has been just prepared.
+     * In this state, MediaPlayer2 just fetches data from media source,
+     * but doesn't actively render data.
+     */
+    public static final int MEDIAPLAYER2_STATE_PREPARED = 1002;
+
+    /**
+     * MediaPlayer2 is paused.
+     * In this state, MediaPlayer2 doesn't actively render data.
+     */
+    public static final int MEDIAPLAYER2_STATE_PAUSED = 1003;
+
+    /**
+     * MediaPlayer2 is actively playing back data.
+     */
+    public static final int MEDIAPLAYER2_STATE_PLAYING = 1004;
+
+    /**
+     * MediaPlayer2 has hit some fatal error and cannot continue playback.
+     */
+    public static final int MEDIAPLAYER2_STATE_ERROR = 1005;
+
+    /** @hide */
+    @IntDef(flag = false, value = {
+            MEDIAPLAYER2_STATE_IDLE,
+            MEDIAPLAYER2_STATE_PREPARED,
+            MEDIAPLAYER2_STATE_PAUSED,
+            MEDIAPLAYER2_STATE_PLAYING,
+            MEDIAPLAYER2_STATE_ERROR })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    public @interface MediaPlayer2State {}
+
     /* Do not change these values without updating their counterparts
      * in include/media/mediaplayer2.h!
      */
@@ -1331,7 +1092,9 @@
     /** The player switched to this datas source because it is the
      * next-to-be-played in the playlist.
      * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     public static final int MEDIA_INFO_STARTED_AS_NEXT = 2;
 
     /** The player just pushed the very first video frame for rendering.
@@ -1512,16 +1275,6 @@
      */
     public static final int CALL_COMPLETED_PREPARE = 6;
 
-    /** The player just completed a call {@link #releaseDrm}.
-     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
-     */
-    public static final int CALL_COMPLETED_RELEASE_DRM = 12;
-
-    /** The player just completed a call {@link #restoreDrmKeys}.
-     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
-     */
-    public static final int CALL_COMPLETED_RESTORE_DRM_KEYS = 13;
-
     /** The player just completed a call {@link #seekTo}.
      * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
      */
@@ -1609,8 +1362,6 @@
             CALL_COMPLETED_PAUSE,
             CALL_COMPLETED_PLAY,
             CALL_COMPLETED_PREPARE,
-            CALL_COMPLETED_RELEASE_DRM,
-            CALL_COMPLETED_RESTORE_DRM_KEYS,
             CALL_COMPLETED_SEEK_TO,
             CALL_COMPLETED_SELECT_TRACK,
             CALL_COMPLETED_SET_AUDIO_ATTRIBUTES,
@@ -1661,12 +1412,6 @@
      */
     public static final int CALL_STATUS_ERROR_IO = 4;
 
-    /** Status code represents that DRM operation is called before preparing a DRM scheme through
-     *  {@link #prepareDrm}.
-     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
-     */
-    public static final int CALL_STATUS_NO_DRM_SCHEME = 5;
-
     /**
      * @hide
      */
@@ -1676,8 +1421,7 @@
             CALL_STATUS_INVALID_OPERATION,
             CALL_STATUS_BAD_VALUE,
             CALL_STATUS_PERMISSION_DENIED,
-            CALL_STATUS_ERROR_IO,
-            CALL_STATUS_NO_DRM_SCHEME})
+            CALL_STATUS_ERROR_IO})
     @Retention(RetentionPolicy.SOURCE)
     @RestrictTo(LIBRARY_GROUP)
     public @interface CallStatus {}
@@ -2008,4 +1752,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 3b3e119..3504876 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;
@@ -36,9 +37,7 @@
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Parcel;
-import android.os.Parcelable;
 import android.os.PersistableBundle;
-import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Pair;
 import android.view.Surface;
@@ -47,7 +46,11 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.collection.ArrayMap;
 import androidx.core.util.Preconditions;
+import androidx.media.MediaPlayerInterface.BuffState;
+import androidx.media.MediaPlayerInterface.PlayerEventCallback;
+import androidx.media.MediaPlayerInterface.PlayerState;
 
 import java.io.IOException;
 import java.nio.ByteOrder;
@@ -70,13 +73,15 @@
 
     private static final String TAG = "MediaPlayer2Impl";
 
-    private static final int NEXT_SOURCE_STATE_ERROR = -1;
-    private static final int NEXT_SOURCE_STATE_INIT = 0;
-    private static final int NEXT_SOURCE_STATE_PREPARING = 1;
-    private static final int NEXT_SOURCE_STATE_PREPARED = 2;
+    private static final int SOURCE_STATE_ERROR = -1;
+    private static final int SOURCE_STATE_INIT = 0;
+    private static final int SOURCE_STATE_PREPARING = 1;
+    private static final int SOURCE_STATE_PREPARED = 2;
 
     private static ArrayMap<Integer, Integer> sInfoEventMap;
     private static ArrayMap<Integer, Integer> sErrorEventMap;
+    private static ArrayMap<Integer, Integer> sPrepareDrmStatusMap;
+    private static ArrayMap<Integer, Integer> sStateMap;
 
     static {
         sInfoEventMap = new ArrayMap<>();
@@ -106,24 +111,29 @@
         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 = new ArrayMap<>();
+        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);
+
+        sStateMap = new ArrayMap<>();
+        sStateMap.put(MEDIAPLAYER2_STATE_IDLE, MediaPlayerInterface.PLAYER_STATE_IDLE);
+        sStateMap.put(MEDIAPLAYER2_STATE_PREPARED, MediaPlayerInterface.PLAYER_STATE_PAUSED);
+        sStateMap.put(MEDIAPLAYER2_STATE_PAUSED, MediaPlayerInterface.PLAYER_STATE_PAUSED);
+        sStateMap.put(MEDIAPLAYER2_STATE_PLAYING, MediaPlayerInterface.PLAYER_STATE_PLAYING);
+        sStateMap.put(MEDIAPLAYER2_STATE_ERROR, MediaPlayerInterface.PLAYER_STATE_ERROR);
     }
 
-    private MediaPlayer mPlayer;  // MediaPlayer is thread-safe.
-
-    private final Object mSrcLock = new Object();
-    //--- guarded by |mSrcLock| start
-    private long mSrcIdGenerator = 0;
-    private DataSourceDesc mCurrentDSD;
-    private long mCurrentSrcId = mSrcIdGenerator++;
-    private List<DataSourceDesc> mNextDSDs;
-    private long mNextSrcId = mSrcIdGenerator++;
-    private int mNextSourceState = NEXT_SOURCE_STATE_INIT;
-    private boolean mNextSourcePlayPending = false;
-    //--- guarded by |mSrcLock| end
-
-    private AtomicInteger mBufferedPercentageCurrent = new AtomicInteger(0);
-    private AtomicInteger mBufferedPercentageNext = new AtomicInteger(0);
-    private volatile float mVolume = 1.0f;
+    private MediaPlayerSourceQueue mPlayer;
 
     private HandlerThread mHandlerThread;
     private final Handler mTaskHandler;
@@ -135,8 +145,6 @@
 
     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<>();
@@ -144,8 +152,21 @@
             new ArrayMap<>();
     private ArrayList<Pair<Executor, DrmEventCallback>> mDrmEventCallbackRecords =
             new ArrayList<>();
+    private MediaPlayerInterfaceImpl mMediaPlayerInterfaceImpl;
     //--- guarded by |mLock| end
 
+    private void handleDataSourceError(final DataSourceError err) {
+        if (err == null) {
+            return;
+        }
+        notifyMediaPlayer2Event(new Mp2EventNotifier() {
+            @Override
+            public void notify(MediaPlayer2EventCallback callback) {
+                callback.onError(MediaPlayer2Impl.this, err.mDSD, err.mWhat, err.mExtra);
+            }
+        });
+    }
+
     /**
      * Default constructor.
      * <p>When done with the MediaPlayer2Impl, you should call  {@link #close()},
@@ -157,10 +178,24 @@
         mHandlerThread.start();
         Looper looper = mHandlerThread.getLooper();
         mTaskHandler = new Handler(looper);
-        mPlayer = new MediaPlayer();
-        mPlayerState = PLAYER_STATE_IDLE;
-        mBufferingState = BUFFERING_STATE_UNKNOWN;
-        setUpListeners();
+
+        // TODO: To make sure MediaPlayer1 listeners work, the caller thread should have a looper.
+        // Fix the framework or document this behavior.
+        mPlayer = new MediaPlayerSourceQueue();
+    }
+
+    /**
+     * Returns a {@link MediaPlayerInterface} implementation which runs based on
+     * this MediaPlayer2 instance.
+     */
+    @Override
+    public MediaPlayerInterface getMediaPlayerInterface() {
+        synchronized (mLock) {
+            if (mMediaPlayerInterfaceImpl == null) {
+                mMediaPlayerInterfaceImpl = new MediaPlayerInterfaceImpl();
+            }
+            return mMediaPlayerInterfaceImpl;
+        }
     }
 
     /**
@@ -204,8 +239,7 @@
         addTask(new Task(CALL_COMPLETED_PLAY, false) {
             @Override
             void process() {
-                mPlayer.start();
-                setPlayerState(PLAYER_STATE_PLAYING);
+                mPlayer.play();
             }
         });
     }
@@ -226,7 +260,6 @@
             @Override
             void process() throws IOException {
                 mPlayer.prepareAsync();
-                setBufferingState(BUFFERING_STATE_BUFFERING_AND_STARVED);
             }
         });
     }
@@ -242,7 +275,6 @@
             @Override
             void process() {
                 mPlayer.pause();
-                setPlayerState(PLAYER_STATE_PAUSED);
             }
         });
     }
@@ -257,7 +289,7 @@
         addTask(new Task(CALL_COMPLETED_SKIP_TO_NEXT, false) {
             @Override
             void process() {
-                // TODO: switch to next data source and play
+                mPlayer.skipToNext();
             }
         });
     }
@@ -295,14 +327,21 @@
     @Override
     public long getBufferedPosition() {
         // Use cached buffered percent for now.
-        return getDuration() * mBufferedPercentageCurrent.get() / 100;
+        return mPlayer.getBufferedPosition();
     }
 
+    /**
+     * Gets the current MediaPlayer2 state.
+     *
+     * @return the current MediaPlayer2 state.
+     */
     @Override
-    public @PlayerState int getPlayerState() {
-        synchronized (mLock) {
-            return mPlayerState;
-        }
+    public @MediaPlayer2State int getMediaPlayer2State() {
+        return mPlayer.getMediaPlayer2State();
+    }
+
+    private @MediaPlayerInterface.PlayerState int getPlayerState() {
+        return mPlayer.getPlayerState();
     }
 
     /**
@@ -310,11 +349,8 @@
      * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
      * buffered.
      */
-    @Override
-    public @BuffState int getBufferingState() {
-        synchronized (mLock) {
-            return mBufferingState;
-        }
+    private @MediaPlayerInterface.BuffState int getBufferingState() {
+        return  mPlayer.getBufferingState();
     }
 
     /**
@@ -361,13 +397,10 @@
             void process() {
                 Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
                 // TODO: setDataSource could update exist data source
-                synchronized (mSrcLock) {
-                    mCurrentDSD = dsd;
-                    mCurrentSrcId = mSrcIdGenerator++;
-                    try {
-                        handleDataSource(true /* isCurrent */, dsd, mCurrentSrcId);
-                    } catch (IOException e) {
-                    }
+                try {
+                    mPlayer.setFirst(dsd);
+                } catch (IOException e) {
+                    Log.e(TAG, "process: setDataSource", e);
                 }
             }
         });
@@ -387,21 +420,7 @@
             @Override
             void process() {
                 Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
-                synchronized (mSrcLock) {
-                    mNextDSDs = new ArrayList<DataSourceDesc>(1);
-                    mNextDSDs.add(dsd);
-                    mNextSrcId = mSrcIdGenerator++;
-                    mNextSourceState = NEXT_SOURCE_STATE_INIT;
-                    mNextSourcePlayPending = false;
-                }
-                /* FIXME : define and handle state.
-                int state = getMediaPlayer2State();
-                if (state != MEDIAPLAYER2_STATE_IDLE) {
-                    synchronized (mSrcLock) {
-                        prepareNextDataSource_l();
-                    }
-                }
-                */
+                handleDataSourceError(mPlayer.setNext(dsd));
             }
         });
     }
@@ -427,30 +446,14 @@
                                 "DataSourceDesc in the source list cannot be null.");
                     }
                 }
-
-                synchronized (mSrcLock) {
-                    mNextDSDs = new ArrayList(dsds);
-                    mNextSrcId = mSrcIdGenerator++;
-                    mNextSourceState = NEXT_SOURCE_STATE_INIT;
-                    mNextSourcePlayPending = false;
-                }
-                /* FIXME : define and handle state.
-                int state = getMediaPlayer2State();
-                if (state != MEDIAPLAYER2_STATE_IDLE) {
-                    synchronized (mSrcLock) {
-                        prepareNextDataSource_l();
-                    }
-                }
-                */
+                handleDataSourceError(mPlayer.setNextMultiple(dsds));
             }
         });
     }
 
     @Override
     public @NonNull DataSourceDesc getCurrentDataSource() {
-        synchronized (mSrcLock) {
-            return mCurrentDSD;
-        }
+        return mPlayer.getFirst().getDSD();
     }
 
     /**
@@ -521,8 +524,7 @@
         addTask(new Task(CALL_COMPLETED_SET_PLAYER_VOLUME, false) {
             @Override
             void process() {
-                mVolume = volume;
-                mPlayer.setVolume(volume, volume);
+                mPlayer.setVolume(volume);
             }
         });
     }
@@ -534,7 +536,7 @@
      */
     @Override
     public float getPlayerVolume() {
-        return mVolume;
+        return mPlayer.getVolume();
     }
 
     /**
@@ -550,8 +552,7 @@
      * @param e the {@link Executor} to be used for the events.
      * @param cb the callback to receive the events.
      */
-    @Override
-    public void registerPlayerEventCallback(@NonNull Executor e,
+    private void registerPlayerEventCallback(@NonNull Executor e,
             @NonNull PlayerEventCallback cb) {
         if (cb == null) {
             throw new IllegalArgumentException("Illegal null PlayerEventCallback");
@@ -569,8 +570,7 @@
      * Removes a previously registered callback for player events
      * @param cb the callback to remove
      */
-    @Override
-    public void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb) {
+    private void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb) {
         if (cb == null) {
             throw new IllegalArgumentException("Illegal null PlayerEventCallback");
         }
@@ -628,7 +628,9 @@
      */
     @Override
     public void clearPendingCommands() {
-        // TODO: implement this.
+        synchronized (mTaskLock) {
+            mPendingTasks.clear();
+        }
     }
 
     private void addTask(Task task) {
@@ -650,14 +652,15 @@
         }
     }
 
-    private void handleDataSource(boolean isCurrent, @NonNull final DataSourceDesc dsd, long srcId)
+    private static void handleDataSource(MediaPlayerSource src)
             throws IOException {
+        final DataSourceDesc dsd = src.getDSD();
         Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
 
-        // TODO: handle the case isCurrent is false.
+        MediaPlayer player = src.mPlayer;
         switch (dsd.getType()) {
             case DataSourceDesc.TYPE_CALLBACK:
-                mPlayer.setDataSource(new MediaDataSource() {
+                player.setDataSource(new MediaDataSource() {
                     Media2DataSource mDataSource = dsd.getMedia2DataSource();
                     @Override
                     public int readAt(long position, byte[] buffer, int offset, int size)
@@ -678,14 +681,14 @@
                 break;
 
             case DataSourceDesc.TYPE_FD:
-                mPlayer.setDataSource(
+                player.setDataSource(
                         dsd.getFileDescriptor(),
                         dsd.getFileDescriptorOffset(),
                         dsd.getFileDescriptorLength());
                 break;
 
             case DataSourceDesc.TYPE_URI:
-                mPlayer.setDataSource(
+                player.setDataSource(
                         dsd.getUriContext(),
                         dsd.getUri(),
                         dsd.getUriHeaders(),
@@ -871,9 +874,12 @@
     @Override
     public void reset() {
         mPlayer.reset();
-        setPlayerState(PLAYER_STATE_IDLE);
-        setBufferingState(BUFFERING_STATE_UNKNOWN);
-        /* FIXME: reset other internal variables. */
+        synchronized (mLock) {
+            mAudioAttributes = null;
+            mMp2EventCallbackRecords.clear();
+            mPlayerEventCallbackMap.clear();
+            mDrmEventCallbackRecords.clear();
+        }
     }
 
     /**
@@ -997,45 +1003,11 @@
             return null;
         }
 
-        TrackInfoImpl(Parcel in) {
-            mTrackType = in.readInt();
-            // TODO: parcel in the full MediaFormat; currently we are using createSubtitleFormat
-            // even for audio/video tracks, meaning we only set the mime and language.
-            String mime = in.readString();
-            String language = in.readString();
-            mFormat = MediaFormat.createSubtitleFormat(mime, language);
-
-            if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
-                mFormat.setInteger(MediaFormat.KEY_IS_AUTOSELECT, in.readInt());
-                mFormat.setInteger(MediaFormat.KEY_IS_DEFAULT, in.readInt());
-                mFormat.setInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, in.readInt());
-            }
-        }
-
         TrackInfoImpl(int type, MediaFormat format) {
             mTrackType = type;
             mFormat = format;
         }
 
-        /**
-         * Flatten this object in to a Parcel.
-         *
-         * @param dest The Parcel in which the object should be written.
-         * @param flags Additional flags about how the object should be written.
-         * May be 0 or {@link android.os.Parcelable#PARCELABLE_WRITE_RETURN_VALUE}.
-         */
-        /* package private */ void writeToParcel(Parcel dest, int flags) {
-            dest.writeInt(mTrackType);
-            dest.writeString(getLanguage());
-
-            if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
-                dest.writeString(mFormat.getString(MediaFormat.KEY_MIME));
-                dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_AUTOSELECT));
-                dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_DEFAULT));
-                dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE));
-            }
-        }
-
         @Override
         public String toString() {
             StringBuilder out = new StringBuilder(128);
@@ -1062,23 +1034,6 @@
             out.append("}");
             return out.toString();
         }
-
-        /**
-         * Used to read a TrackInfoImpl from a Parcel.
-         */
-        /* package private */ static final Parcelable.Creator<TrackInfoImpl> CREATOR =
-                new Parcelable.Creator<TrackInfoImpl>() {
-                    @Override
-                    public TrackInfoImpl createFromParcel(Parcel in) {
-                        return new TrackInfoImpl(in);
-                    }
-
-                    @Override
-                    public TrackInfoImpl[] newArray(int size) {
-                        return new TrackInfoImpl[size];
-                    }
-                };
-
     };
 
     /**
@@ -1230,8 +1185,9 @@
         mPlayer.setOnDrmConfigHelper(new MediaPlayer.OnDrmConfigHelper() {
             @Override
             public void onDrmConfig(MediaPlayer mp) {
-                /** FIXME: pass the right DSD. */
-                listener.onDrmConfig(MediaPlayer2Impl.this, null);
+                MediaPlayerSource src = mPlayer.getSourceForPlayer(mp);
+                DataSourceDesc dsd = src == null ? null : src.getDSD();
+                listener.onDrmConfig(MediaPlayer2Impl.this, dsd);
             }
         });
     }
@@ -1340,16 +1296,11 @@
      */
     @Override
     public void releaseDrm() throws NoDrmSchemeException {
-        addTask(new Task(CALL_COMPLETED_RELEASE_DRM, false) {
-            @Override
-            void process() throws NoDrmSchemeException {
-                try {
-                    mPlayer.releaseDrm();
-                } catch (MediaPlayer.NoDrmSchemeException e) {
-                    throw new NoDrmSchemeException(e.getMessage());
-                }
-            }
-        });
+        try {
+            mPlayer.releaseDrm();
+        } catch (MediaPlayer.NoDrmSchemeException e) {
+            throw new NoDrmSchemeException(e.getMessage());
+        }
     }
 
 
@@ -1443,16 +1394,11 @@
     @Override
     public void restoreDrmKeys(@NonNull final byte[] keySetId)
             throws NoDrmSchemeException {
-        addTask(new Task(CALL_COMPLETED_RESTORE_DRM_KEYS, false) {
-            @Override
-            void process() throws NoDrmSchemeException {
-                try {
-                    mPlayer.restoreKeys(keySetId);
-                } catch (MediaPlayer.NoDrmSchemeException e) {
-                    throw new NoDrmSchemeException(e.getMessage());
-                }
-            }
-        });
+        try {
+            mPlayer.restoreKeys(keySetId);
+        } catch (MediaPlayer.NoDrmSchemeException e) {
+            throw new NoDrmSchemeException(e.getMessage());
+        }
     }
 
 
@@ -1504,46 +1450,16 @@
     private void setPlaybackParamsInternal(final PlaybackParams params) {
         PlaybackParams current = mPlayer.getPlaybackParams();
         mPlayer.setPlaybackParams(params);
-        if (Math.abs(current.getSpeed() - params.getSpeed()) > 0.0001f) {
+        if (current.getSpeed() != params.getSpeed()) {
             notifyPlayerEvent(new PlayerEventNotifier() {
                 @Override
                 public void notify(PlayerEventCallback cb) {
-                    cb.onPlaybackSpeedChanged(MediaPlayer2Impl.this, params.getSpeed());
+                    cb.onPlaybackSpeedChanged(mMediaPlayerInterfaceImpl, params.getSpeed());
                 }
             });
         }
     }
 
-    private void setPlayerState(@PlayerState final int 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) {
-        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) {
@@ -1577,6 +1493,21 @@
         }
     }
 
+    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);
     }
@@ -1585,28 +1516,34 @@
         void notify(PlayerEventCallback callback);
     }
 
-    private void setUpListeners() {
-        mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+    private interface DrmEventNotifier {
+        void notify(DrmEventCallback callback);
+    }
+
+    private void setUpListeners(final MediaPlayerSource src) {
+        MediaPlayer p = src.mPlayer;
+        p.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
             @Override
             public void onPrepared(MediaPlayer mp) {
-                setPlayerState(PLAYER_STATE_PAUSED);
-                setBufferingState(BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
+                handleDataSourceError(mPlayer.onPrepared(mp));
                 notifyMediaPlayer2Event(new Mp2EventNotifier() {
                     @Override
                     public void notify(MediaPlayer2EventCallback callback) {
-                        callback.onInfo(MediaPlayer2Impl.this, mCurrentDSD, MEDIA_INFO_PREPARED, 0);
+                        MediaPlayer2Impl mp2 = MediaPlayer2Impl.this;
+                        DataSourceDesc dsd = src.getDSD();
+                        callback.onInfo(mp2, dsd, MEDIA_INFO_PREPARED, 0);
                     }
                 });
                 notifyPlayerEvent(new PlayerEventNotifier() {
                     @Override
                     public void notify(PlayerEventCallback cb) {
-                        cb.onMediaPrepared(MediaPlayer2Impl.this, mCurrentDSD);
+                        cb.onMediaPrepared(mMediaPlayerInterfaceImpl, src.getDSD());
                     }
                 });
                 synchronized (mTaskLock) {
                     if (mCurrentTask != null
                             && mCurrentTask.mMediaCallType == CALL_COMPLETED_PREPARE
-                            && mCurrentTask.mDSD == mCurrentDSD
+                            && mCurrentTask.mDSD == src.getDSD()
                             && mCurrentTask.mNeedToWaitForEventToComplete) {
                         mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
                         mCurrentTask = null;
@@ -1615,18 +1552,18 @@
                 }
             }
         });
-        mPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
+        p.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
             @Override
             public void onVideoSizeChanged(MediaPlayer mp, final int width, final int height) {
                 notifyMediaPlayer2Event(new Mp2EventNotifier() {
                     @Override
                     public void notify(MediaPlayer2EventCallback cb) {
-                        cb.onVideoSizeChanged(MediaPlayer2Impl.this, mCurrentDSD, width, height);
+                        cb.onVideoSizeChanged(MediaPlayer2Impl.this, src.getDSD(), width, height);
                     }
                 });
             }
         });
-        mPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() {
+        p.setOnInfoListener(new MediaPlayer.OnInfoListener() {
             @Override
             public boolean onInfo(MediaPlayer mp, int what, int extra) {
                 switch (what) {
@@ -1634,50 +1571,52 @@
                         notifyMediaPlayer2Event(new Mp2EventNotifier() {
                             @Override
                             public void notify(MediaPlayer2EventCallback cb) {
-                                cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
+                                cb.onInfo(MediaPlayer2Impl.this, src.getDSD(),
                                         MEDIA_INFO_VIDEO_RENDERING_START, 0);
                             }
                         });
                         break;
                     case MediaPlayer.MEDIA_INFO_BUFFERING_START:
-                        setBufferingState(BUFFERING_STATE_BUFFERING_AND_STARVED);
+                        mPlayer.setBufferingState(
+                                mp, MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_STARVED);
                         break;
                     case MediaPlayer.MEDIA_INFO_BUFFERING_END:
-                        setBufferingState(BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
+                        mPlayer.setBufferingState(
+                                mp, MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
                         break;
                 }
                 return false;
             }
         });
-        mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+        p.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
             @Override
             public void onCompletion(MediaPlayer mp) {
-                setPlayerState(PLAYER_STATE_PAUSED);
+                handleDataSourceError(mPlayer.onCompletion(mp));
                 notifyMediaPlayer2Event(new Mp2EventNotifier() {
                     @Override
                     public void notify(MediaPlayer2EventCallback cb) {
-                        cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD, MEDIA_INFO_PLAYBACK_COMPLETE,
-                                0);
+                        MediaPlayer2Impl mp2 = MediaPlayer2Impl.this;
+                        DataSourceDesc dsd = src.getDSD();
+                        cb.onInfo(mp2, dsd, MEDIA_INFO_PLAYBACK_COMPLETE, 0);
                     }
                 });
             }
         });
-        mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+        p.setOnErrorListener(new MediaPlayer.OnErrorListener() {
             @Override
             public boolean onError(MediaPlayer mp, final int what, final int extra) {
-                setPlayerState(PLAYER_STATE_ERROR);
-                setBufferingState(BUFFERING_STATE_UNKNOWN);
+                mPlayer.onError(mp);
                 notifyMediaPlayer2Event(new Mp2EventNotifier() {
                     @Override
                     public void notify(MediaPlayer2EventCallback cb) {
                         int w = sErrorEventMap.getOrDefault(what, MEDIA_ERROR_UNKNOWN);
-                        cb.onError(MediaPlayer2Impl.this, mCurrentDSD, w, extra);
+                        cb.onError(MediaPlayer2Impl.this, src.getDSD(), w, extra);
                     }
                 });
                 return true;
             }
         });
-        mPlayer.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
+        p.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
             @Override
             public void onSeekComplete(MediaPlayer mp) {
                 synchronized (mTaskLock) {
@@ -1695,12 +1634,12 @@
                     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);
+                        cb.onSeekCompleted(mMediaPlayerInterfaceImpl, seekPos);
                     }
                 });
             }
         });
-        mPlayer.setOnTimedMetaDataAvailableListener(
+        p.setOnTimedMetaDataAvailableListener(
                 new MediaPlayer.OnTimedMetaDataAvailableListener() {
                     @Override
                     public void onTimedMetaDataAvailable(MediaPlayer mp, final TimedMetaData data) {
@@ -1708,40 +1647,91 @@
                             @Override
                             public void notify(MediaPlayer2EventCallback cb) {
                                 cb.onTimedMetaDataAvailable(
-                                        MediaPlayer2Impl.this, mCurrentDSD, data);
+                                        MediaPlayer2Impl.this, src.getDSD(), data);
                             }
                         });
                     }
                 });
-        mPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() {
+        p.setOnInfoListener(new MediaPlayer.OnInfoListener() {
             @Override
             public boolean onInfo(MediaPlayer mp, final int what, final int 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);
+                        cb.onInfo(MediaPlayer2Impl.this, src.getDSD(), w, extra);
                     }
                 });
                 return true;
             }
         });
-        mPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
+        p.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
             @Override
             public void onBufferingUpdate(MediaPlayer mp, final int percent) {
                 if (percent >= 100) {
-                    setBufferingState(BUFFERING_STATE_BUFFERING_COMPLETE);
+                    mPlayer.setBufferingState(
+                            mp, MediaPlayerInterface.BUFFERING_STATE_BUFFERING_COMPLETE);
                 }
-                mBufferedPercentageCurrent.set(percent);
+                src.mBufferedPercentage.set(percent);
                 notifyMediaPlayer2Event(new Mp2EventNotifier() {
                     @Override
                     public void notify(MediaPlayer2EventCallback cb) {
-                        cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
+                        cb.onInfo(MediaPlayer2Impl.this, src.getDSD(),
                                 MEDIA_INFO_BUFFERING_UPDATE, percent);
                     }
                 });
             }
         });
+        p.setOnMediaTimeDiscontinuityListener(
+                new MediaPlayer.OnMediaTimeDiscontinuityListener() {
+                    @Override
+                    public void onMediaTimeDiscontinuity(
+                            MediaPlayer mp, final MediaTimestamp timestamp) {
+                        notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                            @Override
+                            public void notify(MediaPlayer2EventCallback cb) {
+                                cb.onMediaTimeDiscontinuity(
+                                        MediaPlayer2Impl.this, src.getDSD(), timestamp);
+                            }
+                        });
+                    }
+                });
+        p.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, src.getDSD(), data);
+                    }
+                });
+            }
+        });
+        p.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, src.getDSD(),
+                                new DrmInfoImpl(drmInfo.getPssh(), drmInfo.getSupportedSchemes()));
+                    }
+                });
+            }
+        });
+        p.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, src.getDSD(), s);
+                    }
+                });
+            }
+        });
     }
 
     /**
@@ -1944,14 +1934,10 @@
                 status = CALL_STATUS_PERMISSION_DENIED;
             } catch (IOException e) {
                 status = CALL_STATUS_ERROR_IO;
-            } catch (NoDrmSchemeException e) {
-                status = CALL_STATUS_NO_DRM_SCHEME;
             } catch (Exception e) {
                 status = CALL_STATUS_ERROR_UNKNOWN;
             }
-            synchronized (mSrcLock) {
-                mDSD = mCurrentDSD;
-            }
+            mDSD = getCurrentDataSource();
 
             if (!mNeedToWaitForEventToComplete || status != CALL_STATUS_NO_ERROR) {
 
@@ -1979,4 +1965,590 @@
             });
         }
     };
+
+    private static class DataSourceError {
+        final DataSourceDesc mDSD;
+        final int mWhat;
+
+        final int mExtra;
+        DataSourceError(DataSourceDesc dsd, int what, int extra) {
+            mDSD = dsd;
+            mWhat = what;
+            mExtra = extra;
+        }
+
+    }
+
+    private class MediaPlayerSource {
+
+        volatile DataSourceDesc mDSD;
+        final MediaPlayer mPlayer = new MediaPlayer();
+        final AtomicInteger mBufferedPercentage = new AtomicInteger(0);
+        int mSourceState = SOURCE_STATE_INIT;
+        @MediaPlayer2State int mMp2State = MEDIAPLAYER2_STATE_IDLE;
+        @BuffState int mBufferingState = MediaPlayerInterface.BUFFERING_STATE_UNKNOWN;
+        @PlayerState int mPlayerState = MediaPlayerInterface.PLAYER_STATE_IDLE;
+        boolean mPlayPending;
+
+        MediaPlayerSource(final DataSourceDesc dsd) {
+            mDSD = dsd;
+            setUpListeners(this);
+        }
+
+        DataSourceDesc getDSD() {
+            return mDSD;
+        }
+
+    }
+
+    private class MediaPlayerSourceQueue {
+
+        List<MediaPlayerSource> mQueue = new ArrayList<>();
+        float mVolume = 1.0f;
+        Surface mSurface;
+
+        MediaPlayerSourceQueue() {
+            mQueue.add(new MediaPlayerSource(null));
+        }
+
+        synchronized MediaPlayer getCurrentPlayer() {
+            return mQueue.get(0).mPlayer;
+        }
+
+        synchronized MediaPlayerSource getFirst() {
+            return mQueue.get(0);
+        }
+
+        synchronized void setFirst(DataSourceDesc dsd) throws IOException {
+            if (mQueue.isEmpty()) {
+                mQueue.add(0, new MediaPlayerSource(dsd));
+            } else {
+                mQueue.get(0).mDSD = dsd;
+                setUpListeners(mQueue.get(0));
+            }
+            handleDataSource(mQueue.get(0));
+        }
+
+        synchronized DataSourceError setNext(DataSourceDesc dsd) {
+            MediaPlayerSource src = new MediaPlayerSource(dsd);
+            if (mQueue.isEmpty()) {
+                mQueue.add(src);
+                return prepareAt(0);
+            } else {
+                mQueue.add(1, src);
+                return prepareAt(1);
+            }
+        }
+
+        synchronized DataSourceError setNextMultiple(List<DataSourceDesc> descs) {
+            List<MediaPlayerSource> sources = new ArrayList<>();
+            for (DataSourceDesc dsd: descs) {
+                sources.add(new MediaPlayerSource(dsd));
+            }
+            if (mQueue.isEmpty()) {
+                mQueue.addAll(sources);
+                return prepareAt(0);
+            } else {
+                mQueue.addAll(1, sources);
+                return prepareAt(1);
+            }
+        }
+
+        synchronized void play() {
+            MediaPlayerSource src = mQueue.get(0);
+            if (src.mSourceState == SOURCE_STATE_PREPARED) {
+                src.mPlayer.start();
+                setMp2State(src.mPlayer, MEDIAPLAYER2_STATE_PLAYING);
+            }
+        }
+
+        synchronized void prepare() {
+            getCurrentPlayer().prepareAsync();
+        }
+
+        synchronized void release() {
+            getCurrentPlayer().release();
+        }
+
+        synchronized void prepareAsync() {
+            MediaPlayer mp = getCurrentPlayer();
+            mp.prepareAsync();
+            setBufferingState(mp, MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_STARVED);
+        }
+
+        synchronized void pause() {
+            MediaPlayer mp = getCurrentPlayer();
+            mp.pause();
+            setMp2State(mp, MEDIAPLAYER2_STATE_PAUSED);
+        }
+
+        synchronized long getCurrentPosition() {
+            return getCurrentPlayer().getCurrentPosition();
+        }
+
+        synchronized long getDuration() {
+            return getCurrentPlayer().getDuration();
+        }
+
+        synchronized long getBufferedPosition() {
+            MediaPlayerSource src = mQueue.get(0);
+            return (long) src.mPlayer.getDuration() * src.mBufferedPercentage.get() / 100;
+        }
+
+        synchronized void setAudioAttributes(AudioAttributes attributes) {
+            getCurrentPlayer().setAudioAttributes(attributes);
+        }
+
+        synchronized DataSourceError onPrepared(MediaPlayer mp) {
+            for (int i = 0; i < mQueue.size(); i++) {
+                MediaPlayerSource src = mQueue.get(i);
+                if (mp == src.mPlayer) {
+                    if (i == 0) {
+                        if (src.mPlayPending) {
+                            src.mPlayPending = false;
+                            src.mPlayer.start();
+                            setMp2State(src.mPlayer, MEDIAPLAYER2_STATE_PLAYING);
+                        } else {
+                            setMp2State(src.mPlayer, MEDIAPLAYER2_STATE_PREPARED);
+                        }
+                    }
+                    src.mSourceState = SOURCE_STATE_PREPARED;
+                    setBufferingState(src.mPlayer,
+                            MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
+                    return prepareAt(i + 1);
+                }
+            }
+            return null;
+        }
+
+        synchronized DataSourceError onCompletion(MediaPlayer mp) {
+            if (!mQueue.isEmpty() && mp == getCurrentPlayer()) {
+                if (mQueue.size() == 1) {
+                    setMp2State(mp, MEDIAPLAYER2_STATE_PAUSED);
+                    return null;
+                }
+                moveToNext();
+            }
+            return playCurrent();
+        }
+
+        synchronized void moveToNext() {
+            final MediaPlayerSource src1 = mQueue.remove(0);
+            src1.mPlayer.release();
+            if (mQueue.isEmpty()) {
+                throw new IllegalStateException("player/source queue emptied");
+            }
+            final MediaPlayerSource src2 = mQueue.get(0);
+            if (src1.mPlayerState != src2.mPlayerState) {
+                notifyPlayerEvent(new PlayerEventNotifier() {
+                    @Override
+                    public void notify(PlayerEventCallback cb) {
+                        cb.onPlayerStateChanged(mMediaPlayerInterfaceImpl, src2.mPlayerState);
+                    }
+                });
+            }
+            notifyPlayerEvent(new PlayerEventNotifier() {
+                @Override
+                public void notify(PlayerEventCallback cb) {
+                    cb.onCurrentDataSourceChanged(mMediaPlayerInterfaceImpl, src2.mDSD);
+                }
+            });
+        }
+
+        synchronized DataSourceError playCurrent() {
+            DataSourceError err = null;
+            final MediaPlayerSource src = mQueue.get(0);
+            src.mPlayer.setSurface(mSurface);
+            src.mPlayer.setVolume(mVolume, mVolume);
+            if (src.mSourceState == SOURCE_STATE_PREPARED) {
+                // start next source only when it's in prepared state.
+                src.mPlayer.start();
+                notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                    @Override
+                    public void notify(MediaPlayer2EventCallback callback) {
+                        callback.onInfo(MediaPlayer2Impl.this, src.getDSD(),
+                                MEDIA_INFO_STARTED_AS_NEXT, 0);
+                    }
+                });
+
+            } else {
+                if (src.mSourceState == SOURCE_STATE_INIT) {
+                    err = prepareAt(0);
+                }
+                src.mPlayPending = true;
+            }
+            return err;
+        }
+
+        synchronized void onError(MediaPlayer mp) {
+            setMp2State(mp, MEDIAPLAYER2_STATE_ERROR);
+            setBufferingState(mp, MediaPlayerInterface.BUFFERING_STATE_UNKNOWN);
+        }
+
+        synchronized DataSourceError prepareAt(int n) {
+            if (n >= mQueue.size()
+                    || mQueue.get(n).mSourceState != SOURCE_STATE_INIT
+                    || getPlayerState() == MediaPlayerInterface.PLAYER_STATE_IDLE) {
+                // There is no next source or it's in preparing or prepared state.
+                return null;
+            }
+
+            MediaPlayerSource src = mQueue.get(n);
+            try {
+                src.mSourceState = SOURCE_STATE_PREPARING;
+                handleDataSource(src);
+                src.mPlayer.prepareAsync();
+                return null;
+            } catch (Exception e) {
+                DataSourceDesc dsd = src.getDSD();
+                setMp2State(src.mPlayer, MEDIAPLAYER2_STATE_ERROR);
+                return new DataSourceError(dsd, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED);
+            }
+
+        }
+
+        synchronized void skipToNext() {
+            if (mQueue.size() <= 1) {
+                throw new IllegalStateException("No next source available");
+            }
+            final MediaPlayerSource src = mQueue.get(0);
+            moveToNext();
+            if (src.mPlayerState == MediaPlayerInterface.PLAYER_STATE_PLAYING) {
+                playCurrent();
+            }
+        }
+
+        synchronized void setLooping(boolean loop) {
+            getCurrentPlayer().setLooping(loop);
+        }
+
+        synchronized void setPlaybackParams(PlaybackParams playbackParams) {
+            getCurrentPlayer().setPlaybackParams(playbackParams);
+        }
+
+        synchronized float getVolume() {
+            return mVolume;
+        }
+
+        synchronized void setVolume(float volume) {
+            mVolume = volume;
+            getCurrentPlayer().setVolume(volume, volume);
+        }
+
+        synchronized void setSurface(Surface surface) {
+            mSurface = surface;
+            getCurrentPlayer().setSurface(surface);
+        }
+
+        synchronized int getVideoWidth() {
+            return getCurrentPlayer().getVideoWidth();
+        }
+
+        synchronized int getVideoHeight() {
+            return getCurrentPlayer().getVideoHeight();
+        }
+
+        synchronized PersistableBundle getMetrics() {
+            return getCurrentPlayer().getMetrics();
+        }
+
+        synchronized PlaybackParams getPlaybackParams() {
+            return getCurrentPlayer().getPlaybackParams();
+        }
+
+        synchronized void setSyncParams(SyncParams params) {
+            getCurrentPlayer().setSyncParams(params);
+        }
+
+        synchronized SyncParams getSyncParams() {
+            return getCurrentPlayer().getSyncParams();
+        }
+
+        synchronized void seekTo(long msec, int mode) {
+            getCurrentPlayer().seekTo(msec, mode);
+        }
+
+        synchronized void reset() {
+            MediaPlayerSource src = mQueue.get(0);
+            src.mPlayer.reset();
+            src.mBufferedPercentage.set(0);
+            mVolume = 1.0f;
+            setMp2State(src.mPlayer, MEDIAPLAYER2_STATE_IDLE);
+            setBufferingState(src.mPlayer, MediaPlayerInterface.BUFFERING_STATE_UNKNOWN);
+        }
+
+        synchronized MediaTimestamp getTimestamp() {
+            return getCurrentPlayer().getTimestamp();
+        }
+
+        synchronized void setAudioSessionId(int sessionId) {
+            getCurrentPlayer().setAudioSessionId(sessionId);
+        }
+
+        synchronized int getAudioSessionId() {
+            return getCurrentPlayer().getAudioSessionId();
+        }
+
+        synchronized void attachAuxEffect(int effectId) {
+            getCurrentPlayer().attachAuxEffect(effectId);
+        }
+
+        synchronized void setAuxEffectSendLevel(float level) {
+            getCurrentPlayer().setAuxEffectSendLevel(level);
+        }
+
+        synchronized MediaPlayer.TrackInfo[] getTrackInfo() {
+            return getCurrentPlayer().getTrackInfo();
+        }
+
+        synchronized int getSelectedTrack(int trackType) {
+            return getCurrentPlayer().getSelectedTrack(trackType);
+        }
+
+        synchronized void selectTrack(int index) {
+            getCurrentPlayer().selectTrack(index);
+        }
+
+        synchronized void deselectTrack(int index) {
+            getCurrentPlayer().deselectTrack(index);
+        }
+
+        synchronized MediaPlayer.DrmInfo getDrmInfo() {
+            return getCurrentPlayer().getDrmInfo();
+        }
+
+        synchronized void prepareDrm(UUID uuid)
+                throws ResourceBusyException, MediaPlayer.ProvisioningServerErrorException,
+                MediaPlayer.ProvisioningNetworkErrorException, UnsupportedSchemeException {
+            getCurrentPlayer().prepareDrm(uuid);
+        }
+
+        synchronized void releaseDrm() throws MediaPlayer.NoDrmSchemeException {
+            getCurrentPlayer().releaseDrm();
+        }
+
+        synchronized byte[] provideKeyResponse(byte[] keySetId, byte[] response)
+                throws DeniedByServerException, MediaPlayer.NoDrmSchemeException {
+            return getCurrentPlayer().provideKeyResponse(keySetId, response);
+        }
+
+        synchronized void restoreKeys(byte[] keySetId) throws MediaPlayer.NoDrmSchemeException {
+            getCurrentPlayer().restoreKeys(keySetId);
+        }
+
+        synchronized String getDrmPropertyString(String propertyName)
+                throws MediaPlayer.NoDrmSchemeException {
+            return getCurrentPlayer().getDrmPropertyString(propertyName);
+        }
+
+        synchronized void setDrmPropertyString(String propertyName, String value)
+                throws MediaPlayer.NoDrmSchemeException {
+            getCurrentPlayer().setDrmPropertyString(propertyName, value);
+        }
+
+        synchronized void setOnDrmConfigHelper(MediaPlayer.OnDrmConfigHelper onDrmConfigHelper) {
+            getCurrentPlayer().setOnDrmConfigHelper(onDrmConfigHelper);
+        }
+
+        synchronized MediaDrm.KeyRequest getKeyRequest(byte[] keySetId, byte[] initData,
+                String mimeType,
+                int keyType, Map<String, String> optionalParameters)
+                throws MediaPlayer.NoDrmSchemeException {
+            return getCurrentPlayer().getKeyRequest(keySetId, initData, mimeType, keyType,
+                    optionalParameters);
+        }
+
+        synchronized void setMp2State(MediaPlayer mp, @MediaPlayer2State int mp2State) {
+            for (final MediaPlayerSource src: mQueue) {
+                if (src.mPlayer != mp) {
+                    continue;
+                }
+                if (src.mMp2State == mp2State) {
+                    return;
+                }
+                src.mMp2State = mp2State;
+
+                final int playerState = sStateMap.get(mp2State);
+                if (src.mPlayerState == playerState) {
+                    return;
+                }
+                src.mPlayerState = playerState;
+                notifyPlayerEvent(new PlayerEventNotifier() {
+                    @Override
+                    public void notify(PlayerEventCallback cb) {
+                        cb.onPlayerStateChanged(mMediaPlayerInterfaceImpl, playerState);
+                    }
+                });
+                return;
+            }
+        }
+
+        synchronized void setBufferingState(MediaPlayer mp, @BuffState final int state) {
+            for (final MediaPlayerSource src: mQueue) {
+                if (src.mPlayer != mp) {
+                    continue;
+                }
+                if (src.mBufferingState == state) {
+                    return;
+                }
+                src.mBufferingState = state;
+                notifyPlayerEvent(new PlayerEventNotifier() {
+                    @Override
+                    public void notify(PlayerEventCallback cb) {
+                        DataSourceDesc dsd = src.getDSD();
+                        cb.onBufferingStateChanged(mMediaPlayerInterfaceImpl, dsd, state);
+                    }
+                });
+                return;
+            }
+        }
+
+        synchronized @MediaPlayer2State int getMediaPlayer2State() {
+            return mQueue.get(0).mMp2State;
+        }
+
+        synchronized @BuffState int getBufferingState() {
+            return mQueue.get(0).mBufferingState;
+        }
+
+        synchronized @PlayerState int getPlayerState() {
+            return mQueue.get(0).mPlayerState;
+        }
+
+        synchronized MediaPlayerSource getSourceForPlayer(MediaPlayer mp) {
+            for (MediaPlayerSource src: mQueue) {
+                if (src.mPlayer == mp) {
+                    return src;
+                }
+            }
+            return null;
+        }
+    }
+
+    private class MediaPlayerInterfaceImpl extends MediaPlayerInterface {
+        @Override
+        public void play() {
+            MediaPlayer2Impl.this.play();
+        }
+
+        @Override
+        public void prepare() {
+            MediaPlayer2Impl.this.prepare();
+        }
+
+        @Override
+        public void pause() {
+            MediaPlayer2Impl.this.pause();
+        }
+
+        @Override
+        public void reset() {
+            MediaPlayer2Impl.this.reset();
+        }
+
+        @Override
+        public void skipToNext() {
+            MediaPlayer2Impl.this.skipToNext();
+        }
+
+        @Override
+        public void seekTo(long pos) {
+            MediaPlayer2Impl.this.seekTo(pos);
+        }
+
+        @Override
+        public long getCurrentPosition() {
+            return MediaPlayer2Impl.this.getCurrentPosition();
+        }
+
+        @Override
+        public long getDuration() {
+            return MediaPlayer2Impl.this.getDuration();
+        }
+
+        @Override
+        public long getBufferedPosition() {
+            return MediaPlayer2Impl.this.getBufferedPosition();
+        }
+
+        @Override
+        public int getPlayerState() {
+            return MediaPlayer2Impl.this.getPlayerState();
+        }
+
+        @Override
+        public int getBufferingState() {
+            return MediaPlayer2Impl.this.getBufferingState();
+        }
+
+        @Override
+        public void setAudioAttributes(AudioAttributesCompat attributes) {
+            MediaPlayer2Impl.this.setAudioAttributes(attributes);
+        }
+
+        @Override
+        public AudioAttributesCompat getAudioAttributes() {
+            return MediaPlayer2Impl.this.getAudioAttributes();
+        }
+
+        @Override
+        public void setDataSource(DataSourceDesc dsd) {
+            MediaPlayer2Impl.this.setDataSource(dsd);
+        }
+
+        @Override
+        public void setNextDataSource(DataSourceDesc dsd) {
+            MediaPlayer2Impl.this.setNextDataSource(dsd);
+        }
+
+        @Override
+        public void setNextDataSources(List<DataSourceDesc> dsds) {
+            MediaPlayer2Impl.this.setNextDataSources(dsds);
+        }
+
+        @Override
+        public DataSourceDesc getCurrentDataSource() {
+            return MediaPlayer2Impl.this.getCurrentDataSource();
+        }
+
+        @Override
+        public void loopCurrent(boolean loop) {
+            MediaPlayer2Impl.this.loopCurrent(loop);
+        }
+
+        @Override
+        public void setPlaybackSpeed(float speed) {
+            MediaPlayer2Impl.this.setPlaybackSpeed(speed);
+        }
+
+        @Override
+        public float getPlaybackSpeed() {
+            return MediaPlayer2Impl.this.getPlaybackSpeed();
+        }
+
+        @Override
+        public void setPlayerVolume(float volume) {
+            MediaPlayer2Impl.this.setPlayerVolume(volume);
+        }
+
+        @Override
+        public float getPlayerVolume() {
+            return MediaPlayer2Impl.this.getPlayerVolume();
+        }
+
+        @Override
+        public void registerPlayerEventCallback(Executor e, final PlayerEventCallback cb) {
+            MediaPlayer2Impl.this.registerPlayerEventCallback(e, cb);
+        }
+
+        @Override
+        public void unregisterPlayerEventCallback(PlayerEventCallback cb) {
+            MediaPlayer2Impl.this.unregisterPlayerEventCallback(cb);
+        }
+
+        @Override
+        public void close() throws Exception {
+            MediaPlayer2Impl.this.close();
+        }
+    }
 }
diff --git a/media/src/main/java/androidx/media/MediaPlayerBase.java b/media/src/main/java/androidx/media/MediaPlayerInterface.java
similarity index 90%
rename from media/src/main/java/androidx/media/MediaPlayerBase.java
rename to media/src/main/java/androidx/media/MediaPlayerInterface.java
index de0e128..9d51609 100644
--- a/media/src/main/java/androidx/media/MediaPlayerBase.java
+++ b/media/src/main/java/androidx/media/MediaPlayerInterface.java
@@ -32,10 +32,10 @@
 import java.util.concurrent.Executor;
 
 /**
- * Base class for all media players that want media session.
+ * Base interface for all media players that want media session.
  */
 @TargetApi(Build.VERSION_CODES.KITKAT)
-public abstract class MediaPlayerBase implements AutoCloseable {
+public abstract class MediaPlayerInterface implements AutoCloseable {
     /**
      * @hide
      */
@@ -111,9 +111,9 @@
 
     /**
      * Prepares the player for playback.
-     * See {@link PlayerEventCallback#onMediaPrepared(MediaPlayerBase, DataSourceDesc)} for being
-     * notified when the preparation phase completed. During this time, the player may allocate
-     * resources required to play, such as audio and video decoders.
+     * See {@link PlayerEventCallback#onMediaPrepared(MediaPlayerInterface, DataSourceDesc)} for
+     * being notified when the preparation phase completed. During this time, the player may
+     * allocate resources required to play, such as audio and video decoders.
      */
     public abstract void prepare();
 
@@ -123,7 +123,7 @@
     public abstract void pause();
 
     /**
-     * Resets the MediaPlayerBase to its uninitialized state.
+     * Resets the MediaPlayerInterface to its uninitialized state.
      */
     public abstract void reset();
 
@@ -166,7 +166,7 @@
 
     /**
      * Returns the current player state.
-     * See also {@link PlayerEventCallback#onPlayerStateChanged(MediaPlayerBase, int)} for
+     * See also {@link PlayerEventCallback#onPlayerStateChanged(MediaPlayerInterface, int)} for
      * notification of changes.
      * @return the current player state
      */
@@ -294,8 +294,8 @@
 
     /**
      * A callback class to receive notifications for events on the media player.
-     * See {@link MediaPlayerBase#registerPlayerEventCallback(Executor, PlayerEventCallback)} to
-     * register this callback.
+     * See {@link MediaPlayerInterface#registerPlayerEventCallback(Executor, PlayerEventCallback)}
+     * to register this callback.
      */
     public abstract static class PlayerEventCallback {
         /**
@@ -304,7 +304,7 @@
          * @param mpb the player whose data source changed.
          * @param dsd the new current data source. null, if no more data sources available.
          */
-        public void onCurrentDataSourceChanged(@NonNull MediaPlayerBase mpb,
+        public void onCurrentDataSourceChanged(@NonNull MediaPlayerInterface mpb,
                 @Nullable DataSourceDesc dsd) { }
 
         /**
@@ -313,16 +313,17 @@
          * @param mpb the player that is prepared.
          * @param dsd the data source that the player is prepared to play.
          */
-        public void onMediaPrepared(@NonNull MediaPlayerBase mpb,
+        public void onMediaPrepared(@NonNull MediaPlayerInterface mpb,
                 @NonNull DataSourceDesc dsd) { }
 
         /**
          * Called to indicate that the state of the player has changed.
-         * See {@link MediaPlayerBase#getPlayerState()} for polling the player state.
+         * See {@link MediaPlayerInterface#getPlayerState()} for polling the player state.
          * @param mpb the player whose state has changed.
          * @param state the new state of the player.
          */
-        public void onPlayerStateChanged(@NonNull MediaPlayerBase mpb, @PlayerState int state) { }
+        public void onPlayerStateChanged(@NonNull MediaPlayerInterface mpb,
+                @PlayerState int state) { }
 
         /**
          * Called to report buffering events for a data source.
@@ -330,7 +331,7 @@
          * @param dsd the data source for which buffering is happening.
          * @param state the new buffering state.
          */
-        public void onBufferingStateChanged(@NonNull MediaPlayerBase mpb,
+        public void onBufferingStateChanged(@NonNull MediaPlayerInterface mpb,
                 @NonNull DataSourceDesc dsd, @BuffState int state) { }
 
         /**
@@ -338,7 +339,7 @@
          * @param mpb the player that has changed the playback speed.
          * @param speed the new playback speed.
          */
-        public void onPlaybackSpeedChanged(@NonNull MediaPlayerBase mpb, float speed) { }
+        public void onPlaybackSpeedChanged(@NonNull MediaPlayerInterface mpb, float speed) { }
 
         /**
          * Called to indicate that {@link #seekTo(long)} is completed.
@@ -347,6 +348,6 @@
          * @param position the previous seeking request.
          * @see #seekTo(long)
          */
-        public void onSeekCompleted(@NonNull MediaPlayerBase mpb, long position) { }
+        public void onSeekCompleted(@NonNull MediaPlayerInterface mpb, long position) { }
     }
 }
diff --git a/media/src/main/java/androidx/media/MediaPlaylistAgent.java b/media/src/main/java/androidx/media/MediaPlaylistAgent.java
index 07838e8..4e1eee4 100644
--- a/media/src/main/java/androidx/media/MediaPlaylistAgent.java
+++ b/media/src/main/java/androidx/media/MediaPlaylistAgent.java
@@ -377,12 +377,12 @@
 
     /**
      * Called by {@link MediaSession2} when it wants to translate {@link DataSourceDesc} from the
-     * {@link MediaPlayerBase.PlayerEventCallback} to the {@link MediaItem2}. Override this method
-     * if you want to create {@link DataSourceDesc}s dynamically, instead of specifying them with
-     * {@link #setPlaylist(List, MediaMetadata2)}.
+     * {@link MediaPlayerInterface.PlayerEventCallback} to the {@link MediaItem2}. Override this
+     * method if you want to create {@link DataSourceDesc}s dynamically, instead of specifying them
+     * with {@link #setPlaylist(List, MediaMetadata2)}.
      * <p>
      * Session would throw an exception if this returns {@code null} for the dsd from the
-     * {@link MediaPlayerBase.PlayerEventCallback}.
+     * {@link MediaPlayerInterface.PlayerEventCallback}.
      * <p>
      * Default implementation calls the {@link #getPlaylist()} and searches the {@link MediaItem2}
      * with the {@param dsd}.
diff --git a/media/src/main/java/androidx/media/MediaSession2.java b/media/src/main/java/androidx/media/MediaSession2.java
index 909e979..ce95078 100644
--- a/media/src/main/java/androidx/media/MediaSession2.java
+++ b/media/src/main/java/androidx/media/MediaSession2.java
@@ -27,8 +27,9 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.os.RemoteException;
 import android.os.ResultReceiver;
-import android.support.v4.media.session.IMediaControllerCallback;
+import android.support.v4.media.session.MediaSessionCompat;
 import android.support.v4.media.session.PlaybackStateCompat;
 
 import androidx.annotation.IntDef;
@@ -36,8 +37,8 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.media.MediaController2.PlaybackInfo;
-import androidx.media.MediaPlayerBase.BuffState;
-import androidx.media.MediaPlayerBase.PlayerState;
+import androidx.media.MediaPlayerInterface.BuffState;
+import androidx.media.MediaPlayerInterface.PlayerState;
 import androidx.media.MediaPlaylistAgent.PlaylistEventCallback;
 import androidx.media.MediaPlaylistAgent.RepeatMode;
 import androidx.media.MediaPlaylistAgent.ShuffleMode;
@@ -60,6 +61,10 @@
  * 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.
@@ -72,6 +77,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)
 public class MediaSession2 extends MediaInterface2.SessionPlayer implements AutoCloseable {
@@ -154,6 +161,594 @@
      */
     public static final int ERROR_CODE_SETUP_REQUIRED = 12;
 
+    static final String TAG = "MediaSession2";
+
+    private final SupportLibraryImpl mImpl;
+
+    MediaSession2(SupportLibraryImpl impl) {
+        mImpl = impl;
+    }
+
+    SupportLibraryImpl getImpl() {
+        return mImpl;
+    }
+
+    /**
+     * Sets the underlying {@link MediaPlayerInterface} and {@link MediaPlaylistAgent} for this
+     * session to dispatch incoming event to.
+     * <p>
+     * When a {@link MediaPlaylistAgent} is specified here, the playlist agent should manage
+     * {@link MediaPlayerInterface} for calling
+     * {@link MediaPlayerInterface#setNextDataSources(List)}.
+     * <p>
+     * If the {@link MediaPlaylistAgent} isn't set, session will recreate the default playlist
+     * agent.
+     *
+     * @param player a {@link MediaPlayerInterface} that handles actual media playback in your app
+     * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the {@code player}
+     * @param volumeProvider a {@link VolumeProviderCompat}. If {@code null}, system will adjust the
+     *                       appropriate stream volume for this session's player.
+     */
+    public void updatePlayer(@NonNull MediaPlayerInterface player,
+            @Nullable MediaPlaylistAgent playlistAgent,
+            @Nullable VolumeProviderCompat volumeProvider) {
+        mImpl.updatePlayer(player, playlistAgent, volumeProvider);
+    }
+
+    @Override
+    public void close() {
+        try {
+            mImpl.close();
+        } catch (Exception e) {
+            // Should not be here.
+        }
+    }
+
+    /**
+     * @return player
+     */
+    public @NonNull MediaPlayerInterface getPlayer() {
+        return mImpl.getPlayer();
+    }
+
+    /**
+     * @return playlist agent
+     */
+    public @NonNull MediaPlaylistAgent getPlaylistAgent() {
+        return mImpl.getPlaylistAgent();
+    }
+
+    /**
+     * @return volume provider
+     */
+    public @Nullable VolumeProviderCompat getVolumeProvider() {
+        return mImpl.getVolumeProvider();
+    }
+
+    /**
+     * Returns the {@link SessionToken2} for creating {@link MediaController2}.
+     */
+    public @NonNull SessionToken2 getToken() {
+        return mImpl.getToken();
+    }
+
+    @NonNull Context getContext() {
+        return mImpl.getContext();
+    }
+
+    @NonNull Executor getCallbackExecutor() {
+        return mImpl.getCallbackExecutor();
+    }
+
+    @NonNull SessionCallback getCallback() {
+        return mImpl.getCallback();
+    }
+
+    /**
+     * Returns the list of connected controller.
+     *
+     * @return list of {@link ControllerInfo}
+     */
+    public @NonNull List<ControllerInfo> getConnectedControllers() {
+        return mImpl.getConnectedControllers();
+    }
+
+    /**
+     * Set the {@link AudioFocusRequest} to obtain the audio focus
+     *
+     * @param afr the full request parameters
+     */
+    public void setAudioFocusRequest(@Nullable AudioFocusRequest afr) {
+        mImpl.setAudioFocusRequest(afr);
+    }
+
+    /**
+     * Sets ordered list of {@link CommandButton} for controllers to build UI with it.
+     * <p>
+     * It's up to controller's decision how to represent the layout in its own UI.
+     * Here's the same way
+     * (layout[i] means a CommandButton at index i in the given list)
+     * For 5 icons row
+     *      layout[3] layout[1] layout[0] layout[2] layout[4]
+     * For 3 icons row
+     *      layout[1] layout[0] layout[2]
+     * For 5 icons row with overflow icon (can show +5 extra buttons with overflow button)
+     *      expanded row:   layout[5] layout[6] layout[7] layout[8] layout[9]
+     *      main row:       layout[3] layout[1] layout[0] layout[2] layout[4]
+     * <p>
+     * This API can be called in the
+     * {@link SessionCallback#onConnect(MediaSession2, ControllerInfo)}.
+     *
+     * @param controller controller to specify layout.
+     * @param layout ordered list of layout.
+     */
+    public void setCustomLayout(@NonNull ControllerInfo controller,
+            @NonNull List<CommandButton> layout) {
+        mImpl.setCustomLayout(controller, layout);
+    }
+
+    /**
+     * Set the new allowed command group for the controller
+     *
+     * @param controller controller to change allowed commands
+     * @param commands new allowed commands
+     */
+    public void setAllowedCommands(@NonNull ControllerInfo controller,
+            @NonNull SessionCommandGroup2 commands) {
+        mImpl.setAllowedCommands(controller, commands);
+    }
+
+    /**
+     * Send custom command to all connected controllers.
+     *
+     * @param command a command
+     * @param args optional argument
+     */
+    public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) {
+        mImpl.sendCustomCommand(command, args);
+    }
+
+    /**
+     * Send custom command to a specific controller.
+     *
+     * @param command a command
+     * @param args optional argument
+     * @param receiver result receiver for the session
+     */
+    public void sendCustomCommand(@NonNull ControllerInfo controller,
+            @NonNull SessionCommand2 command, @Nullable Bundle args,
+            @Nullable ResultReceiver receiver) {
+        mImpl.sendCustomCommand(controller, command, args, receiver);
+    }
+
+    /**
+     * Play playback.
+     * <p>
+     * This calls {@link MediaPlayerInterface#play()}.
+     */
+    @Override
+    public void play() {
+        mImpl.play();
+    }
+
+    /**
+     * Pause playback.
+     * <p>
+     * This calls {@link MediaPlayerInterface#pause()}.
+     */
+    @Override
+    public void pause() {
+        mImpl.pause();
+    }
+
+    /**
+     * Stop playback, and reset the player to the initial state.
+     * <p>
+     * This calls {@link MediaPlayerInterface#reset()}.
+     */
+    @Override
+    public void reset() {
+        mImpl.reset();
+    }
+
+    /**
+     * Request that the player prepare its playback. In other words, other sessions can continue
+     * to play during the preparation of this session. This method can be used to speed up the
+     * start of the playback. Once the preparation is done, the session will change its playback
+     * state to {@link MediaPlayerInterface#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be
+     * called to start playback.
+     * <p>
+     * This calls {@link MediaPlayerInterface#reset()}.
+     */
+    @Override
+    public void prepare() {
+        mImpl.prepare();
+    }
+
+    /**
+     * Move to a new location in the media stream.
+     *
+     * @param pos Position to move to, in milliseconds.
+     */
+    @Override
+    public void seekTo(long pos) {
+        mImpl.seekTo(pos);
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void skipForward() {
+        mImpl.skipForward();
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void skipBackward() {
+        mImpl.skipBackward();
+    }
+
+    /**
+     * Notify errors to the connected controllers
+     *
+     * @param errorCode error code
+     * @param extras extras
+     */
+    @Override
+    public void notifyError(@ErrorCode int errorCode, @Nullable Bundle extras) {
+        mImpl.notifyError(errorCode, extras);
+    }
+
+    /**
+     * Notify routes information to a connected controller
+     *
+     * @param controller controller information
+     * @param routes The routes information. Each bundle should be from
+     *              MediaRouteDescritor.asBundle().
+     */
+    public void notifyRoutesInfoChanged(@NonNull ControllerInfo controller,
+            @Nullable List<Bundle> routes) {
+        mImpl.notifyRoutesInfoChanged(controller, routes);
+    }
+
+    /**
+     * Gets the current player state.
+     *
+     * @return the current player state
+     */
+    @Override
+    public @PlayerState int getPlayerState() {
+        return mImpl.getPlayerState();
+    }
+
+    /**
+     * Gets the current position.
+     *
+     * @return the current playback position in ms, or {@link MediaPlayerInterface#UNKNOWN_TIME} if
+     *         unknown.
+     */
+    @Override
+    public long getCurrentPosition() {
+        return mImpl.getCurrentPosition();
+    }
+
+    /**
+     * Gets the duration of the currently playing media item.
+     *
+     * @return the duration of the current item from {@link MediaPlayerInterface#getDuration()}.
+     */
+    @Override
+    public long getDuration() {
+        return mImpl.getDuration();
+    }
+
+    /**
+     * Gets the buffered position, or {@link MediaPlayerInterface#UNKNOWN_TIME} if unknown.
+     *
+     * @return the buffered position in ms, or {@link MediaPlayerInterface#UNKNOWN_TIME}.
+     */
+    @Override
+    public long getBufferedPosition() {
+        return mImpl.getBufferedPosition();
+    }
+
+    /**
+     * Gets the current buffering state of the player.
+     * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+     * buffered.
+     *
+     * @return the buffering state.
+     */
+    @Override
+    public @BuffState int getBufferingState() {
+        return mImpl.getBufferingState();
+    }
+
+    /**
+     * Get the playback speed.
+     *
+     * @return speed
+     */
+    @Override
+    public float getPlaybackSpeed() {
+        return mImpl.getPlaybackSpeed();
+    }
+
+    /**
+     * Set the playback speed.
+     */
+    @Override
+    public void setPlaybackSpeed(float speed) {
+        mImpl.setPlaybackSpeed(speed);
+    }
+
+    /**
+     * 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>
+     * 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
+     * when
+     * <ul>
+     *      <li>{@link MediaItem2} specified by {@link #setPlaylist(List, MediaMetadata2)} doesn't
+     *          have {@link DataSourceDesc}</li>
+     *      <li>{@link MediaController2#addPlaylistItem(int, MediaItem2)} is called and accepted
+     *          by {@link SessionCallback#onCommandRequest(
+     *          MediaSession2, ControllerInfo, SessionCommand2)}.
+     *          In that case, an item would be added automatically without the data source.</li>
+     * </ul>
+     * <p>
+     * If it's not set, playback wouldn't happen for the item without data source descriptor.
+     * <p>
+     * The helper will be run on the executor that was specified by
+     * {@link Builder#setSessionCallback(Executor, SessionCallback)}.
+     *
+     * @param helper a data source missing helper.
+     * @throws IllegalStateException when the helper is set when the playlist agent is set
+     * @see #setPlaylist(List, MediaMetadata2)
+     * @see SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)
+     * @see SessionCommand2#COMMAND_CODE_PLAYLIST_ADD_ITEM
+     * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REPLACE_ITEM
+     */
+    @Override
+    public void setOnDataSourceMissingHelper(@NonNull OnDataSourceMissingHelper helper) {
+        mImpl.setOnDataSourceMissingHelper(helper);
+    }
+
+    /**
+     * Clears the data source missing helper.
+     *
+     * @see #setOnDataSourceMissingHelper(OnDataSourceMissingHelper)
+     */
+    @Override
+    public void clearOnDataSourceMissingHelper() {
+        mImpl.clearOnDataSourceMissingHelper();
+    }
+
+    /**
+     * Returns the playlist from the {@link MediaPlaylistAgent}.
+     * <p>
+     * This list may differ with the list that was specified with
+     * {@link #setPlaylist(List, MediaMetadata2)} depending on the {@link MediaPlaylistAgent}
+     * implementation. Use media items returned here for other playlist agent APIs such as
+     * {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
+     *
+     * @return playlist
+     * @see MediaPlaylistAgent#getPlaylist()
+     * @see SessionCallback#onPlaylistChanged(
+     *          MediaSession2, MediaPlaylistAgent, List, MediaMetadata2)
+     */
+    @Override
+    public List<MediaItem2> getPlaylist() {
+        return mImpl.getPlaylist();
+    }
+
+    /**
+     * Sets a list of {@link MediaItem2} to the {@link MediaPlaylistAgent}. Ensure uniqueness of
+     * each {@link MediaItem2} in the playlist so the session can uniquely identity individual
+     * items.
+     * <p>
+     * This may be an asynchronous call, and {@link MediaPlaylistAgent} may keep the copy of the
+     * list. Wait for {@link SessionCallback#onPlaylistChanged(MediaSession2, MediaPlaylistAgent,
+     * List, MediaMetadata2)} to know the operation finishes.
+     * <p>
+     * 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.
+     * <p>
+     * It's recommended to fill {@link MediaMetadata2} in each {@link MediaItem2} especially for the
+     * duration information with the key {@link MediaMetadata2#METADATA_KEY_DURATION}. Without the
+     * duration information in the metadata, session will do extra work to get the duration and send
+     * it to the controller.
+     *
+     * @param list A list of {@link MediaItem2} objects to set as a play list.
+     * @throws IllegalArgumentException if given list is {@code null}, or has duplicated media
+     * items.
+     * @see MediaPlaylistAgent#setPlaylist(List, MediaMetadata2)
+     * @see SessionCallback#onPlaylistChanged(
+     *          MediaSession2, MediaPlaylistAgent, List, MediaMetadata2)
+     * @see #setOnDataSourceMissingHelper
+     */
+    @Override
+    public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+        mImpl.setPlaylist(list, metadata);
+    }
+
+    /**
+     * Skips to the item in the playlist.
+     * <p>
+     * This calls {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)} and the behavior depends
+     * on the playlist agent implementation, especially with the shuffle/repeat mode.
+     *
+     * @param item The item in the playlist you want to play
+     * @see #getShuffleMode()
+     * @see #getRepeatMode()
+     */
+    @Override
+    public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+        mImpl.skipToPlaylistItem(item);
+    }
+
+    /**
+     * Skips to the previous item.
+     * <p>
+     * This calls {@link MediaPlaylistAgent#skipToPreviousItem()} and the behavior depends on the
+     * playlist agent implementation, especially with the shuffle/repeat mode.
+     *
+     * @see #getShuffleMode()
+     * @see #getRepeatMode()
+     **/
+    @Override
+    public void skipToPreviousItem() {
+        mImpl.skipToPreviousItem();
+    }
+
+    /**
+     * Skips to the next item.
+     * <p>
+     * This calls {@link MediaPlaylistAgent#skipToNextItem()} and the behavior depends on the
+     * playlist agent implementation, especially with the shuffle/repeat mode.
+     *
+     * @see #getShuffleMode()
+     * @see #getRepeatMode()
+     */
+    @Override
+    public void skipToNextItem() {
+        mImpl.skipToNextItem();
+    }
+
+    /**
+     * Gets the playlist metadata from the {@link MediaPlaylistAgent}.
+     *
+     * @return the playlist metadata
+     */
+    @Override
+    public MediaMetadata2 getPlaylistMetadata() {
+        return mImpl.getPlaylistMetadata();
+    }
+
+    /**
+     * Adds the media item to the playlist at position index. Index equals or greater than
+     * 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,
+     * the current index of the play list will be incremented correspondingly.
+     *
+     * @param index the index you want to add
+     * @param item the media item you want to add
+     */
+    @Override
+    public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+        mImpl.addPlaylistItem(index, item);
+    }
+
+    /**
+     * Removes the media item in the playlist.
+     * <p>
+     * If the item is the currently playing item of the playlist, current playback
+     * will be stopped and playback moves to next source in the list.
+     *
+     * @param item the media item you want to add
+     */
+    @Override
+    public void removePlaylistItem(@NonNull MediaItem2 item) {
+        mImpl.removePlaylistItem(item);
+    }
+
+    /**
+     * Replaces the media item at index in the playlist. This can be also used to update metadata of
+     * an item.
+     *
+     * @param index the index of the item to replace
+     * @param item the new item
+     */
+    @Override
+    public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+        mImpl.replacePlaylistItem(index, item);
+    }
+
+    /**
+     * Return currently playing media item.
+     *
+     * @return currently playing media item
+     */
+    @Override
+    public MediaItem2 getCurrentMediaItem() {
+        return mImpl.getCurrentMediaItem();
+    }
+
+    /**
+     * Updates the playlist metadata to the {@link MediaPlaylistAgent}.
+     *
+     * @param metadata metadata of the playlist
+     */
+    @Override
+    public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+        mImpl.updatePlaylistMetadata(metadata);
+    }
+
+    /**
+     * Gets the repeat mode from the {@link MediaPlaylistAgent}.
+     *
+     * @return repeat mode
+     * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+     * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+     */
+    @Override
+    public @RepeatMode int getRepeatMode() {
+        return mImpl.getRepeatMode();
+    }
+
+    /**
+     * Sets the repeat mode to the {@link MediaPlaylistAgent}.
+     *
+     * @param repeatMode repeat mode
+     * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+     * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+     */
+    @Override
+    public void setRepeatMode(@RepeatMode int repeatMode) {
+        mImpl.setRepeatMode(repeatMode);
+    }
+
+    /**
+     * Gets the shuffle mode from the {@link MediaPlaylistAgent}.
+     *
+     * @return The shuffle mode
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+     */
+    @Override
+    public @ShuffleMode int getShuffleMode() {
+        return mImpl.getShuffleMode();
+    }
+
+    /**
+     * Sets the shuffle mode to the {@link MediaPlaylistAgent}.
+     *
+     * @param shuffleMode The shuffle mode
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+     */
+    @Override
+    public void setShuffleMode(@ShuffleMode int shuffleMode) {
+        mImpl.setShuffleMode(shuffleMode);
+    }
+
     /**
      * 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.
@@ -217,7 +812,7 @@
          * Called when a controller sent a command which will be sent directly to one of the
          * following:
          * <ul>
-         *  <li> {@link MediaPlayerBase} </li>
+         *  <li> {@link MediaPlayerInterface} </li>
          *  <li> {@link MediaPlaylistAgent} </li>
          *  <li> {@link android.media.AudioManager} or {@link VolumeProviderCompat} </li>
          * </ul>
@@ -336,7 +931,7 @@
          * <p>
          * During the preparation, a session should not hold audio focus in order to allow other
          * sessions play seamlessly. The state of playback should be updated to
-         * {@link MediaPlayerBase#PLAYER_STATE_PAUSED} after the preparation is done.
+         * {@link MediaPlayerInterface#PLAYER_STATE_PAUSED} after the preparation is done.
          * <p>
          * The playback of the prepared content should start in the later calls of
          * {@link MediaSession2#play()}.
@@ -361,8 +956,9 @@
          * An empty query indicates that the app may prepare any music. The implementation should
          * attempt to make a smart choice about what to play.
          * <p>
-         * The state of playback should be updated to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}
-         * after the preparation is done. The playback of the prepared content should start in the
+         * The state of playback should be updated to
+         * {@link MediaPlayerInterface#PLAYER_STATE_PAUSED} after the preparation is done.
+         * The playback of the prepared content should start in the
          * later calls of {@link MediaSession2#play()}.
          * <p>
          * Override {@link #onPlayFromSearch} to handle requests for starting playback without
@@ -384,7 +980,7 @@
          * <p>
          * During the preparation, a session should not hold audio focus in order to allow
          * other sessions play seamlessly. The state of playback should be updated to
-         * {@link MediaPlayerBase#PLAYER_STATE_PAUSED} after the preparation is done.
+         * {@link MediaPlayerInterface#PLAYER_STATE_PAUSED} after the preparation is done.
          * <p>
          * The playback of the prepared content should start in the later calls of
          * {@link MediaSession2#play()}.
@@ -461,7 +1057,7 @@
          * @param item new item
          */
         public void onCurrentMediaItemChanged(@NonNull MediaSession2 session,
-                @NonNull MediaPlayerBase player, @NonNull MediaItem2 item) { }
+                @NonNull MediaPlayerInterface player, @Nullable MediaItem2 item) { }
 
         /**
          * Called when the player is <i>prepared</i>, i.e. it is ready to play the content
@@ -470,18 +1066,18 @@
          * @param player the player for this event
          * @param item the media item for which buffering is happening
          */
-        public void onMediaPrepared(@NonNull MediaSession2 session, @NonNull MediaPlayerBase player,
-                @NonNull MediaItem2 item) { }
+        public void onMediaPrepared(@NonNull MediaSession2 session,
+                @NonNull MediaPlayerInterface player, @NonNull MediaItem2 item) { }
 
         /**
          * Called to indicate that the state of the player has changed.
-         * See {@link MediaPlayerBase#getPlayerState()} for polling the player state.
+         * See {@link MediaPlayerInterface#getPlayerState()} for polling the player state.
          * @param session the session for this event
          * @param player the player for this event
          * @param state the new state of the player.
          */
         public void onPlayerStateChanged(@NonNull MediaSession2 session,
-                @NonNull MediaPlayerBase player, @PlayerState int state) { }
+                @NonNull MediaPlayerInterface player, @PlayerState int state) { }
 
         /**
          * Called to report buffering events for a data source.
@@ -492,7 +1088,8 @@
          * @param state the new buffering state.
          */
         public void onBufferingStateChanged(@NonNull MediaSession2 session,
-                @NonNull MediaPlayerBase player, @NonNull MediaItem2 item, @BuffState int state) { }
+                @NonNull MediaPlayerInterface player, @NonNull MediaItem2 item,
+                @BuffState int state) { }
 
         /**
          * Called to indicate that the playback speed has changed.
@@ -501,18 +1098,18 @@
          * @param speed the new playback speed.
          */
         public void onPlaybackSpeedChanged(@NonNull MediaSession2 session,
-                @NonNull MediaPlayerBase player, float speed) { }
+                @NonNull MediaPlayerInterface player, float speed) { }
 
         /**
          * Called to indicate that {@link #seekTo(long)} is completed.
          *
          * @param session the session for this event.
-         * @param mpb the player that has completed seeking.
+         * @param player the player that has completed seeking.
          * @param position the previous seeking request.
          * @see #seekTo(long)
          */
-        public void onSeekCompleted(@NonNull MediaSession2 session, @NonNull MediaPlayerBase mpb,
-                long position) { }
+        public void onSeekCompleted(@NonNull MediaSession2 session,
+                @NonNull MediaPlayerInterface player, long position) { }
 
         /**
          * Called when a playlist is changed from the {@link MediaPlaylistAgent}.
@@ -571,153 +1168,6 @@
     }
 
     /**
-     * Base builder class for MediaSession2 and its subclass. Any change in this class should be
-     * also applied to the subclasses {@link MediaSession2.Builder} and
-     * {@link MediaLibraryService2.MediaLibrarySession.Builder}.
-     * <p>
-     * APIs here should be package private, but should have documentations for developers.
-     * Otherwise, javadoc will generate documentation with the generic types such as follows.
-     * <pre>U extends BuilderBase<T, U, C> setSessionCallback(Executor executor, C callback)</pre>
-     * <p>
-     * This class is hidden to prevent from generating test stub, which fails with
-     * 'unexpected bound' because it tries to auto generate stub class as follows.
-     * <pre>abstract static class BuilderBase<
-     *      T extends android.media.MediaSession2,
-     *      U extends android.media.MediaSession2.BuilderBase<
-     *              T, U, C extends android.media.MediaSession2.SessionCallback>, C></pre>
-     * @hide
-     */
-    @RestrictTo(LIBRARY_GROUP)
-    abstract static class BuilderBase
-            <T extends MediaSession2, U extends BuilderBase<T, U, C>, C extends SessionCallback> {
-        final Context mContext;
-        MediaSession2ImplBase.BuilderBase<T, C> mBaseImpl;
-        MediaPlayerBase mPlayer;
-        String mId;
-        Executor mCallbackExecutor;
-        C mCallback;
-        MediaPlaylistAgent mPlaylistAgent;
-        VolumeProviderCompat mVolumeProvider;
-        PendingIntent mSessionActivity;
-
-        BuilderBase(Context context) {
-            if (context == null) {
-                throw new IllegalArgumentException("context shouldn't be null");
-            }
-            mContext = context;
-            // Ensure non-null
-            mId = "";
-        }
-
-        /**
-         * Sets the underlying {@link MediaPlayerBase} for this session to dispatch incoming event
-         * to.
-         *
-         * @param player a {@link MediaPlayerBase} that handles actual media playback in your app.
-         */
-        @NonNull U setPlayer(@NonNull MediaPlayerBase player) {
-            if (player == null) {
-                throw new IllegalArgumentException("player shouldn't be null");
-            }
-            mBaseImpl.setPlayer(player);
-            return (U) this;
-        }
-
-        /**
-         * Sets the {@link MediaPlaylistAgent} for this session to manages playlist of the
-         * underlying {@link MediaPlayerBase}. The playlist agent should manage
-         * {@link MediaPlayerBase} for calling {@link MediaPlayerBase#setNextDataSources(List)}.
-         * <p>
-         * If the {@link MediaPlaylistAgent} isn't set, session will create the default playlist
-         * agent.
-         *
-         * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the
-         *                      {@code player}
-         */
-        U setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
-            if (playlistAgent == null) {
-                throw new IllegalArgumentException("playlistAgent shouldn't be null");
-            }
-            mBaseImpl.setPlaylistAgent(playlistAgent);
-            return (U) this;
-        }
-
-        /**
-         * Sets the {@link VolumeProviderCompat} for this session to handle volume events. If not
-         * set, system will adjust the appropriate stream volume for this session's player.
-         *
-         * @param volumeProvider The provider that will receive volume button events.
-         */
-        @NonNull U setVolumeProvider(@Nullable VolumeProviderCompat volumeProvider) {
-            mBaseImpl.setVolumeProvider(volumeProvider);
-            return (U) this;
-        }
-
-        /**
-         * Set an intent for launching UI for this Session. This can be used as a
-         * quick link to an ongoing media screen. The intent should be for an
-         * activity that may be started using {@link Context#startActivity(Intent)}.
-         *
-         * @param pi The intent to launch to show UI for this session.
-         */
-        @NonNull U setSessionActivity(@Nullable PendingIntent pi) {
-            mBaseImpl.setSessionActivity(pi);
-            return (U) this;
-        }
-
-        /**
-         * Set ID of the session. If it's not set, an empty string with used to create a session.
-         * <p>
-         * Use this if and only if your app supports multiple playback at the same time and also
-         * wants to provide external apps to have finer controls of them.
-         *
-         * @param id id of the session. Must be unique per package.
-         * @throws IllegalArgumentException if id is {@code null}
-         * @return
-         */
-        @NonNull U setId(@NonNull String id) {
-            if (id == null) {
-                throw new IllegalArgumentException("id shouldn't be null");
-            }
-            mBaseImpl.setId(id);
-            return (U) this;
-        }
-
-        /**
-         * Set callback for the session.
-         *
-         * @param executor callback executor
-         * @param callback session callback.
-         * @return
-         */
-        @NonNull U setSessionCallback(@NonNull Executor executor, @NonNull C callback) {
-            if (executor == null) {
-                throw new IllegalArgumentException("executor shouldn't be null");
-            }
-            if (callback == null) {
-                throw new IllegalArgumentException("callback shouldn't be null");
-            }
-            mBaseImpl.setSessionCallback(executor, callback);
-            return (U) this;
-        }
-
-        /**
-         * Build {@link MediaSession2}.
-         *
-         * @return a new session
-         * @throws IllegalStateException if the session with the same id is already exists for the
-         *      package.
-         */
-        @NonNull T build() {
-            return mBaseImpl.build();
-        }
-
-        void setImpl(MediaSession2ImplBase.BuilderBase<T, C> impl) {
-            mBaseImpl = impl;
-        }
-    }
-
-    /**
      * Builder for {@link MediaSession2}.
      * <p>
      * Any incoming event from the {@link MediaController2} will be handled on the thread
@@ -733,7 +1183,7 @@
         }
 
         @Override
-        public @NonNull Builder setPlayer(@NonNull MediaPlayerBase player) {
+        public @NonNull Builder setPlayer(@NonNull MediaPlayerInterface player) {
             return super.setPlayer(player);
         }
 
@@ -775,20 +1225,18 @@
     public static final class ControllerInfo {
         private final int mUid;
         private final String mPackageName;
-        // Note: IMediaControllerCallback should be used only for MediaSession2ImplBase
-        private final IMediaControllerCallback mIControllerCallback;
         private final boolean mIsTrusted;
+        private final ControllerCb mControllerCb;
 
         /**
          * @hide
          */
         @RestrictTo(LIBRARY_GROUP)
-        public ControllerInfo(@NonNull Context context, int uid, int pid,
-                @NonNull String packageName, @NonNull IMediaControllerCallback callback) {
+        ControllerInfo(@NonNull String packageName, int pid, int uid, @NonNull ControllerCb cb) {
             mUid = uid;
             mPackageName = packageName;
-            mIControllerCallback = callback;
             mIsTrusted = false;
+            mControllerCb = cb;
         }
 
         /**
@@ -818,13 +1266,9 @@
             return mIsTrusted;
         }
 
-        IBinder getId() {
-            return mIControllerCallback.asBinder();
-        }
-
         @Override
         public int hashCode() {
-            return mIControllerCallback.hashCode();
+            return mControllerCb.hashCode();
         }
 
         @Override
@@ -833,7 +1277,7 @@
                 return false;
             }
             ControllerInfo other = (ControllerInfo) obj;
-            return mIControllerCallback.asBinder().equals(other.mIControllerCallback.asBinder());
+            return mControllerCb.equals(other.mControllerCb);
         }
 
         @Override
@@ -841,26 +1285,12 @@
             return "ControllerInfo {pkg=" + mPackageName + ", uid=" + mUid + "})";
         }
 
-        /**
-         * @hide
-         * @return Bundle
-         */
-        @RestrictTo(LIBRARY_GROUP)
-        public @NonNull Bundle toBundle() {
-            return new Bundle();
+        @NonNull IBinder getId() {
+            return mControllerCb.getId();
         }
 
-        /**
-         * @hide
-         * @return Bundle
-         */
-        @RestrictTo(LIBRARY_GROUP)
-        public static @NonNull ControllerInfo fromBundle(@NonNull Context context, Bundle bundle) {
-            return new ControllerInfo(context, -1, -1, "TODO", null);
-        }
-
-        IMediaControllerCallback getControllerBinder() {
-            return mIControllerCallback;
+        @NonNull ControllerCb getControllerCb() {
+            return mControllerCb;
         }
     }
 
@@ -1060,12 +1490,60 @@
         }
     }
 
+    abstract static class ControllerCb {
+        @Override
+        public int hashCode() {
+            return getId().hashCode();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof ControllerCb)) {
+                return false;
+            }
+            ControllerCb other = (ControllerCb) obj;
+            return getId().equals(other.getId());
+        }
+
+        abstract @NonNull IBinder getId();
+
+        // Mostly matched with the methods in MediaController2.ControllerCallback
+        abstract void onCustomLayoutChanged(@NonNull List<CommandButton> layout)
+                throws RemoteException;
+        abstract void onPlaybackInfoChanged(@NonNull PlaybackInfo info) throws RemoteException;
+        abstract void onAllowedCommandsChanged(@NonNull SessionCommandGroup2 commands)
+                throws RemoteException;
+        abstract void onCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args,
+                @Nullable ResultReceiver receiver) throws RemoteException;
+        abstract void onPlayerStateChanged(int playerState) throws RemoteException;
+        abstract void onPlaybackSpeedChanged(float speed) throws RemoteException;
+        abstract void onBufferingStateChanged(@NonNull MediaItem2 item,
+                @MediaPlayerInterface.BuffState int state) throws RemoteException;
+        abstract void onSeekCompleted(long position) throws RemoteException;
+        abstract void onError(@ErrorCode int errorCode, @Nullable Bundle extras)
+                throws RemoteException;
+        abstract void onCurrentMediaItemChanged(@Nullable MediaItem2 item) throws RemoteException;
+        abstract void onPlaylistChanged(@NonNull List<MediaItem2> playlist,
+                @Nullable MediaMetadata2 metadata) throws RemoteException;
+        abstract void onPlaylistMetadataChanged(@Nullable MediaMetadata2 metadata)
+                throws RemoteException;
+        abstract void onShuffleModeChanged(@MediaPlaylistAgent.ShuffleMode int shuffleMode)
+                throws RemoteException;
+        abstract void onRepeatModeChanged(@MediaPlaylistAgent.RepeatMode int repeatMode)
+                throws RemoteException;
+        abstract void onRoutesInfoChanged(@Nullable List<Bundle> routes) throws RemoteException;
+        abstract void onChildrenChanged(@NonNull  String parentId, int itemCount,
+                @Nullable Bundle extras) throws RemoteException;
+        abstract void onSearchResultChanged(@NonNull String query, int itemCount,
+                @Nullable Bundle extras) throws RemoteException;
+    }
+
     abstract static class SupportLibraryImpl extends MediaInterface2.SessionPlayer
             implements AutoCloseable {
-        abstract void updatePlayer(@NonNull MediaPlayerBase player,
+        abstract void updatePlayer(@NonNull MediaPlayerInterface player,
                 @Nullable MediaPlaylistAgent playlistAgent,
                 @Nullable VolumeProviderCompat volumeProvider);
-        abstract @NonNull MediaPlayerBase getPlayer();
+        abstract @NonNull MediaPlayerInterface getPlayer();
         abstract @NonNull MediaPlaylistAgent getPlaylistAgent();
         abstract @Nullable VolumeProviderCompat getVolumeProvider();
         abstract @NonNull SessionToken2 getToken();
@@ -1083,9 +1561,17 @@
         abstract void notifyRoutesInfoChanged(@NonNull ControllerInfo controller,
                 @Nullable List<Bundle> routes);
 
+        // LibrarySession methods
+        abstract void notifyChildrenChanged(@NonNull ControllerInfo controller,
+                @NonNull String parentId, int itemCount, @Nullable Bundle extras,
+                @NonNull List<MediaSessionManager.RemoteUserInfo> subscribingBrowsers);
+        abstract void notifySearchResultChanged(@NonNull ControllerInfo controller,
+                @NonNull String query, int itemCount, @Nullable Bundle extras);
+
         // Internally used methods
-        abstract void setInstance(MediaSession2 session);
+        abstract MediaSession2 createInstance();
         abstract MediaSession2 getInstance();
+        abstract MediaSessionCompat getSessionCompat();
         abstract Context getContext();
         abstract Executor getCallbackExecutor();
         abstract SessionCallback getCallback();
@@ -1094,577 +1580,152 @@
         abstract PlaybackInfo getPlaybackInfo();
     }
 
-    static final String TAG = "MediaSession2";
-
-    private final SupportLibraryImpl mImpl;
-
-    MediaSession2(SupportLibraryImpl impl) {
-        mImpl = impl;
-        mImpl.setInstance(this);
-    }
-
     /**
-     * Sets the underlying {@link MediaPlayerBase} and {@link MediaPlaylistAgent} for this session
-     * to dispatch incoming event to.
+     * Base builder class for MediaSession2 and its subclass. Any change in this class should be
+     * also applied to the subclasses {@link MediaSession2.Builder} and
+     * {@link MediaLibraryService2.MediaLibrarySession.Builder}.
      * <p>
-     * When a {@link MediaPlaylistAgent} is specified here, the playlist agent should manage
-     * {@link MediaPlayerBase} for calling {@link MediaPlayerBase#setNextDataSources(List)}.
+     * APIs here should be package private, but should have documentations for developers.
+     * Otherwise, javadoc will generate documentation with the generic types such as follows.
+     * <pre>U extends BuilderBase<T, U, C> setSessionCallback(Executor executor, C callback)</pre>
      * <p>
-     * If the {@link MediaPlaylistAgent} isn't set, session will recreate the default playlist
-     * agent.
-     *
-     * @param player a {@link MediaPlayerBase} that handles actual media playback in your app
-     * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the {@code player}
-     * @param volumeProvider a {@link VolumeProviderCompat}. If {@code null}, system will adjust the
-     *                       appropriate stream volume for this session's player.
+     * This class is hidden to prevent from generating test stub, which fails with
+     * 'unexpected bound' because it tries to auto generate stub class as follows.
+     * <pre>abstract static class BuilderBase<
+     *      T extends android.media.MediaSession2,
+     *      U extends android.media.MediaSession2.BuilderBase<
+     *              T, U, C extends android.media.MediaSession2.SessionCallback>, C></pre>
+     * @hide
      */
-    public void updatePlayer(@NonNull MediaPlayerBase player,
-            @Nullable MediaPlaylistAgent playlistAgent,
-            @Nullable VolumeProviderCompat volumeProvider) {
-        mImpl.updatePlayer(player, playlistAgent, volumeProvider);
-    }
+    @RestrictTo(LIBRARY_GROUP)
+    abstract static class BuilderBase
+            <T extends MediaSession2, U extends BuilderBase<T, U, C>, C extends SessionCallback> {
+        final Context mContext;
+        MediaSession2ImplBase.BuilderBase<T, C> mBaseImpl;
+        MediaPlayerInterface mPlayer;
+        String mId;
+        Executor mCallbackExecutor;
+        C mCallback;
+        MediaPlaylistAgent mPlaylistAgent;
+        VolumeProviderCompat mVolumeProvider;
+        PendingIntent mSessionActivity;
 
-    @Override
-    public void close() {
-        try {
-            mImpl.close();
-        } catch (Exception e) {
-            // Should not be here.
+        BuilderBase(Context context) {
+            if (context == null) {
+                throw new IllegalArgumentException("context shouldn't be null");
+            }
+            mContext = context;
+            // Ensure non-null
+            mId = "";
         }
-    }
 
-    /**
-     * @return player
-     */
-    public @NonNull MediaPlayerBase getPlayer() {
-        return mImpl.getPlayer();
-    }
+        /**
+         * Sets the underlying {@link MediaPlayerInterface} for this session to dispatch incoming
+         * event to.
+         *
+         * @param player a {@link MediaPlayerInterface} that handles actual media playback in your
+         *               app.
+         */
+        @NonNull U setPlayer(@NonNull MediaPlayerInterface player) {
+            if (player == null) {
+                throw new IllegalArgumentException("player shouldn't be null");
+            }
+            mBaseImpl.setPlayer(player);
+            return (U) this;
+        }
 
-    /**
-     * @return playlist agent
-     */
-    public @NonNull MediaPlaylistAgent getPlaylistAgent() {
-        return mImpl.getPlaylistAgent();
-    }
+        /**
+         * Sets the {@link MediaPlaylistAgent} for this session to manages playlist of the
+         * underlying {@link MediaPlayerInterface}. The playlist agent should manage
+         * {@link MediaPlayerInterface} for calling
+         * {@link MediaPlayerInterface#setNextDataSources(List)}.
+         * <p>
+         * If the {@link MediaPlaylistAgent} isn't set, session will create the default playlist
+         * agent.
+         *
+         * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the
+         *                      {@code player}
+         */
+        U setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
+            if (playlistAgent == null) {
+                throw new IllegalArgumentException("playlistAgent shouldn't be null");
+            }
+            mBaseImpl.setPlaylistAgent(playlistAgent);
+            return (U) this;
+        }
 
-    /**
-     * @return volume provider
-     */
-    public @Nullable VolumeProviderCompat getVolumeProvider() {
-        return mImpl.getVolumeProvider();
-    }
+        /**
+         * Sets the {@link VolumeProviderCompat} for this session to handle volume events. If not
+         * set, system will adjust the appropriate stream volume for this session's player.
+         *
+         * @param volumeProvider The provider that will receive volume button events.
+         */
+        @NonNull U setVolumeProvider(@Nullable VolumeProviderCompat volumeProvider) {
+            mBaseImpl.setVolumeProvider(volumeProvider);
+            return (U) this;
+        }
 
-    /**
-     * Returns the {@link SessionToken2} for creating {@link MediaController2}.
-     */
-    public @NonNull SessionToken2 getToken() {
-        return mImpl.getToken();
-    }
+        /**
+         * Set an intent for launching UI for this Session. This can be used as a
+         * quick link to an ongoing media screen. The intent should be for an
+         * activity that may be started using {@link Context#startActivity(Intent)}.
+         *
+         * @param pi The intent to launch to show UI for this session.
+         */
+        @NonNull U setSessionActivity(@Nullable PendingIntent pi) {
+            mBaseImpl.setSessionActivity(pi);
+            return (U) this;
+        }
 
-    @NonNull Context getContext() {
-        return mImpl.getContext();
-    }
+        /**
+         * Set ID of the session. If it's not set, an empty string with used to create a session.
+         * <p>
+         * Use this if and only if your app supports multiple playback at the same time and also
+         * wants to provide external apps to have finer controls of them.
+         *
+         * @param id id of the session. Must be unique per package.
+         * @throws IllegalArgumentException if id is {@code null}
+         * @return
+         */
+        @NonNull U setId(@NonNull String id) {
+            if (id == null) {
+                throw new IllegalArgumentException("id shouldn't be null");
+            }
+            mBaseImpl.setId(id);
+            return (U) this;
+        }
 
-    @NonNull Executor getCallbackExecutor() {
-        return mImpl.getCallbackExecutor();
-    }
+        /**
+         * Set callback for the session.
+         *
+         * @param executor callback executor
+         * @param callback session callback.
+         * @return
+         */
+        @NonNull U setSessionCallback(@NonNull Executor executor, @NonNull C callback) {
+            if (executor == null) {
+                throw new IllegalArgumentException("executor shouldn't be null");
+            }
+            if (callback == null) {
+                throw new IllegalArgumentException("callback shouldn't be null");
+            }
+            mBaseImpl.setSessionCallback(executor, callback);
+            return (U) this;
+        }
 
-    @NonNull SessionCallback getCallback() {
-        return mImpl.getCallback();
-    }
+        /**
+         * Build {@link MediaSession2}.
+         *
+         * @return a new session
+         * @throws IllegalStateException if the session with the same id is already exists for the
+         *      package.
+         */
+        @NonNull T build() {
+            return mBaseImpl.build();
+        }
 
-    /**
-     * Returns the list of connected controller.
-     *
-     * @return list of {@link ControllerInfo}
-     */
-    public @NonNull List<ControllerInfo> getConnectedControllers() {
-        return mImpl.getConnectedControllers();
-    }
-
-    /**
-     * Set the {@link AudioFocusRequest} to obtain the audio focus
-     *
-     * @param afr the full request parameters
-     */
-    public void setAudioFocusRequest(@Nullable AudioFocusRequest afr) {
-        mImpl.setAudioFocusRequest(afr);
-    }
-
-    /**
-     * Sets ordered list of {@link CommandButton} for controllers to build UI with it.
-     * <p>
-     * It's up to controller's decision how to represent the layout in its own UI.
-     * Here's the same way
-     * (layout[i] means a CommandButton at index i in the given list)
-     * For 5 icons row
-     *      layout[3] layout[1] layout[0] layout[2] layout[4]
-     * For 3 icons row
-     *      layout[1] layout[0] layout[2]
-     * For 5 icons row with overflow icon (can show +5 extra buttons with overflow button)
-     *      expanded row:   layout[5] layout[6] layout[7] layout[8] layout[9]
-     *      main row:       layout[3] layout[1] layout[0] layout[2] layout[4]
-     * <p>
-     * This API can be called in the
-     * {@link SessionCallback#onConnect(MediaSession2, ControllerInfo)}.
-     *
-     * @param controller controller to specify layout.
-     * @param layout ordered list of layout.
-     */
-    public void setCustomLayout(@NonNull ControllerInfo controller,
-            @NonNull List<CommandButton> layout) {
-        mImpl.setCustomLayout(controller, layout);
-    }
-
-    /**
-     * Set the new allowed command group for the controller
-     *
-     * @param controller controller to change allowed commands
-     * @param commands new allowed commands
-     */
-    public void setAllowedCommands(@NonNull ControllerInfo controller,
-            @NonNull SessionCommandGroup2 commands) {
-        mImpl.setAllowedCommands(controller, commands);
-    }
-
-    /**
-     * Send custom command to all connected controllers.
-     *
-     * @param command a command
-     * @param args optional argument
-     */
-    public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) {
-        mImpl.sendCustomCommand(command, args);
-    }
-
-    /**
-     * Send custom command to a specific controller.
-     *
-     * @param command a command
-     * @param args optional argument
-     * @param receiver result receiver for the session
-     */
-    public void sendCustomCommand(@NonNull ControllerInfo controller,
-            @NonNull SessionCommand2 command, @Nullable Bundle args,
-            @Nullable ResultReceiver receiver) {
-        mImpl.sendCustomCommand(controller, command, args, receiver);
-    }
-
-    /**
-     * Play playback.
-     * <p>
-     * This calls {@link MediaPlayerBase#play()}.
-     */
-    @Override
-    public void play() {
-        mImpl.play();
-    }
-
-    /**
-     * Pause playback.
-     * <p>
-     * This calls {@link MediaPlayerBase#pause()}.
-     */
-    @Override
-    public void pause() {
-        mImpl.pause();
-    }
-
-    /**
-     * Stop playback, and reset the player to the initial state.
-     * <p>
-     * This calls {@link MediaPlayerBase#reset()}.
-     */
-    @Override
-    public void reset() {
-        mImpl.reset();
-    }
-
-    /**
-     * Request that the player prepare its playback. In other words, other sessions can continue
-     * to play during the preparation of this session. This method can be used to speed up the
-     * start of the playback. Once the preparation is done, the session will change its playback
-     * state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called
-     * to start playback.
-     * <p>
-     * This calls {@link MediaPlayerBase#reset()}.
-     */
-    @Override
-    public void prepare() {
-        mImpl.prepare();
-    }
-
-    /**
-     * Move to a new location in the media stream.
-     *
-     * @param pos Position to move to, in milliseconds.
-     */
-    @Override
-    public void seekTo(long pos) {
-        mImpl.seekTo(pos);
-    }
-
-    /**
-     * @hide
-     */
-    @RestrictTo(LIBRARY_GROUP)
-    @Override
-    public void skipForward() {
-        mImpl.skipForward();
-    }
-
-    /**
-     * @hide
-     */
-    @RestrictTo(LIBRARY_GROUP)
-    @Override
-    public void skipBackward() {
-        mImpl.skipBackward();
-    }
-
-    /**
-     * Notify errors to the connected controllers
-     *
-     * @param errorCode error code
-     * @param extras extras
-     */
-    @Override
-    public void notifyError(@ErrorCode int errorCode, @Nullable Bundle extras) {
-        mImpl.notifyError(errorCode, extras);
-    }
-
-    /**
-     * Notify routes information to a connected controller
-     *
-     * @param controller controller information
-     * @param routes The routes information. Each bundle should be from
-     *              MediaRouteDescritor.asBundle().
-     */
-    public void notifyRoutesInfoChanged(@NonNull ControllerInfo controller,
-            @Nullable List<Bundle> routes) {
-        mImpl.notifyRoutesInfoChanged(controller, routes);
-    }
-
-    /**
-     * Gets the current player state.
-     *
-     * @return the current player state
-     */
-    @Override
-    public @PlayerState int getPlayerState() {
-        return mImpl.getPlayerState();
-    }
-
-    /**
-     * Gets the current position.
-     *
-     * @return the current playback position in ms, or {@link MediaPlayerBase#UNKNOWN_TIME} if
-     *         unknown.
-     */
-    @Override
-    public long getCurrentPosition() {
-        return mImpl.getCurrentPosition();
-    }
-
-    @Override
-    public long getDuration() {
-        return mImpl.getDuration();
-    }
-
-    /**
-     * Gets the buffered position, or {@link MediaPlayerBase#UNKNOWN_TIME} if unknown.
-     *
-     * @return the buffered position in ms, or {@link MediaPlayerBase#UNKNOWN_TIME}.
-     */
-    @Override
-    public long getBufferedPosition() {
-        return mImpl.getBufferedPosition();
-    }
-
-    /**
-     * Gets the current buffering state of the player.
-     * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
-     * buffered.
-     *
-     * @return the buffering state.
-     */
-    @Override
-    public @BuffState int getBufferingState() {
-        return mImpl.getBufferingState();
-    }
-
-    /**
-     * Get the playback speed.
-     *
-     * @return speed
-     */
-    @Override
-    public float getPlaybackSpeed() {
-        return mImpl.getPlaybackSpeed();
-    }
-
-    /**
-     * Set the playback speed.
-     */
-    @Override
-    public void setPlaybackSpeed(float speed) {
-        mImpl.setPlaybackSpeed(speed);
-    }
-
-    /**
-     * 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>
-     * 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
-     * when
-     * <ul>
-     *      <li>{@link MediaItem2} specified by {@link #setPlaylist(List, MediaMetadata2)} doesn't
-     *          have {@link DataSourceDesc}</li>
-     *      <li>{@link MediaController2#addPlaylistItem(int, MediaItem2)} is called and accepted
-     *          by {@link SessionCallback#onCommandRequest(
-     *          MediaSession2, ControllerInfo, SessionCommand2)}.
-     *          In that case, an item would be added automatically without the data source.</li>
-     * </ul>
-     * <p>
-     * If it's not set, playback wouldn't happen for the item without data source descriptor.
-     * <p>
-     * The helper will be run on the executor that was specified by
-     * {@link Builder#setSessionCallback(Executor, SessionCallback)}.
-     *
-     * @param helper a data source missing helper.
-     * @throws IllegalStateException when the helper is set when the playlist agent is set
-     * @see #setPlaylist(List, MediaMetadata2)
-     * @see SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)
-     * @see SessionCommand2#COMMAND_CODE_PLAYLIST_ADD_ITEM
-     * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REPLACE_ITEM
-     */
-    @Override
-    public void setOnDataSourceMissingHelper(@NonNull OnDataSourceMissingHelper helper) {
-        mImpl.setOnDataSourceMissingHelper(helper);
-    }
-
-    /**
-     * Clears the data source missing helper.
-     *
-     * @see #setOnDataSourceMissingHelper(OnDataSourceMissingHelper)
-     */
-    @Override
-    public void clearOnDataSourceMissingHelper() {
-        mImpl.clearOnDataSourceMissingHelper();
-    }
-
-    /**
-     * Returns the playlist from the {@link MediaPlaylistAgent}.
-     * <p>
-     * This list may differ with the list that was specified with
-     * {@link #setPlaylist(List, MediaMetadata2)} depending on the {@link MediaPlaylistAgent}
-     * implementation. Use media items returned here for other playlist agent APIs such as
-     * {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
-     *
-     * @return playlist
-     * @see MediaPlaylistAgent#getPlaylist()
-     * @see SessionCallback#onPlaylistChanged(
-     *          MediaSession2, MediaPlaylistAgent, List, MediaMetadata2)
-     */
-    @Override
-    public List<MediaItem2> getPlaylist() {
-        return mImpl.getPlaylist();
-    }
-
-    /**
-     * Sets a list of {@link MediaItem2} to the {@link MediaPlaylistAgent}. Ensure uniqueness of
-     * each {@link MediaItem2} in the playlist so the session can uniquely identity individual
-     * items.
-     * <p>
-     * This may be an asynchronous call, and {@link MediaPlaylistAgent} may keep the copy of the
-     * list. Wait for {@link SessionCallback#onPlaylistChanged(MediaSession2, MediaPlaylistAgent,
-     * List, MediaMetadata2)} to know the operation finishes.
-     * <p>
-     * 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.
-     *
-     * @param list A list of {@link MediaItem2} objects to set as a play list.
-     * @throws IllegalArgumentException if given list is {@code null}, or has duplicated media
-     * items.
-     * @see MediaPlaylistAgent#setPlaylist(List, MediaMetadata2)
-     * @see SessionCallback#onPlaylistChanged(
-     *          MediaSession2, MediaPlaylistAgent, List, MediaMetadata2)
-     * @see #setOnDataSourceMissingHelper
-     */
-    @Override
-    public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
-        mImpl.setPlaylist(list, metadata);
-    }
-
-    /**
-     * Skips to the item in the playlist.
-     * <p>
-     * This calls {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)} and the behavior depends
-     * on the playlist agent implementation, especially with the shuffle/repeat mode.
-     *
-     * @param item The item in the playlist you want to play
-     * @see #getShuffleMode()
-     * @see #getRepeatMode()
-     */
-    @Override
-    public void skipToPlaylistItem(@NonNull MediaItem2 item) {
-        mImpl.skipToPlaylistItem(item);
-    }
-
-    /**
-     * Skips to the previous item.
-     * <p>
-     * This calls {@link MediaPlaylistAgent#skipToPreviousItem()} and the behavior depends on the
-     * playlist agent implementation, especially with the shuffle/repeat mode.
-     *
-     * @see #getShuffleMode()
-     * @see #getRepeatMode()
-     **/
-    @Override
-    public void skipToPreviousItem() {
-        mImpl.skipToPreviousItem();
-    }
-
-    /**
-     * Skips to the next item.
-     * <p>
-     * This calls {@link MediaPlaylistAgent#skipToNextItem()} and the behavior depends on the
-     * playlist agent implementation, especially with the shuffle/repeat mode.
-     *
-     * @see #getShuffleMode()
-     * @see #getRepeatMode()
-     */
-    @Override
-    public void skipToNextItem() {
-        mImpl.skipToNextItem();
-    }
-
-    /**
-     * Gets the playlist metadata from the {@link MediaPlaylistAgent}.
-     *
-     * @return the playlist metadata
-     */
-    @Override
-    public MediaMetadata2 getPlaylistMetadata() {
-        return mImpl.getPlaylistMetadata();
-    }
-
-    /**
-     * Adds the media item to the playlist at position index. Index equals or greater than
-     * 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,
-     * the current index of the play list will be incremented correspondingly.
-     *
-     * @param index the index you want to add
-     * @param item the media item you want to add
-     */
-    @Override
-    public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
-        mImpl.addPlaylistItem(index, item);
-    }
-
-    /**
-     * Removes the media item in the playlist.
-     * <p>
-     * If the item is the currently playing item of the playlist, current playback
-     * will be stopped and playback moves to next source in the list.
-     *
-     * @param item the media item you want to add
-     */
-    @Override
-    public void removePlaylistItem(@NonNull MediaItem2 item) {
-        mImpl.removePlaylistItem(item);
-    }
-
-    /**
-     * Replaces the media item at index in the playlist. This can be also used to update metadata of
-     * an item.
-     *
-     * @param index the index of the item to replace
-     * @param item the new item
-     */
-    @Override
-    public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
-        mImpl.replacePlaylistItem(index, item);
-    }
-
-    /**
-     * Return currently playing media item.
-     *
-     * @return currently playing media item
-     */
-    @Override
-    public MediaItem2 getCurrentMediaItem() {
-        return mImpl.getCurrentMediaItem();
-    }
-
-    /**
-     * Updates the playlist metadata to the {@link MediaPlaylistAgent}.
-     *
-     * @param metadata metadata of the playlist
-     */
-    @Override
-    public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
-        mImpl.updatePlaylistMetadata(metadata);
-    }
-
-    /**
-     * Gets the repeat mode from the {@link MediaPlaylistAgent}.
-     *
-     * @return repeat mode
-     * @see MediaPlaylistAgent#REPEAT_MODE_NONE
-     * @see MediaPlaylistAgent#REPEAT_MODE_ONE
-     * @see MediaPlaylistAgent#REPEAT_MODE_ALL
-     * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
-     */
-    @Override
-    public @RepeatMode int getRepeatMode() {
-        return mImpl.getRepeatMode();
-    }
-
-    /**
-     * Sets the repeat mode to the {@link MediaPlaylistAgent}.
-     *
-     * @param repeatMode repeat mode
-     * @see MediaPlaylistAgent#REPEAT_MODE_NONE
-     * @see MediaPlaylistAgent#REPEAT_MODE_ONE
-     * @see MediaPlaylistAgent#REPEAT_MODE_ALL
-     * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
-     */
-    @Override
-    public void setRepeatMode(@RepeatMode int repeatMode) {
-        mImpl.setRepeatMode(repeatMode);
-    }
-
-    /**
-     * Gets the shuffle mode from the {@link MediaPlaylistAgent}.
-     *
-     * @return The shuffle mode
-     * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
-     * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
-     * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
-     */
-    @Override
-    public @ShuffleMode int getShuffleMode() {
-        return mImpl.getShuffleMode();
-    }
-
-    /**
-     * Sets the shuffle mode to the {@link MediaPlaylistAgent}.
-     *
-     * @param shuffleMode The shuffle mode
-     * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
-     * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
-     * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
-     */
-    @Override
-    public void setShuffleMode(@ShuffleMode int shuffleMode) {
-        mImpl.setShuffleMode(shuffleMode);
+        void setImpl(MediaSession2ImplBase.BuilderBase<T, C> impl) {
+            mBaseImpl = impl;
+        }
     }
 }
diff --git a/media/src/main/java/androidx/media/MediaSession2ImplBase.java b/media/src/main/java/androidx/media/MediaSession2ImplBase.java
index e474b45..1e768a0 100644
--- a/media/src/main/java/androidx/media/MediaSession2ImplBase.java
+++ b/media/src/main/java/androidx/media/MediaSession2ImplBase.java
@@ -16,7 +16,8 @@
 
 package androidx.media;
 
-import static androidx.media.MediaPlayerBase.BUFFERING_STATE_UNKNOWN;
+import static androidx.media.MediaPlayerInterface.BUFFERING_STATE_UNKNOWN;
+import static androidx.media.MediaSession2.ControllerCb;
 import static androidx.media.MediaSession2.ControllerInfo;
 import static androidx.media.MediaSession2.ErrorCode;
 import static androidx.media.MediaSession2.OnDataSourceMissingHelper;
@@ -35,9 +36,11 @@
 import android.media.AudioManager;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.DeadObjectException;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Process;
+import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.support.v4.media.session.MediaSessionCompat;
 import android.support.v4.media.session.PlaybackStateCompat;
@@ -47,8 +50,9 @@
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.core.util.ObjectsCompat;
 import androidx.media.MediaController2.PlaybackInfo;
-import androidx.media.MediaPlayerBase.PlayerEventCallback;
+import androidx.media.MediaPlayerInterface.PlayerEventCallback;
 import androidx.media.MediaPlaylistAgent.PlaylistEventCallback;
 
 import java.lang.ref.WeakReference;
@@ -68,19 +72,19 @@
     private final HandlerThread mHandlerThread;
     private final Handler mHandler;
     private final MediaSessionCompat mSessionCompat;
-    private final MediaSession2StubImplBase mSession2Stub;
+    private final MediaSession2Stub mSession2Stub;
+    private final MediaSessionLegacyStub mSessionLegacyStub;
     private final String mId;
     private final Executor mCallbackExecutor;
     private final SessionCallback mCallback;
     private final SessionToken2 mSessionToken;
     private final AudioManager mAudioManager;
-    private final MediaPlayerBase.PlayerEventCallback mPlayerEventCallback;
+    private final MediaPlayerInterface.PlayerEventCallback mPlayerEventCallback;
     private final MediaPlaylistAgent.PlaylistEventCallback mPlaylistEventCallback;
-
-    private WeakReference<MediaSession2> mInstance;
+    private final MediaSession2 mInstance;
 
     @GuardedBy("mLock")
-    private MediaPlayerBase mPlayer;
+    private MediaPlayerInterface mPlayer;
     @GuardedBy("mLock")
     private MediaPlaylistAgent mPlaylistAgent;
     @GuardedBy("mLock")
@@ -90,21 +94,21 @@
     @GuardedBy("mLock")
     private OnDataSourceMissingHelper mDsmHelper;
     @GuardedBy("mLock")
-    private PlaybackStateCompat mPlaybackStateCompat;
-    @GuardedBy("mLock")
     private PlaybackInfo mPlaybackInfo;
 
     MediaSession2ImplBase(Context context, MediaSessionCompat sessionCompat, String id,
-            MediaPlayerBase player, MediaPlaylistAgent playlistAgent,
+            MediaPlayerInterface player, MediaPlaylistAgent playlistAgent,
             VolumeProviderCompat volumeProvider, PendingIntent sessionActivity,
             Executor callbackExecutor, SessionCallback callback) {
         mContext = context;
+        mInstance = createInstance();
         mHandlerThread = new HandlerThread("MediaController2_Thread");
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
 
         mSessionCompat = sessionCompat;
-        mSession2Stub = new MediaSession2StubImplBase(this);
+        mSession2Stub = new MediaSession2Stub(this);
+        mSessionLegacyStub = new MediaSessionLegacyStub(this);
         mSessionCompat.setCallback(mSession2Stub, mHandler);
         mSessionCompat.setSessionActivity(sessionActivity);
 
@@ -113,7 +117,6 @@
         mCallbackExecutor = callbackExecutor;
         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
 
-        // TODO: Set callback values properly
         mPlayerEventCallback = new MyPlayerEventCallback(this);
         mPlaylistEventCallback = new MyPlaylistEventCallback(this);
 
@@ -137,16 +140,22 @@
     }
 
     @Override
-    public void updatePlayer(@NonNull MediaPlayerBase player,
+    public void updatePlayer(@NonNull MediaPlayerInterface player,
             @Nullable MediaPlaylistAgent playlistAgent,
             @Nullable VolumeProviderCompat volumeProvider) {
         if (player == null) {
             throw new IllegalArgumentException("player shouldn't be null");
         }
-        final MediaPlayerBase oldPlayer;
+        final boolean hasPlayerChanged;
+        final boolean hasAgentChanged;
+        final boolean hasPlaybackInfoChanged;
+        final MediaPlayerInterface oldPlayer;
         final MediaPlaylistAgent oldAgent;
         final PlaybackInfo info = createPlaybackInfo(volumeProvider, player.getAudioAttributes());
         synchronized (mLock) {
+            hasPlayerChanged = (mPlayer != player);
+            hasAgentChanged = (mPlaylistAgent != playlistAgent);
+            hasPlaybackInfoChanged = (mPlaybackInfo != info);
             oldPlayer = mPlayer;
             oldAgent = mPlaylistAgent;
             mPlayer = player;
@@ -161,6 +170,10 @@
             mVolumeProvider = volumeProvider;
             mPlaybackInfo = info;
         }
+        if (volumeProvider == null) {
+            int stream = getLegacyStreamType(player.getAudioAttributes());
+            mSessionCompat.setPlaybackToLocal(stream);
+        }
         if (player != oldPlayer) {
             player.registerPlayerEventCallback(mCallbackExecutor, mPlayerEventCallback);
             if (oldPlayer != null) {
@@ -171,35 +184,40 @@
         if (playlistAgent != oldAgent) {
             playlistAgent.registerPlaylistEventCallback(mCallbackExecutor, mPlaylistEventCallback);
             if (oldAgent != null) {
-                // Warning: Poorly implement player may ignore this
+                // Warning: Poorly implement agent may ignore this
                 oldAgent.unregisterPlaylistEventCallback(mPlaylistEventCallback);
             }
         }
 
         if (oldPlayer != null) {
-            mSession2Stub.notifyPlaybackInfoChanged(info);
-            notifyPlayerUpdatedNotLocked(oldPlayer);
+            // If it's not the first updatePlayer(), tell changes in the player, agent, and playback
+            // info.
+            if (hasAgentChanged) {
+                // Update agent first. Otherwise current position may be changed off the current
+                // media item's duration, and controller may consider it as a bug.
+                notifyAgentUpdatedNotLocked(oldAgent);
+            }
+            if (hasPlayerChanged) {
+                notifyPlayerUpdatedNotLocked(oldPlayer);
+            }
+            if (hasPlaybackInfoChanged) {
+                // Currently hasPlaybackInfo is always true, but check this in case that we're
+                // adding PlaybackInfo#equals().
+                notifyToAllControllers(new NotifyRunnable() {
+                    @Override
+                    public void run(ControllerCb callback) throws RemoteException {
+                        callback.onPlaybackInfoChanged(info);
+                    }
+                });
+            }
         }
-        // TODO(jaewan): Repeat the same thing for the playlist agent.
     }
 
     private PlaybackInfo createPlaybackInfo(VolumeProviderCompat volumeProvider,
             AudioAttributesCompat attrs) {
         PlaybackInfo info;
         if (volumeProvider == null) {
-            int stream;
-            if (attrs == null) {
-                stream = AudioManager.STREAM_MUSIC;
-            } else {
-                stream = attrs.getVolumeControlStream();
-                if (stream == AudioManager.USE_DEFAULT_STREAM_TYPE) {
-                    // It may happen if the AudioAttributes doesn't have usage.
-                    // Change it to the STREAM_MUSIC because it's not supported by audio manager
-                    // for querying volume level.
-                    stream = AudioManager.STREAM_MUSIC;
-                }
-            }
-
+            int stream = getLegacyStreamType(attrs);
             int controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
             if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
                 controlType = VolumeProviderCompat.VOLUME_CONTROL_FIXED;
@@ -221,6 +239,23 @@
         return info;
     }
 
+    private int getLegacyStreamType(@Nullable AudioAttributesCompat attrs) {
+        int stream;
+        if (attrs == null) {
+            stream = AudioManager.STREAM_MUSIC;
+        } else {
+            stream = attrs.getLegacyStreamType();
+            if (stream == AudioManager.USE_DEFAULT_STREAM_TYPE) {
+                // Usually, AudioAttributesCompat#getLegacyStreamType() does not return
+                // USE_DEFAULT_STREAM_TYPE unless the developer sets it with
+                // AudioAttributesCompat.Builder#setLegacyStreamType().
+                // But for safety, let's convert USE_DEFAULT_STREAM_TYPE to STREAM_MUSIC here.
+                stream = AudioManager.STREAM_MUSIC;
+            }
+        }
+        return stream;
+    }
+
     @Override
     public void close() {
         synchronized (mLock) {
@@ -232,13 +267,17 @@
             mSessionCompat.release();
             mHandler.removeCallbacksAndMessages(null);
             if (mHandlerThread.isAlive()) {
-                mHandlerThread.quitSafely();
+                if (Build.VERSION.SDK_INT >= 18) {
+                    mHandlerThread.quitSafely();
+                } else {
+                    mHandlerThread.quit();
+                }
             }
         }
     }
 
     @Override
-    public @NonNull MediaPlayerBase getPlayer() {
+    public @NonNull MediaPlayerInterface getPlayer() {
         synchronized (mLock) {
             return mPlayer;
         }
@@ -264,31 +303,34 @@
     }
 
     @Override
-    public @NonNull List<MediaSession2.ControllerInfo> getConnectedControllers() {
+    public @NonNull List<ControllerInfo> getConnectedControllers() {
         return mSession2Stub.getConnectedControllers();
     }
 
     @Override
     public void setAudioFocusRequest(@Nullable AudioFocusRequest afr) {
-        // TODO(jaewan): implement this (b/72529899)
-        // mProvider.setAudioFocusRequest_impl(focusGain);
     }
 
     @Override
     public void setCustomLayout(@NonNull ControllerInfo controller,
-            @NonNull List<MediaSession2.CommandButton> layout) {
+            @NonNull final List<MediaSession2.CommandButton> layout) {
         if (controller == null) {
             throw new IllegalArgumentException("controller shouldn't be null");
         }
         if (layout == null) {
             throw new IllegalArgumentException("layout shouldn't be null");
         }
-        mSession2Stub.notifyCustomLayout(controller, layout);
+        notifyToController(controller, new NotifyRunnable() {
+            @Override
+            public void run(ControllerCb callback) throws RemoteException {
+                callback.onCustomLayoutChanged(layout);
+            }
+        });
     }
 
     @Override
     public void setAllowedCommands(@NonNull ControllerInfo controller,
-            @NonNull SessionCommandGroup2 commands) {
+            @NonNull final SessionCommandGroup2 commands) {
         if (controller == null) {
             throw new IllegalArgumentException("controller shouldn't be null");
         }
@@ -296,32 +338,49 @@
             throw new IllegalArgumentException("commands shouldn't be null");
         }
         mSession2Stub.setAllowedCommands(controller, commands);
+        notifyToController(controller, new NotifyRunnable() {
+            @Override
+            public void run(ControllerCb callback) throws RemoteException {
+                callback.onAllowedCommandsChanged(commands);
+            }
+        });
     }
 
     @Override
-    public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) {
+    public void sendCustomCommand(@NonNull final SessionCommand2 command,
+            @Nullable final Bundle args) {
         if (command == null) {
             throw new IllegalArgumentException("command shouldn't be null");
         }
-        mSession2Stub.sendCustomCommand(command, args);
+        notifyToAllControllers(new NotifyRunnable() {
+            @Override
+            public void run(ControllerCb callback) throws RemoteException {
+                callback.onCustomCommand(command, args, null);
+            }
+        });
     }
 
     @Override
     public void sendCustomCommand(@NonNull ControllerInfo controller,
-            @NonNull SessionCommand2 command, @Nullable Bundle args,
-            @Nullable ResultReceiver receiver) {
+            @NonNull final SessionCommand2 command, @Nullable final Bundle args,
+            @Nullable final ResultReceiver receiver) {
         if (controller == null) {
             throw new IllegalArgumentException("controller shouldn't be null");
         }
         if (command == null) {
             throw new IllegalArgumentException("command shouldn't be null");
         }
-        mSession2Stub.sendCustomCommand(controller, command, args, receiver);
+        notifyToController(controller, new NotifyRunnable() {
+            @Override
+            public void run(ControllerCb callback) throws RemoteException {
+                callback.onCustomCommand(command, args, receiver);
+            }
+        });
     }
 
     @Override
     public void play() {
-        MediaPlayerBase player;
+        MediaPlayerInterface player;
         synchronized (mLock) {
             player = mPlayer;
         }
@@ -334,7 +393,7 @@
 
     @Override
     public void pause() {
-        MediaPlayerBase player;
+        MediaPlayerInterface player;
         synchronized (mLock) {
             player = mPlayer;
         }
@@ -347,7 +406,7 @@
 
     @Override
     public void reset() {
-        MediaPlayerBase player;
+        MediaPlayerInterface player;
         synchronized (mLock) {
             player = mPlayer;
         }
@@ -360,7 +419,7 @@
 
     @Override
     public void prepare() {
-        MediaPlayerBase player;
+        MediaPlayerInterface player;
         synchronized (mLock) {
             player = mPlayer;
         }
@@ -373,7 +432,7 @@
 
     @Override
     public void seekTo(long pos) {
-        MediaPlayerBase player;
+        MediaPlayerInterface player;
         synchronized (mLock) {
             player = mPlayer;
         }
@@ -395,19 +454,29 @@
     }
 
     @Override
-    public void notifyError(@ErrorCode int errorCode, @Nullable Bundle extras) {
-        mSession2Stub.notifyError(errorCode, extras);
+    public void notifyError(@ErrorCode final int errorCode, @Nullable final Bundle extras) {
+        notifyToAllControllers(new NotifyRunnable() {
+            @Override
+            public void run(ControllerCb callback) throws RemoteException {
+                callback.onError(errorCode, extras);
+            }
+        });
     }
 
     @Override
     public void notifyRoutesInfoChanged(@NonNull ControllerInfo controller,
-            @Nullable List<Bundle> routes) {
-        mSession2Stub.notifyRoutesInfoChanged(controller, routes);
+            @Nullable final List<Bundle> routes) {
+        notifyToController(controller, new NotifyRunnable() {
+            @Override
+            public void run(ControllerCb callback) throws RemoteException {
+                callback.onRoutesInfoChanged(routes);
+            }
+        });
     }
 
     @Override
-    public @MediaPlayerBase.PlayerState int getPlayerState() {
-        MediaPlayerBase player;
+    public @MediaPlayerInterface.PlayerState int getPlayerState() {
+        MediaPlayerInterface player;
         synchronized (mLock) {
             player = mPlayer;
         }
@@ -416,12 +485,12 @@
         } else if (DEBUG) {
             Log.d(TAG, "API calls after the close()", new IllegalStateException());
         }
-        return MediaPlayerBase.PLAYER_STATE_ERROR;
+        return MediaPlayerInterface.PLAYER_STATE_ERROR;
     }
 
     @Override
     public long getCurrentPosition() {
-        MediaPlayerBase player;
+        MediaPlayerInterface player;
         synchronized (mLock) {
             player = mPlayer;
         }
@@ -430,18 +499,28 @@
         } else if (DEBUG) {
             Log.d(TAG, "API calls after the close()", new IllegalStateException());
         }
-        return MediaPlayerBase.UNKNOWN_TIME;
+        return MediaPlayerInterface.UNKNOWN_TIME;
     }
 
     @Override
     public long getDuration() {
-        // TODO: implement
-        return 0;
+        MediaPlayerInterface player;
+        synchronized (mLock) {
+            player = mPlayer;
+        }
+        if (player != null) {
+            // Note: This should be the same as
+            // getCurrentMediaItem().getMetadata().getLong(METADATA_KEY_DURATION)
+            return player.getDuration();
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+        return MediaPlayerInterface.UNKNOWN_TIME;
     }
 
     @Override
     public long getBufferedPosition() {
-        MediaPlayerBase player;
+        MediaPlayerInterface player;
         synchronized (mLock) {
             player = mPlayer;
         }
@@ -450,12 +529,12 @@
         } else if (DEBUG) {
             Log.d(TAG, "API calls after the close()", new IllegalStateException());
         }
-        return MediaPlayerBase.UNKNOWN_TIME;
+        return MediaPlayerInterface.UNKNOWN_TIME;
     }
 
     @Override
-    public @MediaPlayerBase.BuffState int getBufferingState() {
-        MediaPlayerBase player;
+    public @MediaPlayerInterface.BuffState int getBufferingState() {
+        MediaPlayerInterface player;
         synchronized (mLock) {
             player = mPlayer;
         }
@@ -469,7 +548,7 @@
 
     @Override
     public float getPlaybackSpeed() {
-        MediaPlayerBase player;
+        MediaPlayerInterface player;
         synchronized (mLock) {
             player = mPlayer;
         }
@@ -483,7 +562,7 @@
 
     @Override
     public void setPlaybackSpeed(float speed) {
-        MediaPlayerBase player;
+        MediaPlayerInterface player;
         synchronized (mLock) {
             player = mPlayer;
         }
@@ -740,18 +819,63 @@
     }
 
     ///////////////////////////////////////////////////
-    // package private and private methods
+    // LibrarySession Methods
     ///////////////////////////////////////////////////
 
     @Override
-    void setInstance(MediaSession2 session) {
-        mInstance = new WeakReference<>(session);
+    void notifyChildrenChanged(ControllerInfo controller, final String parentId,
+            final int itemCount, final Bundle extras,
+            List<MediaSessionManager.RemoteUserInfo> subscribingBrowsers) {
+        if (controller == null) {
+            throw new IllegalArgumentException("controller shouldn't be null");
+        }
+        if (TextUtils.isEmpty(parentId)) {
+            throw new IllegalArgumentException("query shouldn't be empty");
+        }
 
+        // Notify controller only if it has subscribed the parentId.
+        for (MediaSessionManager.RemoteUserInfo info : subscribingBrowsers) {
+            if (info.getPackageName().equals(controller.getPackageName())
+                    && info.getUid() == controller.getUid()) {
+                notifyToController(controller, new NotifyRunnable() {
+                    @Override
+                    public void run(ControllerCb callback) throws RemoteException {
+                        callback.onChildrenChanged(parentId, itemCount, extras);
+                    }
+                });
+                return;
+            }
+        }
     }
 
     @Override
-    MediaSession2 getInstance() {
-        return mInstance.get();
+    void notifySearchResultChanged(ControllerInfo controller, final String query,
+            final int itemCount, final Bundle extras) {
+        if (controller == null) {
+            throw new IllegalArgumentException("controller shouldn't be null");
+        }
+        if (TextUtils.isEmpty(query)) {
+            throw new IllegalArgumentException("query shouldn't be empty");
+        }
+        notifyToController(controller, new NotifyRunnable() {
+            @Override
+            public void run(ControllerCb callback) throws RemoteException {
+                callback.onSearchResultChanged(query, itemCount, extras);
+            }
+        });
+    }
+
+    ///////////////////////////////////////////////////
+    // package private and private methods
+    ///////////////////////////////////////////////////
+    @Override
+    MediaSession2 createInstance() {
+        return new MediaSession2(this);
+    }
+
+    @Override
+    @NonNull MediaSession2 getInstance() {
+        return mInstance;
     }
 
     @Override
@@ -770,6 +894,11 @@
     }
 
     @Override
+    MediaSessionCompat getSessionCompat() {
+        return mSessionCompat;
+    }
+
+    @Override
     boolean isClosed() {
         return !mHandlerThread.isAlive();
     }
@@ -779,12 +908,6 @@
         synchronized (mLock) {
             int state = MediaUtils2.createPlaybackStateCompatState(getPlayerState(),
                     getBufferingState());
-            // TODO: Consider following missing stuff
-            //       - setCustomAction(): Fill custom layout
-            //       - setErrorMessage(): Fill error message when notifyError() is called.
-            //       - setActiveQueueItemId(): Fill here with the current media item...
-            //       - setExtra(): No idea at this moment.
-            // TODO: generate actions from the allowed commands.
             long allActions = PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_PAUSE
                     | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_REWIND
                     | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
@@ -816,11 +939,6 @@
             return mPlaybackInfo;
         }
     }
-
-    MediaSession2StubImplBase getSession2Stub() {
-        return mSession2Stub;
-    }
-
     private static String getServiceName(Context context, String serviceAction, String id) {
         PackageManager manager = context.getPackageManager();
         Intent serviceIntent = new Intent(serviceAction);
@@ -846,105 +964,203 @@
         return serviceName;
     }
 
-    private void notifyPlayerUpdatedNotLocked(MediaPlayerBase oldPlayer) {
-        MediaPlayerBase player;
-        synchronized (mLock) {
-            player = mPlayer;
+    private void notifyAgentUpdatedNotLocked(MediaPlaylistAgent oldAgent) {
+        // Tells the playlist change first, to current item can change be notified with an item
+        // within the playlist.
+        List<MediaItem2> oldPlaylist = oldAgent.getPlaylist();
+        final List<MediaItem2> newPlaylist = getPlaylist();
+        if (!ObjectsCompat.equals(oldPlaylist, newPlaylist)) {
+            notifyToAllControllers(new NotifyRunnable() {
+                @Override
+                public void run(ControllerCb callback) throws RemoteException {
+                    callback.onPlaylistChanged(
+                            newPlaylist, getPlaylistMetadata());
+                }
+            });
+        } else {
+            MediaMetadata2 oldMetadata = oldAgent.getPlaylistMetadata();
+            final MediaMetadata2 newMetadata = getPlaylistMetadata();
+            if (!ObjectsCompat.equals(oldMetadata, newMetadata)) {
+                notifyToAllControllers(new NotifyRunnable() {
+                    @Override
+                    public void run(ControllerCb callback) throws RemoteException {
+                        callback.onPlaylistMetadataChanged(newMetadata);
+                    }
+                });
+            }
         }
-        // TODO(jaewan): (Can be post-P) Find better way for player.getPlayerState() //
-        //               In theory, Session.getXXX() may not be the same as Player.getXXX()
-        //               and we should notify information of the session.getXXX() instead of
-        //               player.getXXX()
-        // Notify to controllers as well.
-        final int state = player.getPlayerState();
-        if (state != oldPlayer.getPlayerState()) {
-            // TODO: implement
-            mSession2Stub.notifyPlayerStateChanged(state);
+        MediaItem2 oldCurrentItem = oldAgent.getCurrentMediaItem();
+        final MediaItem2 newCurrentItem = getCurrentMediaItem();
+        if (!ObjectsCompat.equals(oldCurrentItem, newCurrentItem)) {
+            notifyToAllControllers(new NotifyRunnable() {
+                @Override
+                public void run(ControllerCb callback) throws RemoteException {
+                    callback.onCurrentMediaItemChanged(newCurrentItem);
+                }
+            });
         }
+        final int repeatMode = getRepeatMode();
+        if (oldAgent.getRepeatMode() != repeatMode) {
+            notifyToAllControllers(new NotifyRunnable() {
+                @Override
+                public void run(ControllerCb callback) throws RemoteException {
+                    callback.onRepeatModeChanged(repeatMode);
+                }
+            });
+        }
+        final int shuffleMode = getShuffleMode();
+        if (oldAgent.getShuffleMode() != shuffleMode) {
+            notifyToAllControllers(new NotifyRunnable() {
+                @Override
+                public void run(ControllerCb callback) throws RemoteException {
+                    callback.onShuffleModeChanged(shuffleMode);
+                }
+            });
+        }
+    }
 
-        final long currentTimeMs = System.currentTimeMillis();
-        final long position = player.getCurrentPosition();
-        if (position != oldPlayer.getCurrentPosition()) {
-            // TODO: implement
-            //mSession2Stub.notifyPositionChangedNotLocked(currentTimeMs, position);
+    private void notifyPlayerUpdatedNotLocked(MediaPlayerInterface oldPlayer) {
+        // Always forcefully send the player state and buffered state to send the current position
+        // and buffered position.
+        final int playerState = getPlayerState();
+        notifyToAllControllers(new NotifyRunnable() {
+            @Override
+            public void run(ControllerCb callback) throws RemoteException {
+                callback.onPlayerStateChanged(playerState);
+            }
+        });
+        final MediaItem2 item = getCurrentMediaItem();
+        if (item != null) {
+            final int bufferingState = getBufferingState();
+            notifyToAllControllers(new NotifyRunnable() {
+                @Override
+                public void run(ControllerCb callback) throws RemoteException {
+                    callback.onBufferingStateChanged(item, bufferingState);
+                }
+            });
         }
-
-        final float speed = player.getPlaybackSpeed();
+        final float speed = getPlaybackSpeed();
         if (speed != oldPlayer.getPlaybackSpeed()) {
-            // TODO: implement
-            //mSession2Stub.notifyPlaybackSpeedChangedNotLocked(speed);
+            notifyToAllControllers(new NotifyRunnable() {
+                @Override
+                public void run(ControllerCb callback) throws RemoteException {
+                    callback.onPlaybackSpeedChanged(speed);
+                }
+            });
         }
-
-        final long bufferedPosition = player.getBufferedPosition();
-        if (bufferedPosition != oldPlayer.getBufferedPosition()) {
-            // TODO: implement
-            //mSession2Stub.notifyBufferedPositionChangedNotLocked(bufferedPosition);
-        }
+        // Note: AudioInfo is updated outside of this API.
     }
 
     private void notifyPlaylistChangedOnExecutor(MediaPlaylistAgent playlistAgent,
-            List<MediaItem2> list, MediaMetadata2 metadata) {
+            final List<MediaItem2> list, final MediaMetadata2 metadata) {
         synchronized (mLock) {
             if (playlistAgent != mPlaylistAgent) {
                 // Ignore calls from the old agent.
                 return;
             }
         }
-        MediaSession2 session2 = mInstance.get();
-        if (session2 != null) {
-            mCallback.onPlaylistChanged(session2, playlistAgent, list, metadata);
-            mSession2Stub.notifyPlaylistChanged(list, metadata);
-        }
+        mCallback.onPlaylistChanged(mInstance, playlistAgent, list, metadata);
+        notifyToAllControllers(new NotifyRunnable() {
+            @Override
+            public void run(ControllerCb callback) throws RemoteException {
+                callback.onPlaylistChanged(list, metadata);
+            }
+        });
     }
 
     private void notifyPlaylistMetadataChangedOnExecutor(MediaPlaylistAgent playlistAgent,
-            MediaMetadata2 metadata) {
+            final MediaMetadata2 metadata) {
         synchronized (mLock) {
             if (playlistAgent != mPlaylistAgent) {
                 // Ignore calls from the old agent.
                 return;
             }
         }
-        MediaSession2 session2 = mInstance.get();
-        if (session2 != null) {
-            mCallback.onPlaylistMetadataChanged(session2, playlistAgent, metadata);
-            mSession2Stub.notifyPlaylistMetadataChanged(metadata);
-        }
+        mCallback.onPlaylistMetadataChanged(mInstance, playlistAgent, metadata);
+        notifyToAllControllers(new NotifyRunnable() {
+            @Override
+            public void run(ControllerCb callback) throws RemoteException {
+                callback.onPlaylistMetadataChanged(metadata);
+            }
+        });
     }
 
     private void notifyRepeatModeChangedOnExecutor(MediaPlaylistAgent playlistAgent,
-            int repeatMode) {
+            final int repeatMode) {
         synchronized (mLock) {
             if (playlistAgent != mPlaylistAgent) {
                 // Ignore calls from the old agent.
                 return;
             }
         }
-        MediaSession2 session2 = mInstance.get();
-        if (session2 != null) {
-            mCallback.onRepeatModeChanged(session2, playlistAgent, repeatMode);
-            mSession2Stub.notifyRepeatModeChanged(repeatMode);
-        }
+        mCallback.onRepeatModeChanged(mInstance, playlistAgent, repeatMode);
+        notifyToAllControllers(new NotifyRunnable() {
+            @Override
+            public void run(ControllerCb callback) throws RemoteException {
+                callback.onRepeatModeChanged(repeatMode);
+            }
+        });
     }
 
     private void notifyShuffleModeChangedOnExecutor(MediaPlaylistAgent playlistAgent,
-            int shuffleMode) {
+            final int shuffleMode) {
         synchronized (mLock) {
             if (playlistAgent != mPlaylistAgent) {
                 // Ignore calls from the old agent.
                 return;
             }
         }
-        MediaSession2 session2 = mInstance.get();
-        if (session2 != null) {
-            mCallback.onShuffleModeChanged(session2, playlistAgent, shuffleMode);
-            mSession2Stub.notifyShuffleModeChanged(shuffleMode);
+        mCallback.onShuffleModeChanged(mInstance, playlistAgent, shuffleMode);
+        notifyToAllControllers(new NotifyRunnable() {
+            @Override
+            public void run(ControllerCb callback) throws RemoteException {
+                callback.onShuffleModeChanged(shuffleMode);
+            }
+        });
+    }
+
+    private void notifyToController(@NonNull final ControllerInfo controller,
+            @NonNull NotifyRunnable runnable) {
+        if (controller == null) {
+            return;
+        }
+        try {
+            runnable.run(controller.getControllerCb());
+        } catch (DeadObjectException e) {
+            if (DEBUG) {
+                Log.d(TAG, controller.toString() + " is gone", e);
+            }
+            mSession2Stub.removeControllerInfo(controller);
+            mCallbackExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onDisconnected(MediaSession2ImplBase.this.getInstance(), controller);
+                }
+            });
+        } catch (RemoteException e) {
+            // Currently it's TransactionTooLargeException or DeadSystemException.
+            // We'd better to leave log for those cases because
+            //   - TransactionTooLargeException means that we may need to fix our code.
+            //     (e.g. add pagination or special way to deliver Bitmap)
+            //   - DeadSystemException means that errors around it can be ignored.
+            Log.w(TAG, "Exception in " + controller.toString(), e);
+        }
+    }
+
+    private void notifyToAllControllers(@NonNull NotifyRunnable runnable) {
+        List<ControllerInfo> controllers = getConnectedControllers();
+        for (int i = 0; i < controllers.size(); i++) {
+            notifyToController(controllers.get(i), runnable);
         }
     }
 
     ///////////////////////////////////////////////////
     // Inner classes
     ///////////////////////////////////////////////////
+    @FunctionalInterface
+    private interface NotifyRunnable {
+        void run(ControllerCb callback) throws RemoteException;
+    }
 
     private static class MyPlayerEventCallback extends PlayerEventCallback {
         private final WeakReference<MediaSession2ImplBase> mSession;
@@ -954,66 +1170,41 @@
         }
 
         @Override
-        public void onCurrentDataSourceChanged(final MediaPlayerBase mpb,
+        public void onCurrentDataSourceChanged(final MediaPlayerInterface player,
                 final DataSourceDesc dsd) {
             final MediaSession2ImplBase session = getSession();
-            // TODO: handle properly when dsd == null
-            if (session == null || dsd == null) {
-                return;
-            }
-            session.getCallbackExecutor().execute(new Runnable() {
-                @Override
-                public void run() {
-                    MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd);
-                    if (item == null) {
-                        return;
-                    }
-                    session.getCallback().onCurrentMediaItemChanged(session.getInstance(), mpb,
-                            item);
-                    if (item.equals(session.getCurrentMediaItem())) {
-                        session.getSession2Stub().notifyCurrentMediaItemChanged(item);
-                    }
-                }
-            });
-        }
-
-        @Override
-        public void onMediaPrepared(final MediaPlayerBase mpb, final DataSourceDesc dsd) {
-            final MediaSession2ImplBase session = getSession();
-            if (session == null || dsd == null) {
-                return;
-            }
-            session.getCallbackExecutor().execute(new Runnable() {
-                @Override
-                public void run() {
-                    MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd);
-                    if (item == null) {
-                        return;
-                    }
-                    session.getCallback().onMediaPrepared(session.getInstance(), mpb, item);
-                    // TODO (jaewan): Notify controllers through appropriate callback. (b/74505936)
-                }
-            });
-        }
-
-        @Override
-        public void onPlayerStateChanged(final MediaPlayerBase mpb, final int state) {
-            final MediaSession2ImplBase session = getSession();
             if (session == null) {
                 return;
             }
             session.getCallbackExecutor().execute(new Runnable() {
                 @Override
                 public void run() {
-                    session.getCallback().onPlayerStateChanged(session.getInstance(), mpb, state);
-                    session.getSession2Stub().notifyPlayerStateChanged(state);
+                    final MediaItem2 item;
+                    if (dsd == null) {
+                        // This is OK because onCurrentDataSourceChanged() can be called with the
+                        // null dsd, so onCurrentMediaItemChanged() can be as well.
+                        item = null;
+                    } else {
+                        item = MyPlayerEventCallback.this.getMediaItem(session, dsd);
+                        if (item == null) {
+                            Log.w(TAG, "Cannot obtain media item from the dsd=" + dsd);
+                            return;
+                        }
+                    }
+                    session.getCallback().onCurrentMediaItemChanged(session.getInstance(), player,
+                            item);
+                    session.notifyToAllControllers(new NotifyRunnable() {
+                        @Override
+                        public void run(ControllerCb callback) throws RemoteException {
+                            callback.onCurrentMediaItemChanged(item);
+                        }
+                    });
                 }
             });
         }
 
         @Override
-        public void onBufferingStateChanged(final MediaPlayerBase mpb, final DataSourceDesc dsd,
-                final int state) {
+        public void onMediaPrepared(final MediaPlayerInterface mpb, final DataSourceDesc dsd) {
             final MediaSession2ImplBase session = getSession();
             if (session == null || dsd == null) {
                 return;
@@ -1025,15 +1216,105 @@
                     if (item == null) {
                         return;
                     }
-                    session.getCallback().onBufferingStateChanged(
-                            session.getInstance(), mpb, item, state);
-                    session.getSession2Stub().notifyBufferingStateChanged(item, state);
+                    if (item.equals(session.getCurrentMediaItem())) {
+                        long duration = session.getDuration();
+                        if (duration < 0) {
+                            return;
+                        }
+                        MediaMetadata2 metadata = item.getMetadata();
+                        if (metadata != null) {
+                            if (!metadata.containsKey(MediaMetadata2.METADATA_KEY_DURATION)) {
+                                metadata = new MediaMetadata2.Builder(metadata).putLong(
+                                        MediaMetadata2.METADATA_KEY_DURATION, duration).build();
+                            } else {
+                                long durationFromMetadata =
+                                        metadata.getLong(MediaMetadata2.METADATA_KEY_DURATION);
+                                if (duration != durationFromMetadata) {
+                                    // Warns developers about the mismatch. Don't log media item
+                                    // here to keep metadata secure.
+                                    Log.w(TAG, "duration mismatch for an item."
+                                            + " duration from player=" + duration
+                                            + " duration from metadata=" + durationFromMetadata
+                                            + ". May be a timing issue?");
+                                }
+                                // Trust duration in the metadata set by developer.
+                                // In theory, duration may differ if the current item has been
+                                // changed before the getDuration(). So it's better not touch
+                                // duration set by developer.
+                                metadata = null;
+                            }
+                        } else {
+                            metadata = new MediaMetadata2.Builder()
+                                    .putLong(MediaMetadata2.METADATA_KEY_DURATION, duration)
+                                    .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID,
+                                            item.getMediaId())
+                                    .build();
+                        }
+                        if (metadata != null) {
+                            item.setMetadata(metadata);
+                            session.notifyToAllControllers(new NotifyRunnable() {
+                                @Override
+                                public void run(ControllerCb callback) throws RemoteException {
+                                    callback.onPlaylistChanged(
+                                            session.getPlaylist(), session.getPlaylistMetadata());
+                                }
+                            });
+                        }
+                    }
+                    session.getCallback().onMediaPrepared(session.getInstance(), mpb, item);
                 }
             });
         }
 
         @Override
-        public void onPlaybackSpeedChanged(final MediaPlayerBase mpb, final float speed) {
+        public void onPlayerStateChanged(final MediaPlayerInterface player, final int state) {
+            final MediaSession2ImplBase session = getSession();
+            if (session == null) {
+                return;
+            }
+            session.getCallbackExecutor().execute(new Runnable() {
+                @Override
+                public void run() {
+                    session.getCallback().onPlayerStateChanged(
+                            session.getInstance(), player, state);
+                    session.notifyToAllControllers(new NotifyRunnable() {
+                        @Override
+                        public void run(ControllerCb callback) throws RemoteException {
+                            callback.onPlayerStateChanged(state);
+                        }
+                    });
+                }
+            });
+        }
+
+        @Override
+        public void onBufferingStateChanged(final MediaPlayerInterface mpb,
+                final DataSourceDesc dsd, final int state) {
+            final MediaSession2ImplBase session = getSession();
+            if (session == null || dsd == null) {
+                return;
+            }
+            session.getCallbackExecutor().execute(new Runnable() {
+                @Override
+                public void run() {
+                    final MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd);
+                    if (item == null) {
+                        return;
+                    }
+                    session.getCallback().onBufferingStateChanged(
+                            session.getInstance(), mpb, item, state);
+                    session.notifyToAllControllers(new NotifyRunnable() {
+                        @Override
+                        public void run(ControllerCb callback) throws RemoteException {
+                            callback.onBufferingStateChanged(item, state);
+                        }
+                    });
+                }
+            });
+        }
+
+        @Override
+        public void onPlaybackSpeedChanged(final MediaPlayerInterface mpb, final float speed) {
             final MediaSession2ImplBase session = getSession();
             if (session == null) {
                 return;
@@ -1042,7 +1323,32 @@
                 @Override
                 public void run() {
                     session.getCallback().onPlaybackSpeedChanged(session.getInstance(), mpb, speed);
-                    session.getSession2Stub().notifyPlaybackSpeedChanged(speed);
+                    session.notifyToAllControllers(new NotifyRunnable() {
+                        @Override
+                        public void run(ControllerCb callback) throws RemoteException {
+                            callback.onPlaybackSpeedChanged(speed);
+                        }
+                    });
+                }
+            });
+        }
+
+        @Override
+        public void onSeekCompleted(final MediaPlayerInterface mpb, final long position) {
+            final MediaSession2ImplBase session = getSession();
+            if (session == null) {
+                return;
+            }
+            session.getCallbackExecutor().execute(new Runnable() {
+                @Override
+                public void run() {
+                    session.getCallback().onSeekCompleted(session.getInstance(), mpb, position);
+                    session.notifyToAllControllers(new NotifyRunnable() {
+                        @Override
+                        public void run(ControllerCb callback) throws RemoteException {
+                            callback.onSeekCompleted(position);
+                        }
+                    });
                 }
             });
         }
@@ -1123,7 +1429,7 @@
     abstract static class BuilderBase
             <T extends MediaSession2, C extends SessionCallback> {
         final Context mContext;
-        MediaPlayerBase mPlayer;
+        MediaPlayerInterface mPlayer;
         String mId;
         Executor mCallbackExecutor;
         C mCallback;
@@ -1140,7 +1446,7 @@
             mId = TAG;
         }
 
-        void setPlayer(@NonNull MediaPlayerBase player) {
+        void setPlayer(@NonNull MediaPlayerInterface player) {
             if (player == null) {
                 throw new IllegalArgumentException("player shouldn't be null");
             }
diff --git a/media/src/main/java/androidx/media/MediaSession2StubImplBase.java b/media/src/main/java/androidx/media/MediaSession2Stub.java
similarity index 68%
rename from media/src/main/java/androidx/media/MediaSession2StubImplBase.java
rename to media/src/main/java/androidx/media/MediaSession2Stub.java
index 48e641e..87b17cf 100644
--- a/media/src/main/java/androidx/media/MediaSession2StubImplBase.java
+++ b/media/src/main/java/androidx/media/MediaSession2Stub.java
@@ -25,6 +25,7 @@
 import static androidx.media.MediaConstants2.ARGUMENT_ERROR_CODE;
 import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
 import static androidx.media.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK;
+import static androidx.media.MediaConstants2.ARGUMENT_ITEM_COUNT;
 import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ID;
 import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ITEM;
 import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME;
@@ -55,7 +56,8 @@
 import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_CONNECT;
 import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_DISCONNECT;
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CHILDREN_CHANGED;
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED;
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR;
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED;
@@ -65,9 +67,12 @@
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED;
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_REPEAT_MODE_CHANGED;
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ROUTES_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SEARCH_RESULT_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SEEK_COMPLETED;
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED;
 import static androidx.media.MediaConstants2.SESSION_EVENT_SEND_CUSTOM_COMMAND;
 import static androidx.media.MediaConstants2.SESSION_EVENT_SET_CUSTOM_LAYOUT;
+import static androidx.media.SessionCommand2.COMMAND_CODE_CUSTOM;
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE;
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY;
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE;
@@ -106,21 +111,22 @@
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.DeadObjectException;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.support.v4.media.session.IMediaControllerCallback;
 import android.support.v4.media.session.MediaSessionCompat;
-import android.util.ArrayMap;
 import android.util.Log;
 import android.util.SparseArray;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.collection.ArrayMap;
+import androidx.core.app.BundleCompat;
 import androidx.media.MediaController2.PlaybackInfo;
 import androidx.media.MediaSession2.CommandButton;
+import androidx.media.MediaSession2.ControllerCb;
 import androidx.media.MediaSession2.ControllerInfo;
 
 import java.util.ArrayList;
@@ -129,7 +135,7 @@
 import java.util.Set;
 
 @TargetApi(Build.VERSION_CODES.KITKAT)
-class MediaSession2StubImplBase extends MediaSessionCompat.Callback {
+class MediaSession2Stub extends MediaSessionCompat.Callback {
 
     private static final String TAG = "MS2StubImplBase";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -161,78 +167,16 @@
     private final ArrayMap<ControllerInfo, SessionCommandGroup2> mAllowedCommandGroupMap =
             new ArrayMap<>();
 
-    MediaSession2StubImplBase(MediaSession2.SupportLibraryImpl session) {
+    MediaSession2Stub(MediaSession2.SupportLibraryImpl session) {
         mSession = session;
         mContext = mSession.getContext();
     }
 
     @Override
-    public void onPrepare() {
-        mSession.getCallbackExecutor().execute(new Runnable() {
-            @Override
-            public void run() {
-                if (mSession.isClosed()) {
-                    return;
-                }
-                mSession.prepare();
-            }
-        });
-    }
-
-    @Override
-    public void onPlay() {
-        mSession.getCallbackExecutor().execute(new Runnable() {
-            @Override
-            public void run() {
-                if (mSession.isClosed()) {
-                    return;
-                }
-                mSession.play();
-            }
-        });
-    }
-
-    @Override
-    public void onPause() {
-        mSession.getCallbackExecutor().execute(new Runnable() {
-            @Override
-            public void run() {
-                if (mSession.isClosed()) {
-                    return;
-                }
-                mSession.pause();
-            }
-        });
-    }
-
-    @Override
-    public void onStop() {
-        mSession.getCallbackExecutor().execute(new Runnable() {
-            @Override
-            public void run() {
-                if (mSession.isClosed()) {
-                    return;
-                }
-                mSession.reset();
-            }
-        });
-    }
-
-    @Override
-    public void onSeekTo(final long pos) {
-        mSession.getCallbackExecutor().execute(new Runnable() {
-            @Override
-            public void run() {
-                if (mSession.isClosed()) {
-                    return;
-                }
-                mSession.seekTo(pos);
-            }
-        });
-    }
-
-    @Override
     public void onCommand(String command, final Bundle extras, final ResultReceiver cb) {
+        if (extras != null) {
+            extras.setClassLoader(MediaSession2.class.getClassLoader());
+        }
         switch (command) {
             case CONTROLLER_COMMAND_CONNECT:
                 connect(extras, cb);
@@ -242,8 +186,8 @@
                 break;
             case CONTROLLER_COMMAND_BY_COMMAND_CODE: {
                 final int commandCode = extras.getInt(ARGUMENT_COMMAND_CODE);
-                IMediaControllerCallback caller =
-                        (IMediaControllerCallback) extras.getBinder(ARGUMENT_ICONTROLLER_CALLBACK);
+                IMediaControllerCallback caller = IMediaControllerCallback.Stub.asInterface(
+                        BundleCompat.getBinder(extras, ARGUMENT_ICONTROLLER_CALLBACK));
                 if (caller == null) {
                     return;
                 }
@@ -332,7 +276,10 @@
                                 int flags = extras.getInt(ARGUMENT_VOLUME_FLAGS);
                                 VolumeProviderCompat vp = mSession.getVolumeProvider();
                                 if (vp == null) {
-                                    // TODO: Revisit
+                                    MediaSessionCompat sessionCompat = mSession.getSessionCompat();
+                                    if (sessionCompat != null) {
+                                        sessionCompat.getController().setVolumeTo(value, flags);
+                                    }
                                 } else {
                                     vp.onSetVolumeTo(value);
                                 }
@@ -343,7 +290,11 @@
                                 int flags = extras.getInt(ARGUMENT_VOLUME_FLAGS);
                                 VolumeProviderCompat vp = mSession.getVolumeProvider();
                                 if (vp == null) {
-                                    // TODO: Revisit
+                                    MediaSessionCompat sessionCompat = mSession.getSessionCompat();
+                                    if (sessionCompat != null) {
+                                        sessionCompat.getController().adjustVolume(
+                                                direction, flags);
+                                    }
                                 } else {
                                     vp.onAdjustVolume(direction);
                                 }
@@ -438,8 +389,8 @@
             case CONTROLLER_COMMAND_BY_CUSTOM_COMMAND: {
                 final SessionCommand2 customCommand =
                         SessionCommand2.fromBundle(extras.getBundle(ARGUMENT_CUSTOM_COMMAND));
-                IMediaControllerCallback caller =
-                        (IMediaControllerCallback) extras.getBinder(ARGUMENT_ICONTROLLER_CALLBACK);
+                IMediaControllerCallback caller = IMediaControllerCallback.Stub.asInterface(
+                        BundleCompat.getBinder(extras, ARGUMENT_ICONTROLLER_CALLBACK));
                 if (caller == null || customCommand == null) {
                     return;
                 }
@@ -467,261 +418,10 @@
         return controllers;
     }
 
-    void notifyCustomLayout(ControllerInfo controller, final List<CommandButton> layout) {
-        notifyInternal(controller, new Session2Runnable() {
-            @Override
-            public void run(ControllerInfo controller) throws RemoteException {
-                Bundle bundle = new Bundle();
-                bundle.putParcelableArray(ARGUMENT_COMMAND_BUTTONS,
-                        MediaUtils2.toCommandButtonParcelableArray(layout));
-                controller.getControllerBinder().onEvent(SESSION_EVENT_SET_CUSTOM_LAYOUT, bundle);
-            }
-        });
-    }
-
     void setAllowedCommands(ControllerInfo controller, final SessionCommandGroup2 commands) {
         synchronized (mLock) {
             mAllowedCommandGroupMap.put(controller, commands);
         }
-        notifyInternal(controller, new Session2Runnable() {
-            @Override
-            public void run(ControllerInfo controller) throws RemoteException {
-                Bundle bundle = new Bundle();
-                bundle.putBundle(ARGUMENT_ALLOWED_COMMANDS, commands.toBundle());
-                controller.getControllerBinder().onEvent(
-                        SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED, bundle);
-            }
-        });
-    }
-
-    public void sendCustomCommand(ControllerInfo controller, final SessionCommand2 command,
-            final Bundle args, final ResultReceiver receiver) {
-        if (receiver != null && controller == null) {
-            throw new IllegalArgumentException("Controller shouldn't be null if result receiver is"
-                    + " specified");
-        }
-        if (command == null) {
-            throw new IllegalArgumentException("command shouldn't be null");
-        }
-        notifyInternal(controller, new Session2Runnable() {
-            @Override
-            public void run(ControllerInfo controller) throws RemoteException {
-                // TODO: Send this event through MediaSessionCompat.XXX()
-                Bundle bundle = new Bundle();
-                bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
-                bundle.putBundle(ARGUMENT_ARGUMENTS, args);
-                bundle.putParcelable(ARGUMENT_RESULT_RECEIVER, receiver);
-                controller.getControllerBinder().onEvent(SESSION_EVENT_SEND_CUSTOM_COMMAND, bundle);
-            }
-        });
-    }
-
-    public void sendCustomCommand(final SessionCommand2 command, final Bundle args) {
-        if (command == null) {
-            throw new IllegalArgumentException("command shouldn't be null");
-        }
-        final Bundle bundle = new Bundle();
-        bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
-        bundle.putBundle(ARGUMENT_ARGUMENTS, args);
-        notifyAll(new Session2Runnable() {
-            @Override
-            public void run(ControllerInfo controller) throws RemoteException {
-                controller.getControllerBinder().onEvent(SESSION_EVENT_SEND_CUSTOM_COMMAND, bundle);
-            }
-        });
-    }
-
-    void notifyCurrentMediaItemChanged(final MediaItem2 item) {
-        notifyAll(COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM, new Session2Runnable() {
-            @Override
-            public void run(ControllerInfo controller) throws RemoteException {
-                Bundle bundle = new Bundle();
-                bundle.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
-                controller.getControllerBinder().onEvent(
-                        SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED, bundle);
-            }
-        });
-    }
-
-    void notifyPlaybackInfoChanged(final PlaybackInfo info) {
-        notifyAll(new Session2Runnable() {
-            @Override
-            public void run(ControllerInfo controller) throws RemoteException {
-                Bundle bundle = new Bundle();
-                bundle.putBundle(ARGUMENT_PLAYBACK_INFO, info.toBundle());
-                controller.getControllerBinder().onEvent(
-                        SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED, bundle);
-            }
-        });
-    }
-
-    void notifyPlayerStateChanged(final int state) {
-        notifyAll(new Session2Runnable() {
-            @Override
-            public void run(ControllerInfo controller) throws RemoteException {
-                Bundle bundle = new Bundle();
-                bundle.putInt(ARGUMENT_PLAYER_STATE, state);
-                controller.getControllerBinder().onEvent(
-                        SESSION_EVENT_ON_PLAYER_STATE_CHANGED, bundle);
-            }
-        });
-    }
-
-    void notifyPlaybackSpeedChanged(final float speed) {
-        notifyAll(new Session2Runnable() {
-            @Override
-            public void run(ControllerInfo controller) throws RemoteException {
-                Bundle bundle = new Bundle();
-                bundle.putParcelable(
-                        ARGUMENT_PLAYBACK_STATE_COMPAT, mSession.getPlaybackStateCompat());
-                controller.getControllerBinder().onEvent(
-                        SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED, bundle);
-            }
-        });
-    }
-
-    void notifyBufferingStateChanged(final MediaItem2 item, final int bufferingState) {
-        notifyAll(new Session2Runnable() {
-            @Override
-            public void run(ControllerInfo controller) throws RemoteException {
-                Bundle bundle = new Bundle();
-                bundle.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
-                bundle.putInt(ARGUMENT_BUFFERING_STATE, bufferingState);
-                controller.getControllerBinder().onEvent(
-                        SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED, bundle);
-            }
-        });
-    }
-
-    void notifyError(final int errorCode, final Bundle extras) {
-        notifyAll(new Session2Runnable() {
-            @Override
-            public void run(ControllerInfo controller) throws RemoteException {
-                Bundle bundle = new Bundle();
-                bundle.putInt(ARGUMENT_ERROR_CODE, errorCode);
-                bundle.putBundle(ARGUMENT_EXTRAS, extras);
-                controller.getControllerBinder().onEvent(SESSION_EVENT_ON_ERROR, bundle);
-            }
-        });
-    }
-
-    void notifyRoutesInfoChanged(@NonNull final ControllerInfo controller,
-            @Nullable final List<Bundle> routes) {
-        notifyInternal(controller, new Session2Runnable() {
-            @Override
-            public void run(ControllerInfo controller) throws RemoteException {
-                Bundle bundle = null;
-                if (routes != null) {
-                    bundle = new Bundle();
-                    bundle.putParcelableArray(ARGUMENT_ROUTE_BUNDLE, routes.toArray(new Bundle[0]));
-                }
-                controller.getControllerBinder().onEvent(
-                        SESSION_EVENT_ON_ROUTES_INFO_CHANGED, bundle);
-            }
-        });
-    }
-
-    void notifyPlaylistChanged(final List<MediaItem2> playlist,
-            final MediaMetadata2 metadata) {
-        notifyAll(COMMAND_CODE_PLAYLIST_GET_LIST, new Session2Runnable() {
-            @Override
-            public void run(ControllerInfo controller) throws RemoteException {
-                Bundle bundle = new Bundle();
-                bundle.putParcelableArray(ARGUMENT_PLAYLIST,
-                        MediaUtils2.toMediaItem2ParcelableArray(playlist));
-                bundle.putBundle(ARGUMENT_PLAYLIST_METADATA,
-                        metadata == null ? null : metadata.toBundle());
-                controller.getControllerBinder().onEvent(
-                        SESSION_EVENT_ON_PLAYLIST_CHANGED, bundle);
-            }
-        });
-    }
-
-    void notifyPlaylistMetadataChanged(final MediaMetadata2 metadata) {
-        notifyAll(SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST_METADATA, new Session2Runnable() {
-            @Override
-            public void run(ControllerInfo controller) throws RemoteException {
-                Bundle bundle = new Bundle();
-                bundle.putBundle(ARGUMENT_PLAYLIST_METADATA,
-                        metadata == null ? null : metadata.toBundle());
-                controller.getControllerBinder().onEvent(
-                        SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED, bundle);
-            }
-        });
-    }
-
-    void notifyRepeatModeChanged(final int repeatMode) {
-        notifyAll(new Session2Runnable() {
-            @Override
-            public void run(ControllerInfo controller) throws RemoteException {
-                Bundle bundle = new Bundle();
-                bundle.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
-                controller.getControllerBinder().onEvent(
-                        SESSION_EVENT_ON_REPEAT_MODE_CHANGED, bundle);
-            }
-        });
-    }
-
-    void notifyShuffleModeChanged(final int shuffleMode) {
-        notifyAll(new Session2Runnable() {
-            @Override
-            public void run(ControllerInfo controller) throws RemoteException {
-                Bundle bundle = new Bundle();
-                bundle.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
-                controller.getControllerBinder().onEvent(
-                        SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED, bundle);
-            }
-        });
-    }
-
-    private List<ControllerInfo> getControllers() {
-        ArrayList<ControllerInfo> controllers = new ArrayList<>();
-        synchronized (mLock) {
-            for (int i = 0; i < mControllers.size(); i++) {
-                controllers.add(mControllers.valueAt(i));
-            }
-        }
-        return controllers;
-    }
-
-    private void notifyAll(@NonNull Session2Runnable runnable) {
-        List<ControllerInfo> controllers = getControllers();
-        for (int i = 0; i < controllers.size(); i++) {
-            notifyInternal(controllers.get(i), runnable);
-        }
-    }
-
-    private void notifyAll(int commandCode, @NonNull Session2Runnable runnable) {
-        List<ControllerInfo> controllers = getControllers();
-        for (int i = 0; i < controllers.size(); i++) {
-            ControllerInfo controller = controllers.get(i);
-            if (isAllowedCommand(controller, commandCode)) {
-                notifyInternal(controller, runnable);
-            }
-        }
-    }
-
-    // TODO: Add a way to check permission from here.
-    private void notifyInternal(@NonNull ControllerInfo controller,
-            @NonNull Session2Runnable runnable) {
-        if (controller == null || controller.getControllerBinder() == null) {
-            return;
-        }
-        try {
-            runnable.run(controller);
-        } catch (DeadObjectException e) {
-            if (DEBUG) {
-                Log.d(TAG, controller.toString() + " is gone", e);
-            }
-            onControllerClosed(controller.getControllerBinder());
-        } catch (RemoteException e) {
-            // Currently it's TransactionTooLargeException or DeadSystemException.
-            // We'd better to leave log for those cases because
-            //   - TransactionTooLargeException means that we may need to fix our code.
-            //     (e.g. add pagination or special way to deliver Bitmap)
-            //   - DeadSystemException means that errors around it can be ignored.
-            Log.w(TAG, "Exception in " + controller.toString(), e);
-        }
     }
 
     private boolean isAllowedCommand(ControllerInfo controller, SessionCommand2 command) {
@@ -742,12 +442,17 @@
 
     private void onCommand2(@NonNull IBinder caller, final int commandCode,
             @NonNull final Session2Runnable runnable) {
-        // TODO: Prevent instantiation of SessionCommand2
-        onCommand2(caller, new SessionCommand2(commandCode), runnable);
+        onCommand2Internal(caller, null, commandCode, runnable);
     }
 
     private void onCommand2(@NonNull IBinder caller, @NonNull final SessionCommand2 sessionCommand,
             @NonNull final Session2Runnable runnable) {
+        onCommand2Internal(caller, sessionCommand, COMMAND_CODE_CUSTOM, runnable);
+    }
+
+    private void onCommand2Internal(@NonNull IBinder caller,
+            @Nullable final SessionCommand2 sessionCommand, final int commandCode,
+            @NonNull final Session2Runnable runnable) {
         final ControllerInfo controller;
         synchronized (mLock) {
             controller = mControllers.get(caller);
@@ -758,18 +463,25 @@
         mSession.getCallbackExecutor().execute(new Runnable() {
             @Override
             public void run() {
-                if (!isAllowedCommand(controller, sessionCommand)) {
-                    return;
+                SessionCommand2 command;
+                if (sessionCommand != null) {
+                    if (!isAllowedCommand(controller, sessionCommand)) {
+                        return;
+                    }
+                    command = sCommandsForOnCommandRequest.get(sessionCommand.getCommandCode());
+                } else {
+                    if (!isAllowedCommand(controller, commandCode)) {
+                        return;
+                    }
+                    command = sCommandsForOnCommandRequest.get(commandCode);
                 }
-                int commandCode = sessionCommand.getCommandCode();
-                SessionCommand2 command = sCommandsForOnCommandRequest.get(commandCode);
                 if (command != null) {
                     boolean accepted = mSession.getCallback().onCommandRequest(
                             mSession.getInstance(), controller, command);
                     if (!accepted) {
                         // Don't run rejected command.
                         if (DEBUG) {
-                            Log.d(TAG, "Command (code=" + commandCode + ") from "
+                            Log.d(TAG, "Command (" + command + ") from "
                                     + controller + " was rejected by " + mSession);
                         }
                         return;
@@ -789,35 +501,22 @@
         });
     }
 
-    private void onControllerClosed(IMediaControllerCallback iController) {
-        ControllerInfo controller;
+    void removeControllerInfo(ControllerInfo controller) {
         synchronized (mLock) {
-            controller = mControllers.remove(iController.asBinder());
+            controller = mControllers.remove(controller.getId());
             if (DEBUG) {
                 Log.d(TAG, "releasing " + controller);
             }
         }
-        if (controller == null) {
-            return;
-        }
-        final ControllerInfo removedController = controller;
-        mSession.getCallbackExecutor().execute(new Runnable() {
-            @Override
-            public void run() {
-                mSession.getCallback().onDisconnected(mSession.getInstance(), removedController);
-            }
-        });
     }
 
     private ControllerInfo createControllerInfo(Bundle extras) {
         IMediaControllerCallback callback = IMediaControllerCallback.Stub.asInterface(
-                extras.getBinder(ARGUMENT_ICONTROLLER_CALLBACK));
+                BundleCompat.getBinder(extras, ARGUMENT_ICONTROLLER_CALLBACK));
         String packageName = extras.getString(ARGUMENT_PACKAGE_NAME);
         int uid = extras.getInt(ARGUMENT_UID);
         int pid = extras.getInt(ARGUMENT_PID);
-        // TODO: sanity check for packageName, uid, and pid.
-
-        return new ControllerInfo(mContext, uid, pid, packageName, callback);
+        return new ControllerInfo(packageName, pid, uid, new Controller2Cb(callback));
     }
 
     private void connect(Bundle extras, final ResultReceiver cb) {
@@ -926,4 +625,173 @@
     private interface Session2Runnable {
         void run(ControllerInfo controller) throws RemoteException;
     }
+
+    final class Controller2Cb extends ControllerCb {
+        private final IMediaControllerCallback mIControllerCallback;
+
+        Controller2Cb(@NonNull IMediaControllerCallback callback) {
+            mIControllerCallback = callback;
+        }
+
+        @Override
+        @NonNull IBinder getId() {
+            return mIControllerCallback.asBinder();
+        }
+
+        @Override
+        void onCustomLayoutChanged(List<CommandButton> layout) throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putParcelableArray(ARGUMENT_COMMAND_BUTTONS,
+                    MediaUtils2.toCommandButtonParcelableArray(layout));
+            mIControllerCallback.onEvent(SESSION_EVENT_SET_CUSTOM_LAYOUT, bundle);
+        }
+
+        @Override
+        void onPlaybackInfoChanged(PlaybackInfo info) throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putBundle(ARGUMENT_PLAYBACK_INFO, info.toBundle());
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED, bundle);
+
+        }
+
+        @Override
+        void onAllowedCommandsChanged(SessionCommandGroup2 commands) throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putBundle(ARGUMENT_ALLOWED_COMMANDS, commands.toBundle());
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED, bundle);
+        }
+
+        @Override
+        void onCustomCommand(SessionCommand2 command, Bundle args, ResultReceiver receiver)
+                throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
+            bundle.putBundle(ARGUMENT_ARGUMENTS, args);
+            bundle.putParcelable(ARGUMENT_RESULT_RECEIVER, receiver);
+            mIControllerCallback.onEvent(SESSION_EVENT_SEND_CUSTOM_COMMAND, bundle);
+        }
+
+        @Override
+        void onPlayerStateChanged(int playerState)
+                throws RemoteException {
+            // Note: current position should be also sent to the controller here for controller
+            // to calculate the position more correctly.
+            Bundle bundle = new Bundle();
+            bundle.putInt(ARGUMENT_PLAYER_STATE, playerState);
+            bundle.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT, mSession.getPlaybackStateCompat());
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYER_STATE_CHANGED, bundle);
+        }
+
+        @Override
+        void onPlaybackSpeedChanged(float speed) throws RemoteException {
+            // Note: current position should be also sent to the controller here for controller
+            // to calculate the position more correctly.
+            Bundle bundle = new Bundle();
+            bundle.putParcelable(
+                    ARGUMENT_PLAYBACK_STATE_COMPAT, mSession.getPlaybackStateCompat());
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED, bundle);
+        }
+
+        @Override
+        void onBufferingStateChanged(MediaItem2 item, int state) throws RemoteException {
+            // Note: buffered position should be also sent to the controller here. It's to
+            // follow the behavior of MediaPlayerInterface.PlayerEventCallback.
+            Bundle bundle = new Bundle();
+            bundle.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+            bundle.putInt(ARGUMENT_BUFFERING_STATE, state);
+            bundle.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT,
+                    mSession.getPlaybackStateCompat());
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_BUFFERING_STATE_CHANGED, bundle);
+
+        }
+
+        @Override
+        void onSeekCompleted(long position) throws RemoteException {
+            // Note: current position should be also sent to the controller here because the
+            // position here may refer to the parameter of the previous seek() API calls.
+            Bundle bundle = new Bundle();
+            bundle.putLong(ARGUMENT_SEEK_POSITION, position);
+            bundle.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT,
+                    mSession.getPlaybackStateCompat());
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_SEEK_COMPLETED, bundle);
+        }
+
+        @Override
+        void onError(int errorCode, Bundle extras) throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putInt(ARGUMENT_ERROR_CODE, errorCode);
+            bundle.putBundle(ARGUMENT_EXTRAS, extras);
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_ERROR, bundle);
+        }
+
+        @Override
+        void onCurrentMediaItemChanged(MediaItem2 item) throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putBundle(ARGUMENT_MEDIA_ITEM, (item == null) ? null : item.toBundle());
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED, bundle);
+        }
+
+        @Override
+        void onPlaylistChanged(List<MediaItem2> playlist, MediaMetadata2 metadata)
+                throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putParcelableArray(ARGUMENT_PLAYLIST,
+                    MediaUtils2.toMediaItem2ParcelableArray(playlist));
+            bundle.putBundle(ARGUMENT_PLAYLIST_METADATA,
+                    metadata == null ? null : metadata.toBundle());
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYLIST_CHANGED, bundle);
+        }
+
+        @Override
+        void onPlaylistMetadataChanged(MediaMetadata2 metadata) throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putBundle(ARGUMENT_PLAYLIST_METADATA,
+                    metadata == null ? null : metadata.toBundle());
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED, bundle);
+        }
+
+        @Override
+        void onShuffleModeChanged(int shuffleMode) throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED, bundle);
+        }
+
+        @Override
+        void onRepeatModeChanged(int repeatMode) throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_REPEAT_MODE_CHANGED, bundle);
+        }
+
+        @Override
+        void onRoutesInfoChanged(List<Bundle> routes) throws RemoteException {
+            Bundle bundle = null;
+            if (routes != null) {
+                bundle = new Bundle();
+                bundle.putParcelableArray(ARGUMENT_ROUTE_BUNDLE, routes.toArray(new Bundle[0]));
+            }
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_ROUTES_INFO_CHANGED, bundle);
+        }
+
+        @Override
+        void onChildrenChanged(String parentId, int itemCount, Bundle extras)
+                throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putString(ARGUMENT_MEDIA_ID, parentId);
+            bundle.putInt(ARGUMENT_ITEM_COUNT, itemCount);
+            bundle.putBundle(ARGUMENT_EXTRAS, extras);
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_CHILDREN_CHANGED, bundle);
+        }
+
+        @Override
+        void onSearchResultChanged(String query, int itemCount, Bundle extras)
+                throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putString(ARGUMENT_QUERY, query);
+            bundle.putInt(ARGUMENT_ITEM_COUNT, itemCount);
+            bundle.putBundle(ARGUMENT_EXTRAS, extras);
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_SEARCH_RESULT_CHANGED, bundle);
+        }
+    }
 }
diff --git a/media/src/main/java/androidx/media/MediaSessionLegacyStub.java b/media/src/main/java/androidx/media/MediaSessionLegacyStub.java
new file mode 100644
index 0000000..2e0f945
--- /dev/null
+++ b/media/src/main/java/androidx/media/MediaSessionLegacyStub.java
@@ -0,0 +1,583 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.media.MediaConstants2.ARGUMENT_ALLOWED_COMMANDS;
+import static androidx.media.MediaConstants2.ARGUMENT_ARGUMENTS;
+import static androidx.media.MediaConstants2.ARGUMENT_BUFFERING_STATE;
+import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_BUTTONS;
+import static androidx.media.MediaConstants2.ARGUMENT_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.ARGUMENT_ERROR_CODE;
+import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
+import static androidx.media.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK;
+import static androidx.media.MediaConstants2.ARGUMENT_ITEM_COUNT;
+import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ID;
+import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ITEM;
+import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME;
+import static androidx.media.MediaConstants2.ARGUMENT_PID;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_INFO;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_STATE_COMPAT;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYER_STATE;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_METADATA;
+import static androidx.media.MediaConstants2.ARGUMENT_QUERY;
+import static androidx.media.MediaConstants2.ARGUMENT_REPEAT_MODE;
+import static androidx.media.MediaConstants2.ARGUMENT_RESULT_RECEIVER;
+import static androidx.media.MediaConstants2.ARGUMENT_ROUTE_BUNDLE;
+import static androidx.media.MediaConstants2.ARGUMENT_SEEK_POSITION;
+import static androidx.media.MediaConstants2.ARGUMENT_SHUFFLE_MODE;
+import static androidx.media.MediaConstants2.ARGUMENT_UID;
+import static androidx.media.MediaConstants2.CONNECT_RESULT_CONNECTED;
+import static androidx.media.MediaConstants2.CONNECT_RESULT_DISCONNECTED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CHILDREN_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYER_STATE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_REPEAT_MODE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ROUTES_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SEARCH_RESULT_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SEEK_COMPLETED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_SEND_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.SESSION_EVENT_SET_CUSTOM_LAYOUT;
+import static androidx.media.SessionCommand2.COMMAND_CODE_CUSTOM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.support.v4.media.session.IMediaControllerCallback;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.util.Log;
+import android.util.SparseArray;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.collection.ArrayMap;
+import androidx.core.app.BundleCompat;
+import androidx.media.MediaController2.PlaybackInfo;
+import androidx.media.MediaSession2.CommandButton;
+import androidx.media.MediaSession2.ControllerCb;
+import androidx.media.MediaSession2.ControllerInfo;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@TargetApi(Build.VERSION_CODES.KITKAT)
+class MediaSessionLegacyStub extends MediaSessionCompat.Callback {
+
+    private static final String TAG = "MS2StubImplBase";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final SparseArray<SessionCommand2> sCommandsForOnCommandRequest =
+            new SparseArray<>();
+
+    static {
+        SessionCommandGroup2 group = new SessionCommandGroup2();
+        group.addAllPlaybackCommands();
+        group.addAllPlaylistCommands();
+        group.addAllVolumeCommands();
+        Set<SessionCommand2> commands = group.getCommands();
+        for (SessionCommand2 command : commands) {
+            sCommandsForOnCommandRequest.append(command.getCommandCode(), command);
+        }
+    }
+
+    private final Object mLock = new Object();
+
+    final MediaSession2.SupportLibraryImpl mSession;
+    final Context mContext;
+
+    @GuardedBy("mLock")
+    private final ArrayMap<IBinder, ControllerInfo> mControllers = new ArrayMap<>();
+    @GuardedBy("mLock")
+    private final Set<IBinder> mConnectingControllers = new HashSet<>();
+    @GuardedBy("mLock")
+    private final ArrayMap<ControllerInfo, SessionCommandGroup2> mAllowedCommandGroupMap =
+            new ArrayMap<>();
+
+    MediaSessionLegacyStub(MediaSession2.SupportLibraryImpl session) {
+        mSession = session;
+        mContext = mSession.getContext();
+    }
+
+    @Override
+    public void onPrepare() {
+        mSession.getCallbackExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                if (mSession.isClosed()) {
+                    return;
+                }
+                mSession.prepare();
+            }
+        });
+    }
+
+    @Override
+    public void onPlay() {
+        mSession.getCallbackExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                if (mSession.isClosed()) {
+                    return;
+                }
+                mSession.play();
+            }
+        });
+    }
+
+    @Override
+    public void onPause() {
+        mSession.getCallbackExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                if (mSession.isClosed()) {
+                    return;
+                }
+                mSession.pause();
+            }
+        });
+    }
+
+    @Override
+    public void onStop() {
+        mSession.getCallbackExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                if (mSession.isClosed()) {
+                    return;
+                }
+                mSession.reset();
+            }
+        });
+    }
+
+    @Override
+    public void onSeekTo(final long pos) {
+        mSession.getCallbackExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                if (mSession.isClosed()) {
+                    return;
+                }
+                mSession.seekTo(pos);
+            }
+        });
+    }
+
+    List<ControllerInfo> getConnectedControllers() {
+        ArrayList<ControllerInfo> controllers = new ArrayList<>();
+        synchronized (mLock) {
+            for (int i = 0; i < mControllers.size(); i++) {
+                controllers.add(mControllers.valueAt(i));
+            }
+        }
+        return controllers;
+    }
+
+    void setAllowedCommands(ControllerInfo controller, final SessionCommandGroup2 commands) {
+        synchronized (mLock) {
+            mAllowedCommandGroupMap.put(controller, commands);
+        }
+    }
+
+    private boolean isAllowedCommand(ControllerInfo controller, SessionCommand2 command) {
+        SessionCommandGroup2 allowedCommands;
+        synchronized (mLock) {
+            allowedCommands = mAllowedCommandGroupMap.get(controller);
+        }
+        return allowedCommands != null && allowedCommands.hasCommand(command);
+    }
+
+    private boolean isAllowedCommand(ControllerInfo controller, int commandCode) {
+        SessionCommandGroup2 allowedCommands;
+        synchronized (mLock) {
+            allowedCommands = mAllowedCommandGroupMap.get(controller);
+        }
+        return allowedCommands != null && allowedCommands.hasCommand(commandCode);
+    }
+
+    private void onCommand2(@NonNull IBinder caller, final int commandCode,
+            @NonNull final Session2Runnable runnable) {
+        onCommand2Internal(caller, null, commandCode, runnable);
+    }
+
+    private void onCommand2(@NonNull IBinder caller, @NonNull final SessionCommand2 sessionCommand,
+            @NonNull final Session2Runnable runnable) {
+        onCommand2Internal(caller, sessionCommand, COMMAND_CODE_CUSTOM, runnable);
+    }
+
+    private void onCommand2Internal(@NonNull IBinder caller,
+            @Nullable final SessionCommand2 sessionCommand, final int commandCode,
+            @NonNull final Session2Runnable runnable) {
+        final ControllerInfo controller;
+        synchronized (mLock) {
+            controller = mControllers.get(caller);
+        }
+        if (mSession == null || controller == null) {
+            return;
+        }
+        mSession.getCallbackExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                SessionCommand2 command;
+                if (sessionCommand != null) {
+                    if (!isAllowedCommand(controller, sessionCommand)) {
+                        return;
+                    }
+                    command = sCommandsForOnCommandRequest.get(sessionCommand.getCommandCode());
+                } else {
+                    if (!isAllowedCommand(controller, commandCode)) {
+                        return;
+                    }
+                    command = sCommandsForOnCommandRequest.get(commandCode);
+                }
+                if (command != null) {
+                    boolean accepted = mSession.getCallback().onCommandRequest(
+                            mSession.getInstance(), controller, command);
+                    if (!accepted) {
+                        // Don't run rejected command.
+                        if (DEBUG) {
+                            Log.d(TAG, "Command (" + command + ") from "
+                                    + controller + " was rejected by " + mSession);
+                        }
+                        return;
+                    }
+                }
+                try {
+                    runnable.run(controller);
+                } catch (RemoteException e) {
+                    // Currently it's TransactionTooLargeException or DeadSystemException.
+                    // We'd better to leave log for those cases because
+                    //   - TransactionTooLargeException means that we may need to fix our code.
+                    //     (e.g. add pagination or special way to deliver Bitmap)
+                    //   - DeadSystemException means that errors around it can be ignored.
+                    Log.w(TAG, "Exception in " + controller.toString(), e);
+                }
+            }
+        });
+    }
+
+    void removeControllerInfo(ControllerInfo controller) {
+        synchronized (mLock) {
+            controller = mControllers.remove(controller.getId());
+            if (DEBUG) {
+                Log.d(TAG, "releasing " + controller);
+            }
+        }
+    }
+
+    private ControllerInfo createControllerInfo(Bundle extras) {
+        IMediaControllerCallback callback = IMediaControllerCallback.Stub.asInterface(
+                BundleCompat.getBinder(extras, ARGUMENT_ICONTROLLER_CALLBACK));
+        String packageName = extras.getString(ARGUMENT_PACKAGE_NAME);
+        int uid = extras.getInt(ARGUMENT_UID);
+        int pid = extras.getInt(ARGUMENT_PID);
+        return new ControllerInfo(packageName, pid, uid, new ControllerLegacyCb(callback));
+    }
+
+    private void connect(Bundle extras, final ResultReceiver cb) {
+        final ControllerInfo controllerInfo = createControllerInfo(extras);
+        mSession.getCallbackExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                if (mSession.isClosed()) {
+                    return;
+                }
+                synchronized (mLock) {
+                    // Keep connecting controllers.
+                    // This helps sessions to call APIs in the onConnect()
+                    // (e.g. setCustomLayout()) instead of pending them.
+                    mConnectingControllers.add(controllerInfo.getId());
+                }
+                SessionCommandGroup2 allowedCommands = mSession.getCallback().onConnect(
+                        mSession.getInstance(), controllerInfo);
+                // Don't reject connection for the request from trusted app.
+                // Otherwise server will fail to retrieve session's information to dispatch
+                // media keys to.
+                boolean accept = allowedCommands != null || controllerInfo.isTrusted();
+                if (accept) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Accepting connection, controllerInfo=" + controllerInfo
+                                + " allowedCommands=" + allowedCommands);
+                    }
+                    if (allowedCommands == null) {
+                        // For trusted apps, send non-null allowed commands to keep
+                        // connection.
+                        allowedCommands = new SessionCommandGroup2();
+                    }
+                    synchronized (mLock) {
+                        mConnectingControllers.remove(controllerInfo.getId());
+                        mControllers.put(controllerInfo.getId(), controllerInfo);
+                        mAllowedCommandGroupMap.put(controllerInfo, allowedCommands);
+                    }
+                    // If connection is accepted, notify the current state to the
+                    // controller. It's needed because we cannot call synchronous calls
+                    // between session/controller.
+                    // Note: We're doing this after the onConnectionChanged(), but there's
+                    //       no guarantee that events here are notified after the
+                    //       onConnected() because IMediaController2 is oneway (i.e. async
+                    //       call) and Stub will use thread poll for incoming calls.
+                    final Bundle resultData = new Bundle();
+                    resultData.putBundle(ARGUMENT_ALLOWED_COMMANDS,
+                            allowedCommands.toBundle());
+                    resultData.putInt(ARGUMENT_PLAYER_STATE, mSession.getPlayerState());
+                    resultData.putInt(ARGUMENT_BUFFERING_STATE, mSession.getBufferingState());
+                    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(
+                            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_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()) {
+                        return;
+                    }
+                    cb.send(CONNECT_RESULT_CONNECTED, resultData);
+                } else {
+                    synchronized (mLock) {
+                        mConnectingControllers.remove(controllerInfo.getId());
+                    }
+                    if (DEBUG) {
+                        Log.d(TAG, "Rejecting connection, controllerInfo=" + controllerInfo);
+                    }
+                    cb.send(CONNECT_RESULT_DISCONNECTED, null);
+                }
+            }
+        });
+    }
+
+    private void disconnect(Bundle extras) {
+        final ControllerInfo controllerInfo = createControllerInfo(extras);
+        mSession.getCallbackExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                if (mSession.isClosed()) {
+                    return;
+                }
+                mSession.getCallback().onDisconnected(mSession.getInstance(), controllerInfo);
+            }
+        });
+    }
+
+    @FunctionalInterface
+    private interface Session2Runnable {
+        void run(ControllerInfo controller) throws RemoteException;
+    }
+
+    final class ControllerLegacyCb extends ControllerCb {
+        private final IMediaControllerCallback mIControllerCallback;
+
+        ControllerLegacyCb(@NonNull IMediaControllerCallback callback) {
+            mIControllerCallback = callback;
+        }
+
+        @Override
+        @NonNull IBinder getId() {
+            return mIControllerCallback.asBinder();
+        }
+
+        @Override
+        void onCustomLayoutChanged(List<CommandButton> layout) throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putParcelableArray(ARGUMENT_COMMAND_BUTTONS,
+                    MediaUtils2.toCommandButtonParcelableArray(layout));
+            mIControllerCallback.onEvent(SESSION_EVENT_SET_CUSTOM_LAYOUT, bundle);
+        }
+
+        @Override
+        void onPlaybackInfoChanged(PlaybackInfo info) throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putBundle(ARGUMENT_PLAYBACK_INFO, info.toBundle());
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED, bundle);
+
+        }
+
+        @Override
+        void onAllowedCommandsChanged(SessionCommandGroup2 commands) throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putBundle(ARGUMENT_ALLOWED_COMMANDS, commands.toBundle());
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED, bundle);
+        }
+
+        @Override
+        void onCustomCommand(SessionCommand2 command, Bundle args, ResultReceiver receiver)
+                throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
+            bundle.putBundle(ARGUMENT_ARGUMENTS, args);
+            bundle.putParcelable(ARGUMENT_RESULT_RECEIVER, receiver);
+            mIControllerCallback.onEvent(SESSION_EVENT_SEND_CUSTOM_COMMAND, bundle);
+        }
+
+        @Override
+        void onPlayerStateChanged(int playerState)
+                throws RemoteException {
+            // Note: current position should be also sent to the controller here for controller
+            // to calculate the position more correctly.
+            Bundle bundle = new Bundle();
+            bundle.putInt(ARGUMENT_PLAYER_STATE, playerState);
+            bundle.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT, mSession.getPlaybackStateCompat());
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYER_STATE_CHANGED, bundle);
+        }
+
+        @Override
+        void onPlaybackSpeedChanged(float speed) throws RemoteException {
+            // Note: current position should be also sent to the controller here for controller
+            // to calculate the position more correctly.
+            Bundle bundle = new Bundle();
+            bundle.putParcelable(
+                    ARGUMENT_PLAYBACK_STATE_COMPAT, mSession.getPlaybackStateCompat());
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED, bundle);
+        }
+
+        @Override
+        void onBufferingStateChanged(MediaItem2 item, int state) throws RemoteException {
+            // Note: buffered position should be also sent to the controller here. It's to
+            // follow the behavior of MediaPlayerInterface.PlayerEventCallback.
+            Bundle bundle = new Bundle();
+            bundle.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+            bundle.putInt(ARGUMENT_BUFFERING_STATE, state);
+            bundle.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT,
+                    mSession.getPlaybackStateCompat());
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_BUFFERING_STATE_CHANGED, bundle);
+
+        }
+
+        @Override
+        void onSeekCompleted(long position) throws RemoteException {
+            // Note: current position should be also sent to the controller here because the
+            // position here may refer to the parameter of the previous seek() API calls.
+            Bundle bundle = new Bundle();
+            bundle.putLong(ARGUMENT_SEEK_POSITION, position);
+            bundle.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT,
+                    mSession.getPlaybackStateCompat());
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_SEEK_COMPLETED, bundle);
+        }
+
+        @Override
+        void onError(int errorCode, Bundle extras) throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putInt(ARGUMENT_ERROR_CODE, errorCode);
+            bundle.putBundle(ARGUMENT_EXTRAS, extras);
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_ERROR, bundle);
+        }
+
+        @Override
+        void onCurrentMediaItemChanged(MediaItem2 item) throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putBundle(ARGUMENT_MEDIA_ITEM, (item == null) ? null : item.toBundle());
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED, bundle);
+        }
+
+        @Override
+        void onPlaylistChanged(List<MediaItem2> playlist, MediaMetadata2 metadata)
+                throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putParcelableArray(ARGUMENT_PLAYLIST,
+                    MediaUtils2.toMediaItem2ParcelableArray(playlist));
+            bundle.putBundle(ARGUMENT_PLAYLIST_METADATA,
+                    metadata == null ? null : metadata.toBundle());
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYLIST_CHANGED, bundle);
+        }
+
+        @Override
+        void onPlaylistMetadataChanged(MediaMetadata2 metadata) throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putBundle(ARGUMENT_PLAYLIST_METADATA,
+                    metadata == null ? null : metadata.toBundle());
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED, bundle);
+        }
+
+        @Override
+        void onShuffleModeChanged(int shuffleMode) throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED, bundle);
+        }
+
+        @Override
+        void onRepeatModeChanged(int repeatMode) throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_REPEAT_MODE_CHANGED, bundle);
+        }
+
+        @Override
+        void onRoutesInfoChanged(List<Bundle> routes) throws RemoteException {
+            Bundle bundle = null;
+            if (routes != null) {
+                bundle = new Bundle();
+                bundle.putParcelableArray(ARGUMENT_ROUTE_BUNDLE, routes.toArray(new Bundle[0]));
+            }
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_ROUTES_INFO_CHANGED, bundle);
+        }
+
+        @Override
+        void onChildrenChanged(String parentId, int itemCount, Bundle extras)
+                throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putString(ARGUMENT_MEDIA_ID, parentId);
+            bundle.putInt(ARGUMENT_ITEM_COUNT, itemCount);
+            bundle.putBundle(ARGUMENT_EXTRAS, extras);
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_CHILDREN_CHANGED, bundle);
+        }
+
+        @Override
+        void onSearchResultChanged(String query, int itemCount, Bundle extras)
+                throws RemoteException {
+            Bundle bundle = new Bundle();
+            bundle.putString(ARGUMENT_QUERY, query);
+            bundle.putInt(ARGUMENT_ITEM_COUNT, itemCount);
+            bundle.putBundle(ARGUMENT_EXTRAS, extras);
+            mIControllerCallback.onEvent(SESSION_EVENT_ON_SEARCH_RESULT_CHANGED, bundle);
+        }
+    }
+}
diff --git a/media/src/main/java/androidx/media/MediaSessionManager.java b/media/src/main/java/androidx/media/MediaSessionManager.java
new file mode 100644
index 0000000..8c11228
--- /dev/null
+++ b/media/src/main/java/androidx/media/MediaSessionManager.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import android.content.Context;
+import android.os.Build;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.os.BuildCompat;
+
+/**
+ * Provides support for interacting with {@link MediaSessionCompat media sessions} that
+ * applications have published to express their ongoing media playback state.
+ *
+ * @see MediaSessionCompat
+ * @see MediaControllerCompat
+ */
+public final class MediaSessionManager {
+    static final String TAG = "MediaSessionManager";
+    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final Object sLock = new Object();
+    private static volatile MediaSessionManager sSessionManager;
+
+    MediaSessionManagerImpl mImpl;
+
+    /**
+     * Gets an instance of the media session manager associated with the context.
+     *
+     * @return The MediaSessionManager instance for this context.
+     */
+    public static @NonNull MediaSessionManager getSessionManager(@NonNull Context context) {
+        MediaSessionManager manager = sSessionManager;
+        if (manager == null) {
+            synchronized (sLock) {
+                manager = sSessionManager;
+                if (manager == null) {
+                    sSessionManager = new MediaSessionManager(context.getApplicationContext());
+                    manager = sSessionManager;
+                }
+            }
+        }
+        return manager;
+    }
+
+    private MediaSessionManager(Context context) {
+        if (BuildCompat.isAtLeastP()) {
+            mImpl = new MediaSessionManagerImplApi28(context);
+        } else if (Build.VERSION.SDK_INT >= 21) {
+            mImpl = new MediaSessionManagerImplApi21(context);
+        } else {
+            mImpl = new MediaSessionManagerImplBase(context);
+        }
+    }
+
+    /**
+     * Checks whether the remote user is a trusted app.
+     * <p>
+     * An app is trusted if the app holds the android.Manifest.permission.MEDIA_CONTENT_CONTROL
+     * permission or has an enabled notification listener.
+     *
+     * @param userInfo The remote user info from either
+     *            {@link MediaSessionCompat#getCurrentControllerInfo()} and
+     *            {@link MediaBrowserServiceCompat#getCurrentBrowserInfo()}.
+     * @return {@code true} if the remote user is trusted and its package name matches with the UID.
+     *            {@code false} otherwise.
+     */
+    public boolean isTrustedForMediaControl(@NonNull RemoteUserInfo userInfo) {
+        if (userInfo == null) {
+            throw new IllegalArgumentException("userInfo should not be null");
+        }
+        return mImpl.isTrustedForMediaControl(userInfo.mImpl);
+    }
+
+    Context getContext() {
+        return mImpl.getContext();
+    }
+
+    interface MediaSessionManagerImpl {
+        Context getContext();
+        boolean isTrustedForMediaControl(RemoteUserInfoImpl userInfo);
+    }
+
+    interface RemoteUserInfoImpl {
+        String getPackageName();
+        int getPid();
+        int getUid();
+    }
+
+    /**
+     * Information of a remote user of {@link android.support.v4.media.session.MediaSessionCompat}
+     * or {@link MediaBrowserServiceCompat}.
+     * This can be used to decide whether the remote user is trusted app.
+     *
+     * @see #isTrustedForMediaControl(RemoteUserInfo)
+     */
+    public static final class RemoteUserInfo {
+        /**
+         * Used by {@link #getPackageName()} when the session is connected to the legacy controller
+         * whose exact package name cannot be obtained.
+         */
+        public static String LEGACY_CONTROLLER = "android.media.session.MediaController";
+
+        RemoteUserInfoImpl mImpl;
+
+        public RemoteUserInfo(@NonNull String packageName, int pid, int uid) {
+            if (BuildCompat.isAtLeastP()) {
+                mImpl = new MediaSessionManagerImplApi28.RemoteUserInfo(packageName, pid, uid);
+            } else {
+                mImpl = new MediaSessionManagerImplBase.RemoteUserInfo(packageName, pid, uid);
+            }
+        }
+
+        /**
+         * @return package name of the controller. Can be {@link #LEGACY_CONTROLLER} if the package
+         *         name cannot be obtained.
+         */
+        public @NonNull String getPackageName() {
+            return mImpl.getPackageName();
+        }
+
+        /**
+         * @return pid of the controller
+         */
+        public int getPid() {
+            return mImpl.getPid();
+        }
+
+        /**
+         * @return uid of the controller
+         */
+        public int getUid() {
+            return mImpl.getUid();
+        }
+
+        @Override
+        public boolean equals(@Nullable Object obj) {
+            return mImpl.equals(obj);
+        }
+
+        @Override
+        public int hashCode() {
+            return mImpl.hashCode();
+        }
+    }
+}
diff --git a/media/src/main/java/androidx/media/MediaSessionManagerImplApi21.java b/media/src/main/java/androidx/media/MediaSessionManagerImplApi21.java
new file mode 100644
index 0000000..4fefe70
--- /dev/null
+++ b/media/src/main/java/androidx/media/MediaSessionManagerImplApi21.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+@RequiresApi(21)
+class MediaSessionManagerImplApi21 extends MediaSessionManagerImplBase {
+    MediaSessionManagerImplApi21(Context context) {
+        super(context);
+        mContext = context;
+    }
+
+    @Override
+    public boolean isTrustedForMediaControl(
+            @NonNull MediaSessionManager.RemoteUserInfoImpl userInfo) {
+
+        return hasMediaControlPermission(userInfo) || super.isTrustedForMediaControl(userInfo);
+    }
+
+    /**
+     * Checks the caller has android.Manifest.permission.MEDIA_CONTENT_CONTROL permission.
+     */
+    private boolean hasMediaControlPermission(
+            @NonNull MediaSessionManager.RemoteUserInfoImpl userInfo) {
+        return getContext().checkPermission(
+                android.Manifest.permission.MEDIA_CONTENT_CONTROL,
+                userInfo.getPid(), userInfo.getUid()) == PackageManager.PERMISSION_GRANTED;
+    }
+}
diff --git a/media/src/main/java/androidx/media/MediaSessionManagerImplApi28.java b/media/src/main/java/androidx/media/MediaSessionManagerImplApi28.java
new file mode 100644
index 0000000..48344fa
--- /dev/null
+++ b/media/src/main/java/androidx/media/MediaSessionManagerImplApi28.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import android.content.Context;
+
+import androidx.annotation.RequiresApi;
+
+@RequiresApi(28)
+class MediaSessionManagerImplApi28 extends MediaSessionManagerImplApi21 {
+    android.media.session.MediaSessionManager mObject;
+
+    MediaSessionManagerImplApi28(Context context) {
+        super(context);
+        mObject = (android.media.session.MediaSessionManager) context
+                .getSystemService(Context.MEDIA_SESSION_SERVICE);
+    }
+
+    @Override
+    public boolean isTrustedForMediaControl(MediaSessionManager.RemoteUserInfoImpl userInfo) {
+        if (userInfo instanceof RemoteUserInfo) {
+            return mObject.isTrustedForMediaControl(((RemoteUserInfo) userInfo).mObject);
+        }
+        return false;
+    }
+
+    static final class RemoteUserInfo implements MediaSessionManager.RemoteUserInfoImpl {
+        android.media.session.MediaSessionManager.RemoteUserInfo mObject;
+
+        RemoteUserInfo(String packageName, int pid, int uid) {
+            mObject = new android.media.session.MediaSessionManager.RemoteUserInfo(
+                    packageName, pid, uid);
+        }
+
+        @Override
+        public String getPackageName() {
+            return mObject.getPackageName();
+        }
+
+        @Override
+        public int getPid() {
+            return mObject.getPid();
+        }
+
+        @Override
+        public int getUid() {
+            return mObject.getUid();
+        }
+    }
+}
diff --git a/media/src/main/java/androidx/media/MediaSessionManagerImplBase.java b/media/src/main/java/androidx/media/MediaSessionManagerImplBase.java
new file mode 100644
index 0000000..3fc21f1
--- /dev/null
+++ b/media/src/main/java/androidx/media/MediaSessionManagerImplBase.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Process;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.ObjectsCompat;
+
+class MediaSessionManagerImplBase implements MediaSessionManager.MediaSessionManagerImpl {
+    private static final String TAG = MediaSessionManager.TAG;
+    private static final boolean DEBUG = MediaSessionManager.DEBUG;
+
+    private static final String PERMISSION_STATUS_BAR_SERVICE =
+            "android.permission.STATUS_BAR_SERVICE";
+    private static final String PERMISSION_MEDIA_CONTENT_CONTROL =
+            "android.permission.MEDIA_CONTENT_CONTROL";
+    private static final String ENABLED_NOTIFICATION_LISTENERS = "enabled_notification_listeners";
+
+    Context mContext;
+    ContentResolver mContentResolver;
+
+    MediaSessionManagerImplBase(Context context) {
+        mContext = context;
+        mContentResolver = mContext.getContentResolver();
+    }
+
+    @Override
+    public Context getContext() {
+        return mContext;
+    }
+
+    @Override
+    public boolean isTrustedForMediaControl(
+            @NonNull MediaSessionManager.RemoteUserInfoImpl userInfo) {
+        ApplicationInfo applicationInfo;
+        try {
+            applicationInfo = mContext.getPackageManager().getApplicationInfo(
+                    userInfo.getPackageName(), 0);
+        } catch (PackageManager.NameNotFoundException e) {
+            if (DEBUG) {
+                Log.d(TAG, "Package " + userInfo.getPackageName() + " doesn't exist");
+            }
+            return false;
+        }
+
+        if (applicationInfo.uid != userInfo.getUid()) {
+            if (DEBUG) {
+                Log.d(TAG, "Package name " + userInfo.getPackageName()
+                        + " doesn't match with the uid " + userInfo.getUid());
+            }
+            return false;
+        }
+        return isPermissionGranted(userInfo, PERMISSION_STATUS_BAR_SERVICE)
+                || isPermissionGranted(userInfo, PERMISSION_MEDIA_CONTENT_CONTROL)
+                || userInfo.getUid() == Process.SYSTEM_UID
+                || isEnabledNotificationListener(userInfo);
+    }
+
+    private boolean isPermissionGranted(MediaSessionManager.RemoteUserInfoImpl userInfo,
+            String permission) {
+        if (userInfo.getPid() < 0) {
+            // This may happen for the MediaBrowserServiceCompat#onGetRoot().
+            return mContext.getPackageManager().checkPermission(
+                    permission, userInfo.getPackageName()) == PackageManager.PERMISSION_GRANTED;
+        }
+        return mContext.checkPermission(permission, userInfo.getPid(), userInfo.getUid())
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
+    /**
+     * This checks if the component is an enabled notification listener for the
+     * specified user. Enabled components may only operate on behalf of the user
+     * they're running as.
+     *
+     * @return True if the component is enabled, false otherwise
+     */
+    @SuppressWarnings("StringSplitter")
+    boolean isEnabledNotificationListener(
+            @NonNull MediaSessionManager.RemoteUserInfoImpl userInfo) {
+        final String enabledNotifListeners = Settings.Secure.getString(mContentResolver,
+                ENABLED_NOTIFICATION_LISTENERS);
+        if (enabledNotifListeners != null) {
+            final String[] components = enabledNotifListeners.split(":");
+            for (int i = 0; i < components.length; i++) {
+                final ComponentName component =
+                        ComponentName.unflattenFromString(components[i]);
+                if (component != null) {
+                    if (component.getPackageName().equals(userInfo.getPackageName())) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    static class RemoteUserInfo implements MediaSessionManager.RemoteUserInfoImpl {
+        private String mPackageName;
+        private int mPid;
+        private int mUid;
+
+        RemoteUserInfo(String packageName, int pid, int uid) {
+            mPackageName = packageName;
+            mPid = pid;
+            mUid = uid;
+        }
+
+        @Override
+        public String getPackageName() {
+            return mPackageName;
+        }
+
+        @Override
+        public int getPid() {
+            return mPid;
+        }
+
+        @Override
+        public int getUid() {
+            return mUid;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof RemoteUserInfo)) {
+                return false;
+            }
+            RemoteUserInfo otherUserInfo = (RemoteUserInfo) obj;
+            return TextUtils.equals(mPackageName, otherUserInfo.mPackageName)
+                    && mPid == otherUserInfo.mPid
+                    && mUid == otherUserInfo.mUid;
+        }
+
+        @Override
+        public int hashCode() {
+            return ObjectsCompat.hash(mPackageName, mPid, mUid);
+        }
+    }
+}
+
diff --git a/media/src/main/java/androidx/media/MediaSessionService2.java b/media/src/main/java/androidx/media/MediaSessionService2.java
index 7bad65c..03bb086 100644
--- a/media/src/main/java/androidx/media/MediaSessionService2.java
+++ b/media/src/main/java/androidx/media/MediaSessionService2.java
@@ -16,8 +16,6 @@
 
 package androidx.media;
 
-import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.Service;
@@ -31,7 +29,6 @@
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
 import androidx.media.MediaBrowserServiceCompat.BrowserRoot;
 import androidx.media.MediaSession2.ControllerInfo;
 import androidx.media.SessionToken2.TokenType;
@@ -39,7 +36,6 @@
 import java.util.List;
 
 /**
- * @hide
  * Base class for media session services, which is the service version of the {@link MediaSession2}.
  * <p>
  * It's highly recommended for an app to use this instead of {@link MediaSession2} if it wants
@@ -97,7 +93,6 @@
  * <p>
  * After the binding, session's
  * {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)}
- *
  * will be called to accept or reject connection request from a controller. If the connection is
  * rejected, the controller will unbind. If it's accepted, the controller will be available to use
  * and keep binding.
@@ -106,6 +101,9 @@
  * is called and service would become a foreground service. It's needed to keep playback after the
  * controller is destroyed. The session service becomes background service when the playback is
  * stopped.
+ * <p>
+ * The service is destroyed when the session is closed, or no media controller is bounded to the
+ * session while the service is not running as a foreground service.
  * <a name="Permissions"></a>
  * <h3>Permissions</h3>
  * <p>
@@ -113,10 +111,7 @@
  * the session service accepted the connection request through
  * {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)}.
  */
-@RestrictTo(LIBRARY_GROUP)
 public abstract class MediaSessionService2 extends Service {
-    //private final MediaSessionService2Provider mProvider;
-
     /**
      * This is the interface name that a service implementing a session service should say that it
      * support -- that is, this is the action it uses for its intent filter.
@@ -316,8 +311,6 @@
             //   2. MediaSessionService2 is defined as the simplified version of the library
             //      service with no browsing feature, so shouldn't allow MediaBrowserServiceCompat
             //      specific operations.
-            // TODO: Revisit here API not to return stub root here. The fake media ID here may be
-            //       used by the browser service for real.
             return sDefaultBrowserRoot;
         }
 
diff --git a/media/src/main/java/androidx/media/MediaUtils2.java b/media/src/main/java/androidx/media/MediaUtils2.java
index 657e24d..6a011f2 100644
--- a/media/src/main/java/androidx/media/MediaUtils2.java
+++ b/media/src/main/java/androidx/media/MediaUtils2.java
@@ -16,8 +16,6 @@
 
 package androidx.media;
 
-import static androidx.media.AudioAttributesCompat.CONTENT_TYPE_UNKNOWN;
-import static androidx.media.AudioAttributesCompat.USAGE_UNKNOWN;
 import static androidx.media.MediaMetadata2.METADATA_KEY_DISPLAY_DESCRIPTION;
 import static androidx.media.MediaMetadata2.METADATA_KEY_DISPLAY_ICON;
 import static androidx.media.MediaMetadata2.METADATA_KEY_DISPLAY_ICON_URI;
@@ -44,9 +42,7 @@
 import java.util.List;
 
 class MediaUtils2 {
-    static final String AUDIO_ATTRIBUTES_USAGE = "androidx.media.audio_attrs.USAGE";
-    static final String AUDIO_ATTRIBUTES_CONTENT_TYPE = "androidx.media.audio_attrs.CONTENT_TYPE";
-    static final String AUDIO_ATTRIBUTES_FLAGS = "androidx.media.audio_attrs.FLAGS";
+    static final String TAG = "MediaUtils2";
 
     private MediaUtils2() {
     }
@@ -116,6 +112,28 @@
                 .build();
     }
 
+    static List<MediaItem> fromMediaItem2List(List<MediaItem2> items) {
+        if (items == null) {
+            return null;
+        }
+        List<MediaItem> result = new ArrayList<>();
+        for (int i = 0; i < items.size(); i++) {
+            result.add(createMediaItem(items.get(i)));
+        }
+        return result;
+    }
+
+    static List<MediaItem2> toMediaItem2List(List<MediaItem> items) {
+        if (items == null) {
+            return null;
+        }
+        List<MediaItem2> result = new ArrayList<>();
+        for (int i = 0; i < items.size(); i++) {
+            result.add(createMediaItem2(items.get(i)));
+        }
+        return result;
+    }
+
     /**
      * Creates a {@link MediaMetadata2} from the {@link MediaDescriptionCompat}.
      *
@@ -351,28 +369,6 @@
         return layout;
     }
 
-    static Bundle toAudioAttributesBundle(AudioAttributesCompat attrs) {
-        if (attrs == null) {
-            return null;
-        }
-        Bundle bundle = new Bundle();
-        bundle.putInt(AUDIO_ATTRIBUTES_USAGE, attrs.getUsage());
-        bundle.putInt(AUDIO_ATTRIBUTES_CONTENT_TYPE, attrs.getContentType());
-        bundle.putInt(AUDIO_ATTRIBUTES_FLAGS, attrs.getFlags());
-        return bundle;
-    }
-
-    static AudioAttributesCompat fromAudioAttributesBundle(Bundle bundle) {
-        if (bundle == null) {
-            return null;
-        }
-        return new AudioAttributesCompat.Builder()
-                .setUsage(bundle.getInt(AUDIO_ATTRIBUTES_USAGE, USAGE_UNKNOWN))
-                .setContentType(bundle.getInt(AUDIO_ATTRIBUTES_CONTENT_TYPE, CONTENT_TYPE_UNKNOWN))
-                .setFlags(bundle.getInt(AUDIO_ATTRIBUTES_FLAGS, 0))
-                .build();
-    }
-
     static List<Bundle> toBundleList(Parcelable[] array) {
         if (array == null) {
             return null;
@@ -386,17 +382,17 @@
 
     static int createPlaybackStateCompatState(int playerState, int bufferingState) {
         switch (playerState) {
-            case MediaPlayerBase.PLAYER_STATE_PLAYING:
+            case MediaPlayerInterface.PLAYER_STATE_PLAYING:
                 switch (bufferingState) {
-                    case MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_STARVED:
+                    case MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_STARVED:
                         return PlaybackStateCompat.STATE_BUFFERING;
                 }
                 return PlaybackStateCompat.STATE_PLAYING;
-            case MediaPlayerBase.PLAYER_STATE_PAUSED:
+            case MediaPlayerInterface.PLAYER_STATE_PAUSED:
                 return PlaybackStateCompat.STATE_PAUSED;
-            case MediaPlayerBase.PLAYER_STATE_IDLE:
+            case MediaPlayerInterface.PLAYER_STATE_IDLE:
                 return PlaybackStateCompat.STATE_NONE;
-            case MediaPlayerBase.PLAYER_STATE_ERROR:
+            case MediaPlayerInterface.PLAYER_STATE_ERROR:
                 return PlaybackStateCompat.STATE_ERROR;
         }
         // For unknown value
@@ -406,13 +402,13 @@
     static int toPlayerState(int playbackStateCompatState) {
         switch (playbackStateCompatState) {
             case PlaybackStateCompat.STATE_ERROR:
-                return MediaPlayerBase.PLAYER_STATE_ERROR;
+                return MediaPlayerInterface.PLAYER_STATE_ERROR;
             case PlaybackStateCompat.STATE_NONE:
-                return MediaPlayerBase.PLAYER_STATE_IDLE;
+                return MediaPlayerInterface.PLAYER_STATE_IDLE;
             case PlaybackStateCompat.STATE_PAUSED:
             case PlaybackStateCompat.STATE_STOPPED:
             case PlaybackStateCompat.STATE_BUFFERING: // means paused for buffering.
-                return MediaPlayerBase.PLAYER_STATE_PAUSED;
+                return MediaPlayerInterface.PLAYER_STATE_PAUSED;
             case PlaybackStateCompat.STATE_FAST_FORWARDING:
             case PlaybackStateCompat.STATE_PLAYING:
             case PlaybackStateCompat.STATE_REWINDING:
@@ -420,12 +416,16 @@
             case PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS:
             case PlaybackStateCompat.STATE_SKIPPING_TO_QUEUE_ITEM:
             case PlaybackStateCompat.STATE_CONNECTING: // Note: there's no perfect match for this.
-                return MediaPlayerBase.PLAYER_STATE_PLAYING;
+                return MediaPlayerInterface.PLAYER_STATE_PLAYING;
         }
-        return MediaPlayerBase.PLAYER_STATE_ERROR;
+        return MediaPlayerInterface.PLAYER_STATE_ERROR;
     }
 
     static boolean isDefaultLibraryRootHint(Bundle bundle) {
         return bundle != null && bundle.getBoolean(MediaConstants2.ROOT_EXTRA_DEFAULT, false);
     }
+
+    static Bundle createBundle(Bundle bundle) {
+        return (bundle == null) ? new Bundle() : new Bundle(bundle);
+    }
 }
diff --git a/media/src/main/java/androidx/media/SessionCommand2.java b/media/src/main/java/androidx/media/SessionCommand2.java
index f017941..353688a 100644
--- a/media/src/main/java/androidx/media/SessionCommand2.java
+++ b/media/src/main/java/androidx/media/SessionCommand2.java
@@ -276,52 +276,38 @@
     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;
 
     /**
diff --git a/media/src/main/java/androidx/media/SessionCommandGroup2.java b/media/src/main/java/androidx/media/SessionCommandGroup2.java
index 691eb70..db15bb3 100644
--- a/media/src/main/java/androidx/media/SessionCommandGroup2.java
+++ b/media/src/main/java/androidx/media/SessionCommandGroup2.java
@@ -42,7 +42,7 @@
     private static final String KEY_COMMANDS = "android.media.mediasession2.commandgroup.commands";
     // Prefix for all command codes
     private static final String PREFIX_COMMAND_CODE = "COMMAND_CODE_";
-    // Prefix for command codes that will be sent directly to the MediaPlayerBase
+    // Prefix for command codes that will be sent directly to the MediaPlayerInterface
     private static final String PREFIX_COMMAND_CODE_PLAYBACK = "COMMAND_CODE_PLAYBACK_";
     // Prefix for command codes that will be sent directly to the MediaPlaylistAgent
     private static final String PREFIX_COMMAND_CODE_PLAYLIST = "COMMAND_CODE_PLAYLIST_";
diff --git a/media/src/main/java/androidx/media/SessionPlaylistAgentImplBase.java b/media/src/main/java/androidx/media/SessionPlaylistAgentImplBase.java
index 431b188..2cdc9ab 100644
--- a/media/src/main/java/androidx/media/SessionPlaylistAgentImplBase.java
+++ b/media/src/main/java/androidx/media/SessionPlaylistAgentImplBase.java
@@ -18,13 +18,13 @@
 
 import android.annotation.TargetApi;
 import android.os.Build;
-import android.util.ArrayMap;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
-import androidx.media.MediaPlayerBase.PlayerEventCallback;
+import androidx.collection.ArrayMap;
+import androidx.media.MediaPlayerInterface.PlayerEventCallback;
 import androidx.media.MediaSession2.OnDataSourceMissingHelper;
 
 import java.util.ArrayList;
@@ -46,7 +46,7 @@
     private final MyPlayerEventCallback mPlayerCallback;
 
     @GuardedBy("mLock")
-    private MediaPlayerBase mPlayer;
+    private MediaPlayerInterface mPlayer;
     @GuardedBy("mLock")
     private OnDataSourceMissingHelper mDsmHelper;
     // TODO: Check if having the same item is okay (b/74090741)
@@ -68,7 +68,7 @@
     // Called on session callback executor.
     private class MyPlayerEventCallback extends PlayerEventCallback {
         @Override
-        public void onCurrentDataSourceChanged(@NonNull MediaPlayerBase mpb,
+        public void onCurrentDataSourceChanged(@NonNull MediaPlayerInterface mpb,
                 @Nullable DataSourceDesc dsd) {
             synchronized (mLock) {
                 if (mPlayer != mpb) {
@@ -133,7 +133,7 @@
     }
 
     SessionPlaylistAgentImplBase(@NonNull MediaSession2ImplBase session,
-            @NonNull MediaPlayerBase player) {
+            @NonNull MediaPlayerInterface player) {
         super();
         if (session == null) {
             throw new IllegalArgumentException("sessionImpl shouldn't be null");
@@ -147,7 +147,7 @@
         mPlayer.registerPlayerEventCallback(mSession.getCallbackExecutor(), mPlayerCallback);
     }
 
-    public void setPlayer(@NonNull MediaPlayerBase player) {
+    public void setPlayer(@NonNull MediaPlayerInterface player) {
         if (player == null) {
             throw new IllegalArgumentException("player shouldn't be null");
         }
diff --git a/media/src/main/java/androidx/media/SessionToken2.java b/media/src/main/java/androidx/media/SessionToken2.java
index eb42297..cae6c9b 100644
--- a/media/src/main/java/androidx/media/SessionToken2.java
+++ b/media/src/main/java/androidx/media/SessionToken2.java
@@ -23,7 +23,6 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
-import android.media.session.MediaSessionManager;
 import android.os.Bundle;
 import android.support.v4.media.session.MediaSessionCompat;
 import android.text.TextUtils;
@@ -38,7 +37,8 @@
 import java.util.List;
 
 /**
- * Represents an ongoing {@link MediaSession2}.
+ * Represents an ongoing {@link MediaSession2} or a {@link MediaSessionService2}.
+ * If it's representing a session service, it may not be ongoing.
  * <p>
  * This may be passed to apps by the session owner to allow them to create a
  * {@link MediaController2} to communicate with the session.
@@ -65,15 +65,13 @@
     public static final int TYPE_SESSION = 0;
 
     /**
-     * @hide
+     * Type for {@link MediaSessionService2}.
      */
-    @RestrictTo(LIBRARY_GROUP)
     public static final int TYPE_SESSION_SERVICE = 1;
 
     /**
-     * @hide
+     * Type for {@link MediaLibraryService2}.
      */
-    @RestrictTo(LIBRARY_GROUP)
     public static final int TYPE_LIBRARY_SERVICE = 2;
 
     //private final SessionToken2Provider mProvider;
@@ -97,14 +95,12 @@
     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);
     }
@@ -242,6 +238,8 @@
     /**
      * @return type of the token
      * @see #TYPE_SESSION
+     * @see #TYPE_SESSION_SERVICE
+     * @see #TYPE_LIBRARY_SERVICE
      */
     public @TokenType int getType() {
         return mType;
diff --git a/media/src/main/java/androidx/media/subtitle/Cea608CCParser.java b/media/src/main/java/androidx/media/subtitle/Cea608CCParser.java
new file mode 100644
index 0000000..9205fba
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/Cea608CCParser.java
@@ -0,0 +1,987 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.subtitle;
+
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.TextPaint;
+import android.text.style.CharacterStyle;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+import android.text.style.UpdateAppearance;
+import android.util.Log;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * CCParser processes CEA-608 closed caption data.
+ *
+ * It calls back into OnDisplayChangedListener upon
+ * display change with styled text for rendering.
+ *
+ */
+class Cea608CCParser {
+    public static final int MAX_ROWS = 15;
+    public static final int MAX_COLS = 32;
+
+    private static final String TAG = "Cea608CCParser";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final int INVALID = -1;
+
+    // EIA-CEA-608: Table 70 - Control Codes
+    private static final int RCL = 0x20;
+    private static final int BS  = 0x21;
+    private static final int AOF = 0x22;
+    private static final int AON = 0x23;
+    private static final int DER = 0x24;
+    private static final int RU2 = 0x25;
+    private static final int RU3 = 0x26;
+    private static final int RU4 = 0x27;
+    private static final int FON = 0x28;
+    private static final int RDC = 0x29;
+    private static final int TR  = 0x2a;
+    private static final int RTD = 0x2b;
+    private static final int EDM = 0x2c;
+    private static final int CR  = 0x2d;
+    private static final int ENM = 0x2e;
+    private static final int EOC = 0x2f;
+
+    // Transparent Space
+    private static final char TS = '\u00A0';
+
+    // Captioning Modes
+    private static final int MODE_UNKNOWN = 0;
+    private static final int MODE_PAINT_ON = 1;
+    private static final int MODE_ROLL_UP = 2;
+    private static final int MODE_POP_ON = 3;
+    private static final int MODE_TEXT = 4;
+
+    private final DisplayListener mListener;
+
+    private int mMode = MODE_PAINT_ON;
+    private int mRollUpSize = 4;
+    private int mPrevCtrlCode = INVALID;
+
+    private CCMemory mDisplay = new CCMemory();
+    private CCMemory mNonDisplay = new CCMemory();
+    private CCMemory mTextMem = new CCMemory();
+
+    Cea608CCParser(DisplayListener listener) {
+        mListener = listener;
+    }
+
+    public void parse(byte[] data) {
+        CCData[] ccData = CCData.fromByteArray(data);
+
+        for (int i = 0; i < ccData.length; i++) {
+            if (DEBUG) {
+                Log.d(TAG, ccData[i].toString());
+            }
+
+            if (handleCtrlCode(ccData[i])
+                    || handleTabOffsets(ccData[i])
+                    || handlePACCode(ccData[i])
+                    || handleMidRowCode(ccData[i])) {
+                continue;
+            }
+
+            handleDisplayableChars(ccData[i]);
+        }
+    }
+
+    interface DisplayListener {
+        void onDisplayChanged(SpannableStringBuilder[] styledTexts);
+        CaptionStyle getCaptionStyle();
+    }
+
+    private CCMemory getMemory() {
+        // get the CC memory to operate on for current mode
+        switch (mMode) {
+            case MODE_POP_ON:
+                return mNonDisplay;
+            case MODE_TEXT:
+                // TODO(chz): support only caption mode for now,
+                // in text mode, dump everything to text mem.
+                return mTextMem;
+            case MODE_PAINT_ON:
+            case MODE_ROLL_UP:
+                return mDisplay;
+            default:
+                Log.w(TAG, "unrecoginized mode: " + mMode);
+        }
+        return mDisplay;
+    }
+
+    private boolean handleDisplayableChars(CCData ccData) {
+        if (!ccData.isDisplayableChar()) {
+            return false;
+        }
+
+        // Extended char includes 1 automatic backspace
+        if (ccData.isExtendedChar()) {
+            getMemory().bs();
+        }
+
+        getMemory().writeText(ccData.getDisplayText());
+
+        if (mMode == MODE_PAINT_ON || mMode == MODE_ROLL_UP) {
+            updateDisplay();
+        }
+
+        return true;
+    }
+
+    private boolean handleMidRowCode(CCData ccData) {
+        StyleCode m = ccData.getMidRow();
+        if (m != null) {
+            getMemory().writeMidRowCode(m);
+            return true;
+        }
+        return false;
+    }
+
+    private boolean handlePACCode(CCData ccData) {
+        PAC pac = ccData.getPAC();
+
+        if (pac != null) {
+            if (mMode == MODE_ROLL_UP) {
+                getMemory().moveBaselineTo(pac.getRow(), mRollUpSize);
+            }
+            getMemory().writePAC(pac);
+            return true;
+        }
+
+        return false;
+    }
+
+    private boolean handleTabOffsets(CCData ccData) {
+        int tabs = ccData.getTabOffset();
+
+        if (tabs > 0) {
+            getMemory().tab(tabs);
+            return true;
+        }
+
+        return false;
+    }
+
+    private boolean handleCtrlCode(CCData ccData) {
+        int ctrlCode = ccData.getCtrlCode();
+
+        if (mPrevCtrlCode != INVALID && mPrevCtrlCode == ctrlCode) {
+            // discard double ctrl codes (but if there's a 3rd one, we still take that)
+            mPrevCtrlCode = INVALID;
+            return true;
+        }
+
+        switch(ctrlCode) {
+            case RCL:
+                // select pop-on style
+                mMode = MODE_POP_ON;
+                break;
+            case BS:
+                getMemory().bs();
+                break;
+            case DER:
+                getMemory().der();
+                break;
+            case RU2:
+            case RU3:
+            case RU4:
+                mRollUpSize = (ctrlCode - 0x23);
+                // erase memory if currently in other style
+                if (mMode != MODE_ROLL_UP) {
+                    mDisplay.erase();
+                    mNonDisplay.erase();
+                }
+                // select roll-up style
+                mMode = MODE_ROLL_UP;
+                break;
+            case FON:
+                Log.i(TAG, "Flash On");
+                break;
+            case RDC:
+                // select paint-on style
+                mMode = MODE_PAINT_ON;
+                break;
+            case TR:
+                mMode = MODE_TEXT;
+                mTextMem.erase();
+                break;
+            case RTD:
+                mMode = MODE_TEXT;
+                break;
+            case EDM:
+                // erase display memory
+                mDisplay.erase();
+                updateDisplay();
+                break;
+            case CR:
+                if (mMode == MODE_ROLL_UP) {
+                    getMemory().rollUp(mRollUpSize);
+                } else {
+                    getMemory().cr();
+                }
+                if (mMode == MODE_ROLL_UP) {
+                    updateDisplay();
+                }
+                break;
+            case ENM:
+                // erase non-display memory
+                mNonDisplay.erase();
+                break;
+            case EOC:
+                // swap display/non-display memory
+                swapMemory();
+                // switch to pop-on style
+                mMode = MODE_POP_ON;
+                updateDisplay();
+                break;
+            case INVALID:
+            default:
+                mPrevCtrlCode = INVALID;
+                return false;
+        }
+
+        mPrevCtrlCode = ctrlCode;
+
+        // handled
+        return true;
+    }
+
+    private void updateDisplay() {
+        if (mListener != null) {
+            CaptionStyle captionStyle = mListener.getCaptionStyle();
+            mListener.onDisplayChanged(mDisplay.getStyledText(captionStyle));
+        }
+    }
+
+    private void swapMemory() {
+        CCMemory temp = mDisplay;
+        mDisplay = mNonDisplay;
+        mNonDisplay = temp;
+    }
+
+    private static class StyleCode {
+        static final int COLOR_WHITE = 0;
+        static final int COLOR_GREEN = 1;
+        static final int COLOR_BLUE = 2;
+        static final int COLOR_CYAN = 3;
+        static final int COLOR_RED = 4;
+        static final int COLOR_YELLOW = 5;
+        static final int COLOR_MAGENTA = 6;
+        static final int COLOR_INVALID = 7;
+
+        static final int STYLE_ITALICS   = 0x00000001;
+        static final int STYLE_UNDERLINE = 0x00000002;
+
+        static final String[] sColorMap = {
+            "WHITE", "GREEN", "BLUE", "CYAN", "RED", "YELLOW", "MAGENTA", "INVALID"
+        };
+
+        final int mStyle;
+        final int mColor;
+
+        static StyleCode fromByte(byte data2) {
+            int style = 0;
+            int color = (data2 >> 1) & 0x7;
+
+            if ((data2 & 0x1) != 0) {
+                style |= STYLE_UNDERLINE;
+            }
+
+            if (color == COLOR_INVALID) {
+                // WHITE ITALICS
+                color = COLOR_WHITE;
+                style |= STYLE_ITALICS;
+            }
+
+            return new StyleCode(style, color);
+        }
+
+        StyleCode(int style, int color) {
+            mStyle = style;
+            mColor = color;
+        }
+
+        boolean isItalics() {
+            return (mStyle & STYLE_ITALICS) != 0;
+        }
+
+        boolean isUnderline() {
+            return (mStyle & STYLE_UNDERLINE) != 0;
+        }
+
+        int getColor() {
+            return mColor;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder str = new StringBuilder();
+            str.append("{");
+            str.append(sColorMap[mColor]);
+            if ((mStyle & STYLE_ITALICS) != 0) {
+                str.append(", ITALICS");
+            }
+            if ((mStyle & STYLE_UNDERLINE) != 0) {
+                str.append(", UNDERLINE");
+            }
+            str.append("}");
+
+            return str.toString();
+        }
+    }
+
+    private static class PAC extends StyleCode {
+        final int mRow;
+        final int mCol;
+
+        static PAC fromBytes(byte data1, byte data2) {
+            int[] rowTable = {11, 1, 3, 12, 14, 5, 7, 9};
+            int row = rowTable[data1 & 0x07] + ((data2 & 0x20) >> 5);
+            int style = 0;
+            if ((data2 & 1) != 0) {
+                style |= STYLE_UNDERLINE;
+            }
+            if ((data2 & 0x10) != 0) {
+                // indent code
+                int indent = (data2 >> 1) & 0x7;
+                return new PAC(row, indent * 4, style, COLOR_WHITE);
+            } else {
+                // style code
+                int color = (data2 >> 1) & 0x7;
+
+                if (color == COLOR_INVALID) {
+                    // WHITE ITALICS
+                    color = COLOR_WHITE;
+                    style |= STYLE_ITALICS;
+                }
+                return new PAC(row, -1, style, color);
+            }
+        }
+
+        PAC(int row, int col, int style, int color) {
+            super(style, color);
+            mRow = row;
+            mCol = col;
+        }
+
+        boolean isIndentPAC() {
+            return (mCol >= 0);
+        }
+
+        int getRow() {
+            return mRow;
+        }
+
+        int getCol() {
+            return mCol;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("{%d, %d}, %s",
+                    mRow, mCol, super.toString());
+        }
+    }
+
+    /**
+     * Mutable version of BackgroundSpan to facilitate text rendering with edge styles.
+     */
+    public static class MutableBackgroundColorSpan extends CharacterStyle
+            implements UpdateAppearance {
+        private int mColor;
+
+        MutableBackgroundColorSpan(int color) {
+            mColor = color;
+        }
+
+        public void setBackgroundColor(int color) {
+            mColor = color;
+        }
+
+        public int getBackgroundColor() {
+            return mColor;
+        }
+
+        @Override
+        public void updateDrawState(TextPaint ds) {
+            ds.bgColor = mColor;
+        }
+    }
+
+    /* CCLineBuilder keeps track of displayable chars, as well as
+     * MidRow styles and PACs, for a single line of CC memory.
+     *
+     * It generates styled text via getStyledText() method.
+     */
+    private static class CCLineBuilder {
+        private final StringBuilder mDisplayChars;
+        private final StyleCode[] mMidRowStyles;
+        private final StyleCode[] mPACStyles;
+
+        CCLineBuilder(String str) {
+            mDisplayChars = new StringBuilder(str);
+            mMidRowStyles = new StyleCode[mDisplayChars.length()];
+            mPACStyles = new StyleCode[mDisplayChars.length()];
+        }
+
+        void setCharAt(int index, char ch) {
+            mDisplayChars.setCharAt(index, ch);
+            mMidRowStyles[index] = null;
+        }
+
+        void setMidRowAt(int index, StyleCode m) {
+            mDisplayChars.setCharAt(index, ' ');
+            mMidRowStyles[index] = m;
+        }
+
+        void setPACAt(int index, PAC pac) {
+            mPACStyles[index] = pac;
+        }
+
+        char charAt(int index) {
+            return mDisplayChars.charAt(index);
+        }
+
+        int length() {
+            return mDisplayChars.length();
+        }
+
+        void applyStyleSpan(
+                SpannableStringBuilder styledText,
+                StyleCode s, int start, int end) {
+            if (s.isItalics()) {
+                styledText.setSpan(
+                        new StyleSpan(android.graphics.Typeface.ITALIC),
+                        start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+            }
+            if (s.isUnderline()) {
+                styledText.setSpan(
+                        new UnderlineSpan(),
+                        start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+            }
+        }
+
+        SpannableStringBuilder getStyledText(CaptionStyle captionStyle) {
+            SpannableStringBuilder styledText = new SpannableStringBuilder(mDisplayChars);
+            int start = -1, next = 0;
+            int styleStart = -1;
+            StyleCode curStyle = null;
+            while (next < mDisplayChars.length()) {
+                StyleCode newStyle = null;
+                if (mMidRowStyles[next] != null) {
+                    // apply mid-row style change
+                    newStyle = mMidRowStyles[next];
+                } else if (mPACStyles[next] != null && (styleStart < 0 || start < 0)) {
+                    // apply PAC style change, only if:
+                    // 1. no style set, or
+                    // 2. style set, but prev char is none-displayable
+                    newStyle = mPACStyles[next];
+                }
+                if (newStyle != null) {
+                    curStyle = newStyle;
+                    if (styleStart >= 0 && start >= 0) {
+                        applyStyleSpan(styledText, newStyle, styleStart, next);
+                    }
+                    styleStart = next;
+                }
+
+                if (mDisplayChars.charAt(next) != TS) {
+                    if (start < 0) {
+                        start = next;
+                    }
+                } else if (start >= 0) {
+                    int expandedStart = mDisplayChars.charAt(start) == ' ' ? start : start - 1;
+                    int expandedEnd = mDisplayChars.charAt(next - 1) == ' ' ? next : next + 1;
+                    styledText.setSpan(
+                            new MutableBackgroundColorSpan(captionStyle.backgroundColor),
+                            expandedStart, expandedEnd,
+                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                    if (styleStart >= 0) {
+                        applyStyleSpan(styledText, curStyle, styleStart, expandedEnd);
+                    }
+                    start = -1;
+                }
+                next++;
+            }
+
+            return styledText;
+        }
+    }
+
+    /*
+     * CCMemory models a console-style display.
+     */
+    private static class CCMemory {
+        private final String mBlankLine;
+        private final CCLineBuilder[] mLines = new CCLineBuilder[MAX_ROWS + 2];
+        private int mRow;
+        private int mCol;
+
+        CCMemory() {
+            char[] blank = new char[MAX_COLS + 2];
+            Arrays.fill(blank, TS);
+            mBlankLine = new String(blank);
+        }
+
+        void erase() {
+            // erase all lines
+            for (int i = 0; i < mLines.length; i++) {
+                mLines[i] = null;
+            }
+            mRow = MAX_ROWS;
+            mCol = 1;
+        }
+
+        void der() {
+            if (mLines[mRow] != null) {
+                for (int i = 0; i < mCol; i++) {
+                    if (mLines[mRow].charAt(i) != TS) {
+                        for (int j = mCol; j < mLines[mRow].length(); j++) {
+                            mLines[j].setCharAt(j, TS);
+                        }
+                        return;
+                    }
+                }
+                mLines[mRow] = null;
+            }
+        }
+
+        void tab(int tabs) {
+            moveCursorByCol(tabs);
+        }
+
+        void bs() {
+            moveCursorByCol(-1);
+            if (mLines[mRow] != null) {
+                mLines[mRow].setCharAt(mCol, TS);
+                if (mCol == MAX_COLS - 1) {
+                    // Spec recommendation:
+                    // if cursor was at col 32, move cursor
+                    // back to col 31 and erase both col 31&32
+                    mLines[mRow].setCharAt(MAX_COLS, TS);
+                }
+            }
+        }
+
+        void cr() {
+            moveCursorTo(mRow + 1, 1);
+        }
+
+        void rollUp(int windowSize) {
+            int i;
+            for (i = 0; i <= mRow - windowSize; i++) {
+                mLines[i] = null;
+            }
+            int startRow = mRow - windowSize + 1;
+            if (startRow < 1) {
+                startRow = 1;
+            }
+            for (i = startRow; i < mRow; i++) {
+                mLines[i] = mLines[i + 1];
+            }
+            for (i = mRow; i < mLines.length; i++) {
+                // clear base row
+                mLines[i] = null;
+            }
+            // default to col 1, in case PAC is not sent
+            mCol = 1;
+        }
+
+        void writeText(String text) {
+            for (int i = 0; i < text.length(); i++) {
+                getLineBuffer(mRow).setCharAt(mCol, text.charAt(i));
+                moveCursorByCol(1);
+            }
+        }
+
+        void writeMidRowCode(StyleCode m) {
+            getLineBuffer(mRow).setMidRowAt(mCol, m);
+            moveCursorByCol(1);
+        }
+
+        void writePAC(PAC pac) {
+            if (pac.isIndentPAC()) {
+                moveCursorTo(pac.getRow(), pac.getCol());
+            } else {
+                moveCursorTo(pac.getRow(), 1);
+            }
+            getLineBuffer(mRow).setPACAt(mCol, pac);
+        }
+
+        SpannableStringBuilder[] getStyledText(CaptionStyle captionStyle) {
+            ArrayList<SpannableStringBuilder> rows = new ArrayList<>(MAX_ROWS);
+            for (int i = 1; i <= MAX_ROWS; i++) {
+                rows.add(mLines[i] != null ? mLines[i].getStyledText(captionStyle) : null);
+            }
+            return rows.toArray(new SpannableStringBuilder[MAX_ROWS]);
+        }
+
+        private static int clamp(int x, int min, int max) {
+            return x < min ? min : (x > max ? max : x);
+        }
+
+        private void moveCursorTo(int row, int col) {
+            mRow = clamp(row, 1, MAX_ROWS);
+            mCol = clamp(col, 1, MAX_COLS);
+        }
+
+        private void moveCursorToRow(int row) {
+            mRow = clamp(row, 1, MAX_ROWS);
+        }
+
+        private void moveCursorByCol(int col) {
+            mCol = clamp(mCol + col, 1, MAX_COLS);
+        }
+
+        private void moveBaselineTo(int baseRow, int windowSize) {
+            if (mRow == baseRow) {
+                return;
+            }
+            int actualWindowSize = windowSize;
+            if (baseRow < actualWindowSize) {
+                actualWindowSize = baseRow;
+            }
+            if (mRow < actualWindowSize) {
+                actualWindowSize = mRow;
+            }
+
+            int i;
+            if (baseRow < mRow) {
+                // copy from bottom to top row
+                for (i = actualWindowSize - 1; i >= 0; i--) {
+                    mLines[baseRow - i] = mLines[mRow - i];
+                }
+            } else {
+                // copy from top to bottom row
+                for (i = 0; i < actualWindowSize; i++) {
+                    mLines[baseRow - i] = mLines[mRow - i];
+                }
+            }
+            // clear rest of the rows
+            for (i = 0; i <= baseRow - windowSize; i++) {
+                mLines[i] = null;
+            }
+            for (i = baseRow + 1; i < mLines.length; i++) {
+                mLines[i] = null;
+            }
+        }
+
+        private CCLineBuilder getLineBuffer(int row) {
+            if (mLines[row] == null) {
+                mLines[row] = new CCLineBuilder(mBlankLine);
+            }
+            return mLines[row];
+        }
+    }
+
+    /*
+     * CCData parses the raw CC byte pair into displayable chars,
+     * misc control codes, Mid-Row or Preamble Address Codes.
+     */
+    private static class CCData {
+        private final byte mType;
+        private final byte mData1;
+        private final byte mData2;
+
+        private static final String[] sCtrlCodeMap = {
+            "RCL", "BS" , "AOF", "AON",
+            "DER", "RU2", "RU3", "RU4",
+            "FON", "RDC", "TR" , "RTD",
+            "EDM", "CR" , "ENM", "EOC",
+        };
+
+        private static final String[] sSpecialCharMap = {
+            "\u00AE",
+            "\u00B0",
+            "\u00BD",
+            "\u00BF",
+            "\u2122",
+            "\u00A2",
+            "\u00A3",
+            "\u266A", // Eighth note
+            "\u00E0",
+            "\u00A0", // Transparent space
+            "\u00E8",
+            "\u00E2",
+            "\u00EA",
+            "\u00EE",
+            "\u00F4",
+            "\u00FB",
+        };
+
+        private static final String[] sSpanishCharMap = {
+            // Spanish and misc chars
+            "\u00C1", // A
+            "\u00C9", // E
+            "\u00D3", // I
+            "\u00DA", // O
+            "\u00DC", // U
+            "\u00FC", // u
+            "\u2018", // opening single quote
+            "\u00A1", // inverted exclamation mark
+            "*",
+            "'",
+            "\u2014", // em dash
+            "\u00A9", // Copyright
+            "\u2120", // Servicemark
+            "\u2022", // round bullet
+            "\u201C", // opening double quote
+            "\u201D", // closing double quote
+            // French
+            "\u00C0",
+            "\u00C2",
+            "\u00C7",
+            "\u00C8",
+            "\u00CA",
+            "\u00CB",
+            "\u00EB",
+            "\u00CE",
+            "\u00CF",
+            "\u00EF",
+            "\u00D4",
+            "\u00D9",
+            "\u00F9",
+            "\u00DB",
+            "\u00AB",
+            "\u00BB"
+        };
+
+        private static final String[] sProtugueseCharMap = {
+            // Portuguese
+            "\u00C3",
+            "\u00E3",
+            "\u00CD",
+            "\u00CC",
+            "\u00EC",
+            "\u00D2",
+            "\u00F2",
+            "\u00D5",
+            "\u00F5",
+            "{",
+            "}",
+            "\\",
+            "^",
+            "_",
+            "|",
+            "~",
+            // German and misc chars
+            "\u00C4",
+            "\u00E4",
+            "\u00D6",
+            "\u00F6",
+            "\u00DF",
+            "\u00A5",
+            "\u00A4",
+            "\u2502", // vertical bar
+            "\u00C5",
+            "\u00E5",
+            "\u00D8",
+            "\u00F8",
+            "\u250C", // top-left corner
+            "\u2510", // top-right corner
+            "\u2514", // lower-left corner
+            "\u2518", // lower-right corner
+        };
+
+        static CCData[] fromByteArray(byte[] data) {
+            CCData[] ccData = new CCData[data.length / 3];
+
+            for (int i = 0; i < ccData.length; i++) {
+                ccData[i] = new CCData(
+                        data[i * 3],
+                        data[i * 3 + 1],
+                        data[i * 3 + 2]);
+            }
+
+            return ccData;
+        }
+
+        CCData(byte type, byte data1, byte data2) {
+            mType = type;
+            mData1 = data1;
+            mData2 = data2;
+        }
+
+        int getCtrlCode() {
+            if ((mData1 == 0x14 || mData1 == 0x1c)
+                    && mData2 >= 0x20 && mData2 <= 0x2f) {
+                return mData2;
+            }
+            return INVALID;
+        }
+
+        StyleCode getMidRow() {
+            // only support standard Mid-row codes, ignore
+            // optional background/foreground mid-row codes
+            if ((mData1 == 0x11 || mData1 == 0x19)
+                    && mData2 >= 0x20 && mData2 <= 0x2f) {
+                return StyleCode.fromByte(mData2);
+            }
+            return null;
+        }
+
+        PAC getPAC() {
+            if ((mData1 & 0x70) == 0x10
+                    && (mData2 & 0x40) == 0x40
+                    && ((mData1 & 0x07) != 0 || (mData2 & 0x20) == 0)) {
+                return PAC.fromBytes(mData1, mData2);
+            }
+            return null;
+        }
+
+        int getTabOffset() {
+            if ((mData1 == 0x17 || mData1 == 0x1f)
+                    && mData2 >= 0x21 && mData2 <= 0x23) {
+                return mData2 & 0x3;
+            }
+            return 0;
+        }
+
+        boolean isDisplayableChar() {
+            return isBasicChar() || isSpecialChar() || isExtendedChar();
+        }
+
+        String getDisplayText() {
+            String str = getBasicChars();
+
+            if (str == null) {
+                str =  getSpecialChar();
+
+                if (str == null) {
+                    str = getExtendedChar();
+                }
+            }
+
+            return str;
+        }
+
+        private String ctrlCodeToString(int ctrlCode) {
+            return sCtrlCodeMap[ctrlCode - 0x20];
+        }
+
+        private boolean isBasicChar() {
+            return mData1 >= 0x20 && mData1 <= 0x7f;
+        }
+
+        private boolean isSpecialChar() {
+            return ((mData1 == 0x11 || mData1 == 0x19)
+                    && mData2 >= 0x30 && mData2 <= 0x3f);
+        }
+
+        private boolean isExtendedChar() {
+            return ((mData1 == 0x12 || mData1 == 0x1A
+                    || mData1 == 0x13 || mData1 == 0x1B)
+                    && mData2 >= 0x20 && mData2 <= 0x3f);
+        }
+
+        private char getBasicChar(byte data) {
+            char c;
+            // replace the non-ASCII ones
+            switch (data) {
+                case 0x2A: c = '\u00E1'; break;
+                case 0x5C: c = '\u00E9'; break;
+                case 0x5E: c = '\u00ED'; break;
+                case 0x5F: c = '\u00F3'; break;
+                case 0x60: c = '\u00FA'; break;
+                case 0x7B: c = '\u00E7'; break;
+                case 0x7C: c = '\u00F7'; break;
+                case 0x7D: c = '\u00D1'; break;
+                case 0x7E: c = '\u00F1'; break;
+                case 0x7F: c = '\u2588'; break; // Full block
+                default: c = (char) data; break;
+            }
+            return c;
+        }
+
+        private String getBasicChars() {
+            if (mData1 >= 0x20 && mData1 <= 0x7f) {
+                StringBuilder builder = new StringBuilder(2);
+                builder.append(getBasicChar(mData1));
+                if (mData2 >= 0x20 && mData2 <= 0x7f) {
+                    builder.append(getBasicChar(mData2));
+                }
+                return builder.toString();
+            }
+
+            return null;
+        }
+
+        private String getSpecialChar() {
+            if ((mData1 == 0x11 || mData1 == 0x19)
+                    && mData2 >= 0x30 && mData2 <= 0x3f) {
+                return sSpecialCharMap[mData2 - 0x30];
+            }
+
+            return null;
+        }
+
+        private String getExtendedChar() {
+            if ((mData1 == 0x12 || mData1 == 0x1A) && mData2 >= 0x20 && mData2 <= 0x3f) {
+                // 1 Spanish/French char
+                return sSpanishCharMap[mData2 - 0x20];
+            } else if ((mData1 == 0x13 || mData1 == 0x1B) && mData2 >= 0x20 && mData2 <= 0x3f) {
+                // 1 Portuguese/German/Danish char
+                return sProtugueseCharMap[mData2 - 0x20];
+            }
+
+            return null;
+        }
+
+        @Override
+        public String toString() {
+            String str;
+
+            if (mData1 < 0x10 && mData2 < 0x10) {
+                // Null Pad, ignore
+                return String.format("[%d]Null: %02x %02x", mType, mData1, mData2);
+            }
+
+            int ctrlCode = getCtrlCode();
+            if (ctrlCode != INVALID) {
+                return String.format("[%d]%s", mType, ctrlCodeToString(ctrlCode));
+            }
+
+            int tabOffset = getTabOffset();
+            if (tabOffset > 0) {
+                return String.format("[%d]Tab%d", mType, tabOffset);
+            }
+
+            PAC pac = getPAC();
+            if (pac != null) {
+                return String.format("[%d]PAC: %s", mType, pac.toString());
+            }
+
+            StyleCode m = getMidRow();
+            if (m != null) {
+                return String.format("[%d]Mid-row: %s", mType, m.toString());
+            }
+
+            if (isDisplayableChar()) {
+                return String.format("[%d]Displayable: %s (%02x %02x)",
+                        mType, getDisplayText(), mData1, mData2);
+            }
+
+            return String.format("[%d]Invalid: %02x %02x", mType, mData1, mData2);
+        }
+    }
+}
diff --git a/media/src/main/java/androidx/media/subtitle/ClosedCaptionRenderer.java b/media/src/main/java/androidx/media/subtitle/ClosedCaptionRenderer.java
new file mode 100644
index 0000000..90ff516
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/ClosedCaptionRenderer.java
@@ -0,0 +1,402 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.subtitle;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.media.MediaFormat;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.media.R;
+
+import java.util.ArrayList;
+
+// Note: This is forked from android.media.ClosedCaptionRenderer since P
+/**
+ * @hide
+ */
+@RequiresApi(28)
+@RestrictTo(LIBRARY_GROUP)
+public class ClosedCaptionRenderer extends SubtitleController.Renderer {
+    private final Context mContext;
+    private Cea608CCWidget mCCWidget;
+
+    public ClosedCaptionRenderer(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    public boolean supports(MediaFormat format) {
+        if (format.containsKey(MediaFormat.KEY_MIME)) {
+            String mimeType = format.getString(MediaFormat.KEY_MIME);
+            return MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType);
+        }
+        return false;
+    }
+
+    @Override
+    public SubtitleTrack createTrack(MediaFormat format) {
+        String mimeType = format.getString(MediaFormat.KEY_MIME);
+        if (MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType)) {
+            if (mCCWidget == null) {
+                mCCWidget = new Cea608CCWidget(mContext);
+            }
+            return new Cea608CaptionTrack(mCCWidget, format);
+        }
+        throw new RuntimeException("No matching format: " + format.toString());
+    }
+
+    static class Cea608CaptionTrack extends SubtitleTrack {
+        private final Cea608CCParser mCCParser;
+        private final Cea608CCWidget mRenderingWidget;
+
+        Cea608CaptionTrack(Cea608CCWidget renderingWidget, MediaFormat format) {
+            super(format);
+
+            mRenderingWidget = renderingWidget;
+            mCCParser = new Cea608CCParser(mRenderingWidget);
+        }
+
+        @Override
+        public void onData(byte[] data, boolean eos, long runID) {
+            mCCParser.parse(data);
+        }
+
+        @Override
+        public RenderingWidget getRenderingWidget() {
+            return mRenderingWidget;
+        }
+
+        @Override
+        public void updateView(ArrayList<Cue> activeCues) {
+            // Overriding with NO-OP, CC rendering by-passes this
+        }
+    }
+
+    /**
+     * Widget capable of rendering CEA-608 closed captions.
+     */
+    class Cea608CCWidget extends ClosedCaptionWidget implements Cea608CCParser.DisplayListener {
+        private static final String DUMMY_TEXT = "1234567890123456789012345678901234";
+        private final Rect mTextBounds = new Rect();
+
+        Cea608CCWidget(Context context) {
+            this(context, null);
+        }
+
+        Cea608CCWidget(Context context, AttributeSet attrs) {
+            this(context, attrs, 0);
+        }
+
+        Cea608CCWidget(Context context, AttributeSet attrs, int defStyle) {
+            this(context, attrs, defStyle, 0);
+        }
+
+        Cea608CCWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+            super(context, attrs, defStyleAttr, defStyleRes);
+        }
+
+        @Override
+        public ClosedCaptionLayout createCaptionLayout(Context context) {
+            return new CCLayout(context);
+        }
+
+        @Override
+        public void onDisplayChanged(SpannableStringBuilder[] styledTexts) {
+            ((CCLayout) mClosedCaptionLayout).update(styledTexts);
+
+            if (mListener != null) {
+                mListener.onChanged(this);
+            }
+        }
+
+        @Override
+        public CaptionStyle getCaptionStyle() {
+            return mCaptionStyle;
+        }
+
+        private class CCLineBox extends TextView {
+            private static final float FONT_PADDING_RATIO = 0.75f;
+            private static final float EDGE_OUTLINE_RATIO = 0.1f;
+            private static final float EDGE_SHADOW_RATIO = 0.05f;
+            private float mOutlineWidth;
+            private float mShadowRadius;
+            private float mShadowOffset;
+
+            private int mTextColor = Color.WHITE;
+            private int mBgColor = Color.BLACK;
+            private int mEdgeType = CaptionStyle.EDGE_TYPE_NONE;
+            private int mEdgeColor = Color.TRANSPARENT;
+
+            CCLineBox(Context context) {
+                super(context);
+                setGravity(Gravity.CENTER);
+                setBackgroundColor(Color.TRANSPARENT);
+                setTextColor(Color.WHITE);
+                setTypeface(Typeface.MONOSPACE);
+                setVisibility(View.INVISIBLE);
+
+                final Resources res = getContext().getResources();
+
+                // get the default (will be updated later during measure)
+                mOutlineWidth = res.getDimensionPixelSize(
+                        R.dimen.subtitle_outline_width);
+                mShadowRadius = res.getDimensionPixelSize(
+                        R.dimen.subtitle_shadow_radius);
+                mShadowOffset = res.getDimensionPixelSize(
+                        R.dimen.subtitle_shadow_offset);
+            }
+
+            void setCaptionStyle(CaptionStyle captionStyle) {
+                mTextColor = captionStyle.foregroundColor;
+                mBgColor = captionStyle.backgroundColor;
+                mEdgeType = captionStyle.edgeType;
+                mEdgeColor = captionStyle.edgeColor;
+
+                setTextColor(mTextColor);
+                if (mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
+                    setShadowLayer(mShadowRadius, mShadowOffset, mShadowOffset, mEdgeColor);
+                } else {
+                    setShadowLayer(0, 0, 0, 0);
+                }
+                invalidate();
+            }
+
+            @Override
+            protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+                float fontSize = MeasureSpec.getSize(heightMeasureSpec) * FONT_PADDING_RATIO;
+                setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
+
+                mOutlineWidth = EDGE_OUTLINE_RATIO * fontSize + 1.0f;
+                mShadowRadius = EDGE_SHADOW_RATIO * fontSize + 1.0f;
+                mShadowOffset = mShadowRadius;
+
+                // set font scale in the X direction to match the required width
+                setScaleX(1.0f);
+                getPaint().getTextBounds(DUMMY_TEXT, 0, DUMMY_TEXT.length(), mTextBounds);
+                float actualTextWidth = mTextBounds.width();
+                float requiredTextWidth = MeasureSpec.getSize(widthMeasureSpec);
+                setScaleX(requiredTextWidth / actualTextWidth);
+
+                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+            }
+
+            @Override
+            protected void onDraw(Canvas c) {
+                if (mEdgeType == CaptionStyle.EDGE_TYPE_UNSPECIFIED
+                        || mEdgeType == CaptionStyle.EDGE_TYPE_NONE
+                        || mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
+                    // these edge styles don't require a second pass
+                    super.onDraw(c);
+                    return;
+                }
+
+                if (mEdgeType == CaptionStyle.EDGE_TYPE_OUTLINE) {
+                    drawEdgeOutline(c);
+                } else {
+                    // Raised or depressed
+                    drawEdgeRaisedOrDepressed(c);
+                }
+            }
+
+            @SuppressWarnings("WrongCall")
+            private void drawEdgeOutline(Canvas c) {
+                TextPaint textPaint = getPaint();
+
+                Paint.Style previousStyle = textPaint.getStyle();
+                Paint.Join previousJoin = textPaint.getStrokeJoin();
+                float previousWidth = textPaint.getStrokeWidth();
+
+                setTextColor(mEdgeColor);
+                textPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+                textPaint.setStrokeJoin(Paint.Join.ROUND);
+                textPaint.setStrokeWidth(mOutlineWidth);
+
+                // Draw outline and background only.
+                super.onDraw(c);
+
+                // Restore original settings.
+                setTextColor(mTextColor);
+                textPaint.setStyle(previousStyle);
+                textPaint.setStrokeJoin(previousJoin);
+                textPaint.setStrokeWidth(previousWidth);
+
+                // Remove the background.
+                setBackgroundSpans(Color.TRANSPARENT);
+                // Draw foreground only.
+                super.onDraw(c);
+                // Restore the background.
+                setBackgroundSpans(mBgColor);
+            }
+
+            @SuppressWarnings("WrongCall")
+            private void drawEdgeRaisedOrDepressed(Canvas c) {
+                TextPaint textPaint = getPaint();
+
+                Paint.Style previousStyle = textPaint.getStyle();
+                textPaint.setStyle(Paint.Style.FILL);
+
+                final boolean raised = mEdgeType == CaptionStyle.EDGE_TYPE_RAISED;
+                final int colorUp = raised ? Color.WHITE : mEdgeColor;
+                final int colorDown = raised ? mEdgeColor : Color.WHITE;
+                final float offset = mShadowRadius / 2f;
+
+                // Draw background and text with shadow up
+                setShadowLayer(mShadowRadius, -offset, -offset, colorUp);
+                super.onDraw(c);
+
+                // Remove the background.
+                setBackgroundSpans(Color.TRANSPARENT);
+
+                // Draw text with shadow down
+                setShadowLayer(mShadowRadius, +offset, +offset, colorDown);
+                super.onDraw(c);
+
+                // Restore settings
+                textPaint.setStyle(previousStyle);
+
+                // Restore the background.
+                setBackgroundSpans(mBgColor);
+            }
+
+            private void setBackgroundSpans(int color) {
+                CharSequence text = getText();
+                if (text instanceof Spannable) {
+                    Spannable spannable = (Spannable) text;
+                    Cea608CCParser.MutableBackgroundColorSpan[] bgSpans = spannable.getSpans(
+                            0, spannable.length(), Cea608CCParser.MutableBackgroundColorSpan.class);
+                    for (int i = 0; i < bgSpans.length; i++) {
+                        bgSpans[i].setBackgroundColor(color);
+                    }
+                }
+            }
+        }
+
+        private class CCLayout extends LinearLayout implements ClosedCaptionLayout {
+            private static final int MAX_ROWS = Cea608CCParser.MAX_ROWS;
+            private static final float SAFE_AREA_RATIO = 0.9f;
+
+            private final CCLineBox[] mLineBoxes = new CCLineBox[MAX_ROWS];
+
+            CCLayout(Context context) {
+                super(context);
+                setGravity(Gravity.START);
+                setOrientation(LinearLayout.VERTICAL);
+                for (int i = 0; i < MAX_ROWS; i++) {
+                    mLineBoxes[i] = new CCLineBox(getContext());
+                    addView(mLineBoxes[i], LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+                }
+            }
+
+            @Override
+            public void setCaptionStyle(CaptionStyle captionStyle) {
+                for (int i = 0; i < MAX_ROWS; i++) {
+                    mLineBoxes[i].setCaptionStyle(captionStyle);
+                }
+            }
+
+            @Override
+            public void setFontScale(float fontScale) {
+                // Ignores the font scale changes of the system wide CC preference.
+            }
+
+            void update(SpannableStringBuilder[] textBuffer) {
+                for (int i = 0; i < MAX_ROWS; i++) {
+                    if (textBuffer[i] != null) {
+                        mLineBoxes[i].setText(textBuffer[i], TextView.BufferType.SPANNABLE);
+                        mLineBoxes[i].setVisibility(View.VISIBLE);
+                    } else {
+                        mLineBoxes[i].setVisibility(View.INVISIBLE);
+                    }
+                }
+            }
+
+            @Override
+            protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+                int safeWidth = getMeasuredWidth();
+                int safeHeight = getMeasuredHeight();
+
+                // CEA-608 assumes 4:3 video
+                if (safeWidth * 3 >= safeHeight * 4) {
+                    safeWidth = safeHeight * 4 / 3;
+                } else {
+                    safeHeight = safeWidth * 3 / 4;
+                }
+                safeWidth = (int) (safeWidth * SAFE_AREA_RATIO);
+                safeHeight = (int) (safeHeight * SAFE_AREA_RATIO);
+
+                int lineHeight = safeHeight / MAX_ROWS;
+                int lineHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+                        lineHeight, MeasureSpec.EXACTLY);
+                int lineWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+                        safeWidth, MeasureSpec.EXACTLY);
+
+                for (int i = 0; i < MAX_ROWS; i++) {
+                    mLineBoxes[i].measure(lineWidthMeasureSpec, lineHeightMeasureSpec);
+                }
+            }
+
+            @Override
+            protected void onLayout(boolean changed, int l, int t, int r, int b) {
+                // safe caption area
+                int viewPortWidth = r - l;
+                int viewPortHeight = b - t;
+                int safeWidth, safeHeight;
+                // CEA-608 assumes 4:3 video
+                if (viewPortWidth * 3 >= viewPortHeight * 4) {
+                    safeWidth = viewPortHeight * 4 / 3;
+                    safeHeight = viewPortHeight;
+                } else {
+                    safeWidth = viewPortWidth;
+                    safeHeight = viewPortWidth * 3 / 4;
+                }
+                safeWidth = (int) (safeWidth * SAFE_AREA_RATIO);
+                safeHeight = (int) (safeHeight * SAFE_AREA_RATIO);
+                int left = (viewPortWidth - safeWidth) / 2;
+                int top = (viewPortHeight - safeHeight) / 2;
+
+                for (int i = 0; i < MAX_ROWS; i++) {
+                    mLineBoxes[i].layout(
+                            left,
+                            top + safeHeight * i / MAX_ROWS,
+                            left + safeWidth,
+                            top + safeHeight * (i + 1) / MAX_ROWS);
+                }
+            }
+        }
+    }
+}
diff --git a/media/src/main/java/androidx/media/subtitle/ClosedCaptionWidget.java b/media/src/main/java/androidx/media/subtitle/ClosedCaptionWidget.java
new file mode 100644
index 0000000..a3d3e47
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/ClosedCaptionWidget.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.subtitle;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.CaptioningManager;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
+import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
+
+import androidx.annotation.RequiresApi;
+
+/**
+ * Abstract widget class to render a closed caption track.
+ */
+@RequiresApi(28)
+abstract class ClosedCaptionWidget extends ViewGroup implements SubtitleTrack.RenderingWidget {
+
+    interface ClosedCaptionLayout {
+        void setCaptionStyle(CaptionStyle captionStyle);
+        void setFontScale(float scale);
+    }
+
+    /** Captioning manager, used to obtain and track caption properties. */
+    private final CaptioningManager mManager;
+
+    /** Current caption style. */
+    protected CaptionStyle mCaptionStyle;
+
+    /** Callback for rendering changes. */
+    protected OnChangedListener mListener;
+
+    /** Concrete layout of CC. */
+    protected ClosedCaptionLayout mClosedCaptionLayout;
+
+    /** Whether a caption style change listener is registered. */
+    private boolean mHasChangeListener;
+
+    ClosedCaptionWidget(Context context) {
+        this(context, null);
+    }
+
+    ClosedCaptionWidget(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyle) {
+        this(context, attrs, defStyle, 0);
+    }
+
+    ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        // Cannot render text over video when layer type is hardware.
+        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+
+        mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
+        mCaptionStyle = mManager.getUserStyle();
+
+        mClosedCaptionLayout = createCaptionLayout(context);
+        mClosedCaptionLayout.setCaptionStyle(mCaptionStyle);
+        mClosedCaptionLayout.setFontScale(mManager.getFontScale());
+        addView((ViewGroup) mClosedCaptionLayout, LayoutParams.MATCH_PARENT,
+                LayoutParams.MATCH_PARENT);
+
+        requestLayout();
+    }
+
+    public abstract ClosedCaptionLayout createCaptionLayout(Context context);
+
+    @Override
+    public void setOnChangedListener(OnChangedListener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    public void setSize(int width, int height) {
+        final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
+        final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+
+        measure(widthSpec, heightSpec);
+        layout(0, 0, width, height);
+    }
+
+    @Override
+    public void setVisible(boolean visible) {
+        if (visible) {
+            setVisibility(View.VISIBLE);
+        } else {
+            setVisibility(View.GONE);
+        }
+
+        manageChangeListener();
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        manageChangeListener();
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        manageChangeListener();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        ((ViewGroup) mClosedCaptionLayout).measure(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        ((ViewGroup) mClosedCaptionLayout).layout(l, t, r, b);
+    }
+
+    /**
+     * Manages whether this renderer is listening for caption style changes.
+     */
+    private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() {
+        @Override
+        public void onUserStyleChanged(CaptionStyle userStyle) {
+            mCaptionStyle = userStyle;
+            mClosedCaptionLayout.setCaptionStyle(mCaptionStyle);
+        }
+
+        @Override
+        public void onFontScaleChanged(float fontScale) {
+            mClosedCaptionLayout.setFontScale(fontScale);
+        }
+    };
+
+    private void manageChangeListener() {
+        final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE;
+        if (mHasChangeListener != needsListener) {
+            mHasChangeListener = needsListener;
+
+            if (needsListener) {
+                mManager.addCaptioningChangeListener(mCaptioningListener);
+            } else {
+                mManager.removeCaptioningChangeListener(mCaptioningListener);
+            }
+        }
+    }
+}
+
diff --git a/media/src/main/java/androidx/media/subtitle/MediaTimeProvider.java b/media/src/main/java/androidx/media/subtitle/MediaTimeProvider.java
new file mode 100644
index 0000000..b6f0a14
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/MediaTimeProvider.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.subtitle;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import androidx.annotation.RestrictTo;
+
+// Note: This is just copied from android.media.MediaTimeProvider.
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public interface MediaTimeProvider {
+    // we do not allow negative media time
+    /**
+     * Presentation time value if no timed event notification is requested.
+     */
+    long NO_TIME = -1;
+
+    /**
+     * Cancels all previous notification request from this listener if any.  It
+     * registers the listener to get seek and stop notifications.  If timeUs is
+     * not negative, it also registers the listener for a timed event
+     * notification when the presentation time reaches (becomes greater) than
+     * the value specified.  This happens immediately if the current media time
+     * is larger than or equal to timeUs.
+     *
+     * @param timeUs presentation time to get timed event callback at (or
+     *               {@link #NO_TIME})
+     */
+    void notifyAt(long timeUs, OnMediaTimeListener listener);
+
+    /**
+     * Cancels all previous notification request from this listener if any.  It
+     * registers the listener to get seek and stop notifications.  If the media
+     * is stopped, the listener will immediately receive a stop notification.
+     * Otherwise, it will receive a timed event notificaton.
+     */
+    void scheduleUpdate(OnMediaTimeListener listener);
+
+    /**
+     * Cancels all previous notification request from this listener if any.
+     */
+    void cancelNotifications(OnMediaTimeListener listener);
+
+    /**
+     * Get the current presentation time.
+     *
+     * @param precise   Whether getting a precise time is important. This is
+     *                  more costly.
+     * @param monotonic Whether returned time should be monotonic: that is,
+     *                  greater than or equal to the last returned time.  Don't
+     *                  always set this to true.  E.g. this has undesired
+     *                  consequences if the media is seeked between calls.
+     * @throws IllegalStateException if the media is not initialized
+     */
+    long getCurrentTimeUs(boolean precise, boolean monotonic)
+            throws IllegalStateException;
+
+    /**
+     * Mediatime listener
+     */
+    public interface OnMediaTimeListener {
+        /**
+         * Called when the registered time was reached naturally.
+         *
+         * @param timeUs current media time
+         */
+        void onTimedEvent(long timeUs);
+
+        /**
+         * Called when the media time changed due to seeking.
+         *
+         * @param timeUs current media time
+         */
+        void onSeek(long timeUs);
+
+        /**
+         * Called when the playback stopped.  This is not called on pause, only
+         * on full stop, at which point there is no further current media time.
+         */
+        void onStop();
+    }
+}
+
diff --git a/media/src/main/java/androidx/media/subtitle/SubtitleController.java b/media/src/main/java/androidx/media/subtitle/SubtitleController.java
new file mode 100644
index 0000000..b6dfc2b
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/SubtitleController.java
@@ -0,0 +1,534 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.subtitle;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.media.MediaFormat;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.TrackInfo;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.view.accessibility.CaptioningManager;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.media.subtitle.SubtitleTrack.RenderingWidget;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+// Note: This is forked from android.media.SubtitleController since P
+/**
+ * The subtitle controller provides the architecture to display subtitles for a
+ * media source.  It allows specifying which tracks to display, on which anchor
+ * to display them, and also allows adding external, out-of-band subtitle tracks.
+ *
+ * @hide
+ */
+@RequiresApi(28)
+@RestrictTo(LIBRARY_GROUP)
+public class SubtitleController {
+    private MediaTimeProvider mTimeProvider;
+    private ArrayList<Renderer> mRenderers;
+    private ArrayList<SubtitleTrack> mTracks;
+    private final Object mRenderersLock = new Object();
+    private final Object mTracksLock = new Object();
+    private SubtitleTrack mSelectedTrack;
+    private boolean mShowing;
+    private CaptioningManager mCaptioningManager;
+    private Handler mHandler;
+
+    private static final int WHAT_SHOW = 1;
+    private static final int WHAT_HIDE = 2;
+    private static final int WHAT_SELECT_TRACK = 3;
+    private static final int WHAT_SELECT_DEFAULT_TRACK = 4;
+
+    private final Handler.Callback mCallback = new Handler.Callback() {
+        @Override
+        public boolean handleMessage(Message msg) {
+            switch (msg.what) {
+                case WHAT_SHOW:
+                    doShow();
+                    return true;
+                case WHAT_HIDE:
+                    doHide();
+                    return true;
+                case WHAT_SELECT_TRACK:
+                    doSelectTrack((SubtitleTrack) msg.obj);
+                    return true;
+                case WHAT_SELECT_DEFAULT_TRACK:
+                    doSelectDefaultTrack();
+                    return true;
+                default:
+                    return false;
+            }
+        }
+    };
+
+    private CaptioningManager.CaptioningChangeListener mCaptioningChangeListener =
+            new CaptioningManager.CaptioningChangeListener() {
+                @Override
+                public void onEnabledChanged(boolean enabled) {
+                    selectDefaultTrack();
+                }
+
+                @Override
+                public void onLocaleChanged(Locale locale) {
+                    selectDefaultTrack();
+                }
+            };
+
+    public SubtitleController(Context context) {
+        this(context, null, null);
+    }
+
+    /**
+     * Creates a subtitle controller for a media playback object that implements
+     * the MediaTimeProvider interface.
+     *
+     * @param timeProvider
+     */
+    public SubtitleController(
+            Context context,
+            MediaTimeProvider timeProvider,
+            Listener listener) {
+        mTimeProvider = timeProvider;
+        mListener = listener;
+
+        mRenderers = new ArrayList<Renderer>();
+        mShowing = false;
+        mTracks = new ArrayList<SubtitleTrack>();
+        mCaptioningManager =
+            (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        mCaptioningManager.removeCaptioningChangeListener(
+                mCaptioningChangeListener);
+        super.finalize();
+    }
+
+    /**
+     * @return the available subtitle tracks for this media. These include
+     * the tracks found by {@link MediaPlayer} as well as any tracks added
+     * manually via {@link #addTrack}.
+     */
+    public SubtitleTrack[] getTracks() {
+        synchronized (mTracksLock) {
+            SubtitleTrack[] tracks = new SubtitleTrack[mTracks.size()];
+            mTracks.toArray(tracks);
+            return tracks;
+        }
+    }
+
+    /**
+     * @return the currently selected subtitle track
+     */
+    public SubtitleTrack getSelectedTrack() {
+        return mSelectedTrack;
+    }
+
+    private RenderingWidget getRenderingWidget() {
+        if (mSelectedTrack == null) {
+            return null;
+        }
+        return mSelectedTrack.getRenderingWidget();
+    }
+
+    /**
+     * Selects a subtitle track.  As a result, this track will receive
+     * in-band data from the {@link MediaPlayer}.  However, this does
+     * not change the subtitle visibility.
+     *
+     * Should be called from the anchor's (UI) thread. {@see #Anchor.getSubtitleLooper}
+     *
+     * @param track The subtitle track to select.  This must be one of the
+     *              tracks in {@link #getTracks}.
+     * @return true if the track was successfully selected.
+     */
+    public boolean selectTrack(SubtitleTrack track) {
+        if (track != null && !mTracks.contains(track)) {
+            return false;
+        }
+
+        processOnAnchor(mHandler.obtainMessage(WHAT_SELECT_TRACK, track));
+        return true;
+    }
+
+    private void doSelectTrack(SubtitleTrack track) {
+        mTrackIsExplicit = true;
+        if (mSelectedTrack == track) {
+            return;
+        }
+
+        if (mSelectedTrack != null) {
+            mSelectedTrack.hide();
+            mSelectedTrack.setTimeProvider(null);
+        }
+
+        mSelectedTrack = track;
+        if (mAnchor != null) {
+            mAnchor.setSubtitleWidget(getRenderingWidget());
+        }
+
+        if (mSelectedTrack != null) {
+            mSelectedTrack.setTimeProvider(mTimeProvider);
+            mSelectedTrack.show();
+        }
+
+        if (mListener != null) {
+            mListener.onSubtitleTrackSelected(track);
+        }
+    }
+
+    /**
+     * @return the default subtitle track based on system preferences, or null,
+     * if no such track exists in this manager.
+     *
+     * Supports HLS-flags: AUTOSELECT, FORCED & DEFAULT.
+     *
+     * 1. If captioning is disabled, only consider FORCED tracks. Otherwise,
+     * consider all tracks, but prefer non-FORCED ones.
+     * 2. If user selected "Default" caption language:
+     *   a. If there is a considered track with DEFAULT=yes, returns that track
+     *      (favor the first one in the current language if there are more than
+     *      one default tracks, or the first in general if none of them are in
+     *      the current language).
+     *   b. Otherwise, if there is a track with AUTOSELECT=yes in the current
+     *      language, return that one.
+     *   c. If there are no default tracks, and no autoselectable tracks in the
+     *      current language, return null.
+     * 3. If there is a track with the caption language, select that one.  Prefer
+     * the one with AUTOSELECT=no.
+     *
+     * The default values for these flags are DEFAULT=no, AUTOSELECT=yes
+     * and FORCED=no.
+     */
+    public SubtitleTrack getDefaultTrack() {
+        SubtitleTrack bestTrack = null;
+        int bestScore = -1;
+
+        Locale selectedLocale = mCaptioningManager.getLocale();
+        Locale locale = selectedLocale;
+        if (locale == null) {
+            locale = Locale.getDefault();
+        }
+        boolean selectForced = !mCaptioningManager.isEnabled();
+
+        synchronized (mTracksLock) {
+            for (SubtitleTrack track: mTracks) {
+                MediaFormat format = track.getFormat();
+                String language = format.getString(MediaFormat.KEY_LANGUAGE);
+                boolean forced = MediaFormatUtil
+                        .getInteger(format, MediaFormat.KEY_IS_FORCED_SUBTITLE, 0) != 0;
+                boolean autoselect = MediaFormatUtil
+                        .getInteger(format, MediaFormat.KEY_IS_AUTOSELECT, 1) != 0;
+                boolean is_default = MediaFormatUtil
+                        .getInteger(format, MediaFormat.KEY_IS_DEFAULT, 0) != 0;
+
+                boolean languageMatches = locale == null
+                        || locale.getLanguage().equals("")
+                        || locale.getISO3Language().equals(language)
+                        || locale.getLanguage().equals(language);
+                // is_default is meaningless unless caption language is 'default'
+                int score = (forced ? 0 : 8)
+                        + (((selectedLocale == null) && is_default) ? 4 : 0)
+                        + (autoselect ? 0 : 2) + (languageMatches ? 1 : 0);
+
+                if (selectForced && !forced) {
+                    continue;
+                }
+
+                // we treat null locale/language as matching any language
+                if ((selectedLocale == null && is_default)
+                        || (languageMatches && (autoselect || forced || selectedLocale != null))) {
+                    if (score > bestScore) {
+                        bestScore = score;
+                        bestTrack = track;
+                    }
+                }
+            }
+        }
+        return bestTrack;
+    }
+
+    static class MediaFormatUtil {
+        MediaFormatUtil() { }
+        static int getInteger(MediaFormat format, String name, int defaultValue) {
+            try {
+                return format.getInteger(name);
+            } catch (NullPointerException | ClassCastException e) {
+                /* no such field or field of different type */
+            }
+            return defaultValue;
+        }
+    }
+
+    private boolean mTrackIsExplicit = false;
+    private boolean mVisibilityIsExplicit = false;
+
+    /** should be called from anchor thread */
+    public void selectDefaultTrack() {
+        processOnAnchor(mHandler.obtainMessage(WHAT_SELECT_DEFAULT_TRACK));
+    }
+
+    private void doSelectDefaultTrack() {
+        if (mTrackIsExplicit) {
+            if (mVisibilityIsExplicit) {
+                return;
+            }
+            // If track selection is explicit, but visibility
+            // is not, it falls back to the captioning setting
+            if (mCaptioningManager.isEnabled()
+                    || (mSelectedTrack != null && MediaFormatUtil.getInteger(
+                            mSelectedTrack.getFormat(),
+                            MediaFormat.KEY_IS_FORCED_SUBTITLE, 0) != 0)) {
+                show();
+            } else if (mSelectedTrack != null
+                    && mSelectedTrack.getTrackType() == TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
+                hide();
+            }
+            mVisibilityIsExplicit = false;
+        }
+
+        // We can have a default (forced) track even if captioning
+        // is not enabled.  This is handled by getDefaultTrack().
+        // Show this track unless subtitles were explicitly hidden.
+        SubtitleTrack track = getDefaultTrack();
+        if (track != null) {
+            selectTrack(track);
+            mTrackIsExplicit = false;
+            if (!mVisibilityIsExplicit) {
+                show();
+                mVisibilityIsExplicit = false;
+            }
+        }
+    }
+
+    /** must be called from anchor thread */
+    public void reset() {
+        checkAnchorLooper();
+        hide();
+        selectTrack(null);
+        mTracks.clear();
+        mTrackIsExplicit = false;
+        mVisibilityIsExplicit = false;
+        mCaptioningManager.removeCaptioningChangeListener(
+                mCaptioningChangeListener);
+    }
+
+    /**
+     * Adds a new, external subtitle track to the manager.
+     *
+     * @param format the format of the track that will include at least
+     *               the MIME type {@link MediaFormat@KEY_MIME}.
+     * @return the created {@link SubtitleTrack} object
+     */
+    public SubtitleTrack addTrack(MediaFormat format) {
+        synchronized (mRenderersLock) {
+            for (Renderer renderer: mRenderers) {
+                if (renderer.supports(format)) {
+                    SubtitleTrack track = renderer.createTrack(format);
+                    if (track != null) {
+                        synchronized (mTracksLock) {
+                            if (mTracks.size() == 0) {
+                                mCaptioningManager.addCaptioningChangeListener(
+                                        mCaptioningChangeListener);
+                            }
+                            mTracks.add(track);
+                        }
+                        return track;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Show the selected (or default) subtitle track.
+     *
+     * Should be called from the anchor's (UI) thread. {@see #Anchor.getSubtitleLooper}
+     */
+    public void show() {
+        processOnAnchor(mHandler.obtainMessage(WHAT_SHOW));
+    }
+
+    private void doShow() {
+        mShowing = true;
+        mVisibilityIsExplicit = true;
+        if (mSelectedTrack != null) {
+            mSelectedTrack.show();
+        }
+    }
+
+    /**
+     * Hide the selected (or default) subtitle track.
+     *
+     * Should be called from the anchor's (UI) thread. {@see #Anchor.getSubtitleLooper}
+     */
+    public void hide() {
+        processOnAnchor(mHandler.obtainMessage(WHAT_HIDE));
+    }
+
+    private void doHide() {
+        mVisibilityIsExplicit = true;
+        if (mSelectedTrack != null) {
+            mSelectedTrack.hide();
+        }
+        mShowing = false;
+    }
+
+    /**
+     * Interface for supporting a single or multiple subtitle types in {@link MediaPlayer}.
+     */
+    public abstract static class Renderer {
+        /**
+         * Called by {@link MediaPlayer}'s {@link SubtitleController} when a new
+         * subtitle track is detected, to see if it should use this object to
+         * parse and display this subtitle track.
+         *
+         * @param format the format of the track that will include at least
+         *               the MIME type {@link MediaFormat@KEY_MIME}.
+         *
+         * @return true if and only if the track format is supported by this
+         * renderer
+         */
+        public abstract boolean supports(MediaFormat format);
+
+        /**
+         * Called by {@link MediaPlayer}'s {@link SubtitleController} for each
+         * subtitle track that was detected and is supported by this object to
+         * create a {@link SubtitleTrack} object.  This object will be created
+         * for each track that was found.  If the track is selected for display,
+         * this object will be used to parse and display the track data.
+         *
+         * @param format the format of the track that will include at least
+         *               the MIME type {@link MediaFormat@KEY_MIME}.
+         * @return a {@link SubtitleTrack} object that will be used to parse
+         * and render the subtitle track.
+         */
+        public abstract SubtitleTrack createTrack(MediaFormat format);
+    }
+
+    /**
+     * Add support for a subtitle format in {@link MediaPlayer}.
+     *
+     * @param renderer a {@link SubtitleController.Renderer} object that adds
+     *                 support for a subtitle format.
+     */
+    public void registerRenderer(Renderer renderer) {
+        synchronized (mRenderersLock) {
+            // TODO how to get available renderers in the system
+            if (!mRenderers.contains(renderer)) {
+                // TODO should added renderers override existing ones (to allow replacing?)
+                mRenderers.add(renderer);
+            }
+        }
+    }
+
+    /**
+     * Returns true if one of the registered renders supports given media format.
+     *
+     * @param format a {@link MediaFormat} object
+     * @return true if this SubtitleController has a renderer that supports
+     * the media format.
+     */
+    public boolean hasRendererFor(MediaFormat format) {
+        synchronized (mRenderersLock) {
+            // TODO how to get available renderers in the system
+            for (Renderer renderer: mRenderers) {
+                if (renderer.supports(format)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Subtitle anchor, an object that is able to display a subtitle renderer,
+     * e.g. a VideoView.
+     */
+    public interface Anchor {
+        /**
+         * Anchor should use the supplied subtitle rendering widget, or
+         * none if it is null.
+         */
+        void setSubtitleWidget(RenderingWidget subtitleWidget);
+
+        /**
+         * Anchors provide the looper on which all track visibility changes
+         * (track.show/hide, setSubtitleWidget) will take place.
+         */
+        Looper getSubtitleLooper();
+    }
+
+    private Anchor mAnchor;
+
+    /**
+     *  called from anchor's looper (if any, both when unsetting and
+     *  setting)
+     */
+    public void setAnchor(Anchor anchor) {
+        if (mAnchor == anchor) {
+            return;
+        }
+
+        if (mAnchor != null) {
+            checkAnchorLooper();
+            mAnchor.setSubtitleWidget(null);
+        }
+        mAnchor = anchor;
+        mHandler = null;
+        if (mAnchor != null) {
+            mHandler = new Handler(mAnchor.getSubtitleLooper(), mCallback);
+            checkAnchorLooper();
+            mAnchor.setSubtitleWidget(getRenderingWidget());
+        }
+    }
+
+    private void checkAnchorLooper() {
+        assert mHandler != null : "Should have a looper already";
+        assert Looper.myLooper() == mHandler.getLooper()
+                : "Must be called from the anchor's looper";
+    }
+
+    private void processOnAnchor(Message m) {
+        assert mHandler != null : "Should have a looper already";
+        if (Looper.myLooper() == mHandler.getLooper()) {
+            mHandler.dispatchMessage(m);
+        } else {
+            mHandler.sendMessage(m);
+        }
+    }
+
+    interface Listener {
+        /**
+         * Called when a subtitle track has been selected.
+         *
+         * @param track selected subtitle track or null
+         */
+        void onSubtitleTrackSelected(SubtitleTrack track);
+    }
+
+    private Listener mListener;
+}
diff --git a/media/src/main/java/androidx/media/subtitle/SubtitleTrack.java b/media/src/main/java/androidx/media/subtitle/SubtitleTrack.java
new file mode 100644
index 0000000..30c1316
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/SubtitleTrack.java
@@ -0,0 +1,715 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.subtitle;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.graphics.Canvas;
+import android.media.MediaFormat;
+import android.media.MediaPlayer.TrackInfo;
+import android.media.SubtitleData;
+import android.os.Handler;
+import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.Pair;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+// Note: This is forked from android.media.SubtitleTrack since P
+/**
+ * A subtitle track abstract base class that is responsible for parsing and displaying
+ * an instance of a particular type of subtitle.
+ *
+ * @hide
+ */
+@RequiresApi(28)
+@RestrictTo(LIBRARY_GROUP)
+public abstract class SubtitleTrack implements MediaTimeProvider.OnMediaTimeListener {
+    private static final String TAG = "SubtitleTrack";
+    private long mLastUpdateTimeMs;
+    private long mLastTimeMs;
+
+    private Runnable mRunnable;
+
+    private final LongSparseArray<Run> mRunsByEndTime = new LongSparseArray<Run>();
+    private final LongSparseArray<Run> mRunsByID = new LongSparseArray<Run>();
+
+    private CueList mCues;
+    private final ArrayList<Cue> mActiveCues = new ArrayList<Cue>();
+    protected boolean mVisible;
+
+    public boolean DEBUG = false;
+
+    protected Handler mHandler = new Handler();
+
+    private MediaFormat mFormat;
+
+    public SubtitleTrack(MediaFormat format) {
+        mFormat = format;
+        mCues = new CueList();
+        clearActiveCues();
+        mLastTimeMs = -1;
+    }
+
+    public final MediaFormat getFormat() {
+        return mFormat;
+    }
+
+    private long mNextScheduledTimeMs = -1;
+
+    /**
+     * Called when there is input data for the subtitle track.
+     */
+    public void onData(SubtitleData data) {
+        long runID = data.getStartTimeUs() + 1;
+        onData(data.getData(), true /* eos */, runID);
+        setRunDiscardTimeMs(
+                runID,
+                (data.getStartTimeUs() + data.getDurationUs()) / 1000);
+    }
+
+    /**
+     * Called when there is input data for the subtitle track.  The
+     * complete subtitle for a track can include multiple whole units
+     * (runs).  Each of these units can have multiple sections.  The
+     * contents of a run are submitted in sequential order, with eos
+     * indicating the last section of the run.  Calls from different
+     * runs must not be intermixed.
+     *
+     * @param data subtitle data byte buffer
+     * @param eos true if this is the last section of the run.
+     * @param runID mostly-unique ID for this run of data.  Subtitle cues
+     *              with runID of 0 are discarded immediately after
+     *              display.  Cues with runID of ~0 are discarded
+     *              only at the deletion of the track object.  Cues
+     *              with other runID-s are discarded at the end of the
+     *              run, which defaults to the latest timestamp of
+     *              any of its cues (with this runID).
+     */
+    protected abstract void onData(byte[] data, boolean eos, long runID);
+
+    /**
+     * Called when adding the subtitle rendering widget to the view hierarchy,
+     * as well as when showing or hiding the subtitle track, or when the video
+     * surface position has changed.
+     *
+     * @return the widget that renders this subtitle track. For most renderers
+     *         there should be a single shared instance that is used for all
+     *         tracks supported by that renderer, as at most one subtitle track
+     *         is visible at one time.
+     */
+    public abstract RenderingWidget getRenderingWidget();
+
+    /**
+     * Called when the active cues have changed, and the contents of the subtitle
+     * view should be updated.
+     */
+    public abstract void updateView(ArrayList<Cue> activeCues);
+
+    protected synchronized void updateActiveCues(boolean rebuild, long timeMs) {
+        // out-of-order times mean seeking or new active cues being added
+        // (during their own timespan)
+        if (rebuild || mLastUpdateTimeMs > timeMs) {
+            clearActiveCues();
+        }
+
+        for (Iterator<Pair<Long, Cue>> it =
+                mCues.entriesBetween(mLastUpdateTimeMs, timeMs).iterator(); it.hasNext(); ) {
+            Pair<Long, Cue> event = it.next();
+            Cue cue = event.second;
+
+            if (cue.mEndTimeMs == event.first) {
+                // remove past cues
+                if (DEBUG) Log.v(TAG, "Removing " + cue);
+                mActiveCues.remove(cue);
+                if (cue.mRunID == 0) {
+                    it.remove();
+                }
+            } else if (cue.mStartTimeMs == event.first) {
+                // add new cues
+                // TRICKY: this will happen in start order
+                if (DEBUG) Log.v(TAG, "Adding " + cue);
+                if (cue.mInnerTimesMs != null) {
+                    cue.onTime(timeMs);
+                }
+                mActiveCues.add(cue);
+            } else if (cue.mInnerTimesMs != null) {
+                // cue is modified
+                cue.onTime(timeMs);
+            }
+        }
+
+        /* complete any runs */
+        while (mRunsByEndTime.size() > 0 && mRunsByEndTime.keyAt(0) <= timeMs) {
+            removeRunsByEndTimeIndex(0); // removes element
+        }
+        mLastUpdateTimeMs = timeMs;
+    }
+
+    private void removeRunsByEndTimeIndex(int ix) {
+        Run run = mRunsByEndTime.valueAt(ix);
+        while (run != null) {
+            Cue cue = run.mFirstCue;
+            while (cue != null) {
+                mCues.remove(cue);
+                Cue nextCue = cue.mNextInRun;
+                cue.mNextInRun = null;
+                cue = nextCue;
+            }
+            mRunsByID.remove(run.mRunID);
+            Run nextRun = run.mNextRunAtEndTimeMs;
+            run.mPrevRunAtEndTimeMs = null;
+            run.mNextRunAtEndTimeMs = null;
+            run = nextRun;
+        }
+        mRunsByEndTime.removeAt(ix);
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        /* remove all cues (untangle all cross-links) */
+        int size = mRunsByEndTime.size();
+        for (int ix = size - 1; ix >= 0; ix--) {
+            removeRunsByEndTimeIndex(ix);
+        }
+
+        super.finalize();
+    }
+
+    private synchronized void takeTime(long timeMs) {
+        mLastTimeMs = timeMs;
+    }
+
+    protected synchronized void clearActiveCues() {
+        if (DEBUG) Log.v(TAG, "Clearing " + mActiveCues.size() + " active cues");
+        mActiveCues.clear();
+        mLastUpdateTimeMs = -1;
+    }
+
+    protected void scheduleTimedEvents() {
+        /* get times for the next event */
+        if (mTimeProvider != null) {
+            mNextScheduledTimeMs = mCues.nextTimeAfter(mLastTimeMs);
+            if (DEBUG) Log.d(TAG, "sched @" + mNextScheduledTimeMs + " after " + mLastTimeMs);
+            mTimeProvider.notifyAt(mNextScheduledTimeMs >= 0
+                    ? (mNextScheduledTimeMs * 1000) : MediaTimeProvider.NO_TIME, this);
+        }
+    }
+
+    @Override
+    public void onTimedEvent(long timeUs) {
+        if (DEBUG) Log.d(TAG, "onTimedEvent " + timeUs);
+        synchronized (this) {
+            long timeMs = timeUs / 1000;
+            updateActiveCues(false, timeMs);
+            takeTime(timeMs);
+        }
+        updateView(mActiveCues);
+        scheduleTimedEvents();
+    }
+
+    @Override
+    public void onSeek(long timeUs) {
+        if (DEBUG) Log.d(TAG, "onSeek " + timeUs);
+        synchronized (this) {
+            long timeMs = timeUs / 1000;
+            updateActiveCues(true, timeMs);
+            takeTime(timeMs);
+        }
+        updateView(mActiveCues);
+        scheduleTimedEvents();
+    }
+
+    @Override
+    public void onStop() {
+        synchronized (this) {
+            if (DEBUG) Log.d(TAG, "onStop");
+            clearActiveCues();
+            mLastTimeMs = -1;
+        }
+        updateView(mActiveCues);
+        mNextScheduledTimeMs = -1;
+        mTimeProvider.notifyAt(MediaTimeProvider.NO_TIME, this);
+    }
+
+    protected MediaTimeProvider mTimeProvider;
+
+    /**
+     * Shows subtitle rendering widget
+     */
+    public void show() {
+        if (mVisible) {
+            return;
+        }
+
+        mVisible = true;
+        RenderingWidget renderingWidget = getRenderingWidget();
+        if (renderingWidget != null) {
+            renderingWidget.setVisible(true);
+        }
+        if (mTimeProvider != null) {
+            mTimeProvider.scheduleUpdate(this);
+        }
+    }
+
+    /**
+     * Hides subtitle rendering widget
+     */
+    public void hide() {
+        if (!mVisible) {
+            return;
+        }
+
+        if (mTimeProvider != null) {
+            mTimeProvider.cancelNotifications(this);
+        }
+        RenderingWidget renderingWidget = getRenderingWidget();
+        if (renderingWidget != null) {
+            renderingWidget.setVisible(false);
+        }
+        mVisible = false;
+    }
+
+    protected synchronized boolean addCue(Cue cue) {
+        mCues.add(cue);
+
+        if (cue.mRunID != 0) {
+            Run run = mRunsByID.get(cue.mRunID);
+            if (run == null) {
+                run = new Run();
+                mRunsByID.put(cue.mRunID, run);
+                run.mEndTimeMs = cue.mEndTimeMs;
+            } else if (run.mEndTimeMs < cue.mEndTimeMs) {
+                run.mEndTimeMs = cue.mEndTimeMs;
+            }
+
+            // link-up cues in the same run
+            cue.mNextInRun = run.mFirstCue;
+            run.mFirstCue = cue;
+        }
+
+        // if a cue is added that should be visible, need to refresh view
+        long nowMs = -1;
+        if (mTimeProvider != null) {
+            try {
+                nowMs = mTimeProvider.getCurrentTimeUs(
+                        false /* precise */, true /* monotonic */) / 1000;
+            } catch (IllegalStateException e) {
+                // handle as it we are not playing
+            }
+        }
+
+        if (DEBUG) {
+            Log.v(TAG, "mVisible=" + mVisible + ", "
+                    + cue.mStartTimeMs + " <= " + nowMs + ", "
+                    + cue.mEndTimeMs + " >= " + mLastTimeMs);
+        }
+
+        if (mVisible && cue.mStartTimeMs <= nowMs
+                // we don't trust nowMs, so check any cue since last callback
+                && cue.mEndTimeMs >= mLastTimeMs) {
+            if (mRunnable != null) {
+                mHandler.removeCallbacks(mRunnable);
+            }
+            final SubtitleTrack track = this;
+            final long thenMs = nowMs;
+            mRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    // even with synchronized, it is possible that we are going
+                    // to do multiple updates as the runnable could be already
+                    // running.
+                    synchronized (track) {
+                        mRunnable = null;
+                        updateActiveCues(true, thenMs);
+                        updateView(mActiveCues);
+                    }
+                }
+            };
+            // delay update so we don't update view on every cue.  TODO why 10?
+            if (mHandler.postDelayed(mRunnable, 10 /* delay */)) {
+                if (DEBUG) Log.v(TAG, "scheduling update");
+            } else {
+                if (DEBUG) Log.w(TAG, "failed to schedule subtitle view update");
+            }
+            return true;
+        }
+
+        if (mVisible && cue.mEndTimeMs >= mLastTimeMs
+                && (cue.mStartTimeMs < mNextScheduledTimeMs || mNextScheduledTimeMs < 0)) {
+            scheduleTimedEvents();
+        }
+
+        return false;
+    }
+
+    /**
+     * Sets MediaTimeProvider
+     */
+    public synchronized void setTimeProvider(MediaTimeProvider timeProvider) {
+        if (mTimeProvider == timeProvider) {
+            return;
+        }
+        if (mTimeProvider != null) {
+            mTimeProvider.cancelNotifications(this);
+        }
+        mTimeProvider = timeProvider;
+        if (mTimeProvider != null) {
+            mTimeProvider.scheduleUpdate(this);
+        }
+    }
+
+
+    static class CueList {
+        private static final String TAG = "CueList";
+        // simplistic, inefficient implementation
+        private SortedMap<Long, ArrayList<Cue>> mCues;
+        public boolean DEBUG = false;
+
+        private boolean addEvent(Cue cue, long timeMs) {
+            ArrayList<Cue> cues = mCues.get(timeMs);
+            if (cues == null) {
+                cues = new ArrayList<Cue>(2);
+                mCues.put(timeMs, cues);
+            } else if (cues.contains(cue)) {
+                // do not duplicate cues
+                return false;
+            }
+
+            cues.add(cue);
+            return true;
+        }
+
+        private void removeEvent(Cue cue, long timeMs) {
+            ArrayList<Cue> cues = mCues.get(timeMs);
+            if (cues != null) {
+                cues.remove(cue);
+                if (cues.size() == 0) {
+                    mCues.remove(timeMs);
+                }
+            }
+        }
+
+        public void add(Cue cue) {
+            // ignore non-positive-duration cues
+            if (cue.mStartTimeMs >= cue.mEndTimeMs) return;
+
+            if (!addEvent(cue, cue.mStartTimeMs)) {
+                return;
+            }
+
+            long lastTimeMs = cue.mStartTimeMs;
+            if (cue.mInnerTimesMs != null) {
+                for (long timeMs: cue.mInnerTimesMs) {
+                    if (timeMs > lastTimeMs && timeMs < cue.mEndTimeMs) {
+                        addEvent(cue, timeMs);
+                        lastTimeMs = timeMs;
+                    }
+                }
+            }
+
+            addEvent(cue, cue.mEndTimeMs);
+        }
+
+        public void remove(Cue cue) {
+            removeEvent(cue, cue.mStartTimeMs);
+            if (cue.mInnerTimesMs != null) {
+                for (long timeMs: cue.mInnerTimesMs) {
+                    removeEvent(cue, timeMs);
+                }
+            }
+            removeEvent(cue, cue.mEndTimeMs);
+        }
+
+        public Iterable<Pair<Long, Cue>> entriesBetween(
+                final long lastTimeMs, final long timeMs) {
+            return new Iterable<Pair<Long, Cue>>() {
+                @Override
+                public Iterator<Pair<Long, Cue>> iterator() {
+                    if (DEBUG) Log.d(TAG, "slice (" + lastTimeMs + ", " + timeMs + "]=");
+                    try {
+                        return new EntryIterator(
+                                mCues.subMap(lastTimeMs + 1, timeMs + 1));
+                    } catch (IllegalArgumentException e) {
+                        return new EntryIterator(null);
+                    }
+                }
+            };
+        }
+
+        public long nextTimeAfter(long timeMs) {
+            SortedMap<Long, ArrayList<Cue>> tail = null;
+            try {
+                tail = mCues.tailMap(timeMs + 1);
+                if (tail != null) {
+                    return tail.firstKey();
+                } else {
+                    return -1;
+                }
+            } catch (IllegalArgumentException e) {
+                return -1;
+            } catch (NoSuchElementException e) {
+                return -1;
+            }
+        }
+
+        class EntryIterator implements Iterator<Pair<Long, Cue>> {
+            @Override
+            public boolean hasNext() {
+                return !mDone;
+            }
+
+            @Override
+            public Pair<Long, Cue> next() {
+                if (mDone) {
+                    throw new NoSuchElementException("");
+                }
+                mLastEntry = new Pair<Long, Cue>(
+                        mCurrentTimeMs, mListIterator.next());
+                mLastListIterator = mListIterator;
+                if (!mListIterator.hasNext()) {
+                    nextKey();
+                }
+                return mLastEntry;
+            }
+
+            @Override
+            public void remove() {
+                // only allow removing end tags
+                if (mLastListIterator == null
+                        || mLastEntry.second.mEndTimeMs != mLastEntry.first) {
+                    throw new IllegalStateException("");
+                }
+
+                // remove end-cue
+                mLastListIterator.remove();
+                mLastListIterator = null;
+                if (mCues.get(mLastEntry.first).size() == 0) {
+                    mCues.remove(mLastEntry.first);
+                }
+
+                // remove rest of the cues
+                Cue cue = mLastEntry.second;
+                removeEvent(cue, cue.mStartTimeMs);
+                if (cue.mInnerTimesMs != null) {
+                    for (long timeMs: cue.mInnerTimesMs) {
+                        removeEvent(cue, timeMs);
+                    }
+                }
+            }
+
+            EntryIterator(SortedMap<Long, ArrayList<Cue>> cues) {
+                if (DEBUG) Log.v(TAG, cues + "");
+                mRemainingCues = cues;
+                mLastListIterator = null;
+                nextKey();
+            }
+
+            private void nextKey() {
+                do {
+                    try {
+                        if (mRemainingCues == null) {
+                            throw new NoSuchElementException("");
+                        }
+                        mCurrentTimeMs = mRemainingCues.firstKey();
+                        mListIterator =
+                            mRemainingCues.get(mCurrentTimeMs).iterator();
+                        try {
+                            mRemainingCues =
+                                mRemainingCues.tailMap(mCurrentTimeMs + 1);
+                        } catch (IllegalArgumentException e) {
+                            mRemainingCues = null;
+                        }
+                        mDone = false;
+                    } catch (NoSuchElementException e) {
+                        mDone = true;
+                        mRemainingCues = null;
+                        mListIterator = null;
+                        return;
+                    }
+                } while (!mListIterator.hasNext());
+            }
+
+            private long mCurrentTimeMs;
+            private Iterator<Cue> mListIterator;
+            private boolean mDone;
+            private SortedMap<Long, ArrayList<Cue>> mRemainingCues;
+            private Iterator<Cue> mLastListIterator;
+            private Pair<Long, Cue> mLastEntry;
+        }
+
+        CueList() {
+            mCues = new TreeMap<Long, ArrayList<Cue>>();
+        }
+    }
+
+    static class Cue {
+        public long mStartTimeMs;
+        public long mEndTimeMs;
+        public long[] mInnerTimesMs;
+        public long mRunID;
+
+        public Cue mNextInRun;
+
+        /**
+         * Called to inform current timeMs to the cue
+         */
+        public void onTime(long timeMs) { }
+    }
+
+    /** update mRunsByEndTime (with default end time) */
+    protected void finishedRun(long runID) {
+        if (runID != 0 && runID != ~0) {
+            Run run = mRunsByID.get(runID);
+            if (run != null) {
+                run.storeByEndTimeMs(mRunsByEndTime);
+            }
+        }
+    }
+
+    /** update mRunsByEndTime with given end time */
+    public void setRunDiscardTimeMs(long runID, long timeMs) {
+        if (runID != 0 && runID != ~0) {
+            Run run = mRunsByID.get(runID);
+            if (run != null) {
+                run.mEndTimeMs = timeMs;
+                run.storeByEndTimeMs(mRunsByEndTime);
+            }
+        }
+    }
+
+    /** whether this is a text track who fires events instead getting rendered */
+    public int getTrackType() {
+        return getRenderingWidget() == null
+                ? TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT
+                : TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE;
+    }
+
+
+    private static class Run {
+        public Cue mFirstCue;
+        public Run mNextRunAtEndTimeMs;
+        public Run mPrevRunAtEndTimeMs;
+        public long mEndTimeMs = -1;
+        public long mRunID = 0;
+        private long mStoredEndTimeMs = -1;
+
+        public void storeByEndTimeMs(LongSparseArray<Run> runsByEndTime) {
+            // remove old value if any
+            int ix = runsByEndTime.indexOfKey(mStoredEndTimeMs);
+            if (ix >= 0) {
+                if (mPrevRunAtEndTimeMs == null) {
+                    assert (this == runsByEndTime.valueAt(ix));
+                    if (mNextRunAtEndTimeMs == null) {
+                        runsByEndTime.removeAt(ix);
+                    } else {
+                        runsByEndTime.setValueAt(ix, mNextRunAtEndTimeMs);
+                    }
+                }
+                removeAtEndTimeMs();
+            }
+
+            // add new value
+            if (mEndTimeMs >= 0) {
+                mPrevRunAtEndTimeMs = null;
+                mNextRunAtEndTimeMs = runsByEndTime.get(mEndTimeMs);
+                if (mNextRunAtEndTimeMs != null) {
+                    mNextRunAtEndTimeMs.mPrevRunAtEndTimeMs = this;
+                }
+                runsByEndTime.put(mEndTimeMs, this);
+                mStoredEndTimeMs = mEndTimeMs;
+            }
+        }
+
+        public void removeAtEndTimeMs() {
+            Run prev = mPrevRunAtEndTimeMs;
+
+            if (mPrevRunAtEndTimeMs != null) {
+                mPrevRunAtEndTimeMs.mNextRunAtEndTimeMs = mNextRunAtEndTimeMs;
+                mPrevRunAtEndTimeMs = null;
+            }
+            if (mNextRunAtEndTimeMs != null) {
+                mNextRunAtEndTimeMs.mPrevRunAtEndTimeMs = prev;
+                mNextRunAtEndTimeMs = null;
+            }
+        }
+    }
+
+    /**
+     * Interface for rendering subtitles onto a Canvas.
+     */
+    public interface RenderingWidget {
+        /**
+         * Sets the widget's callback, which is used to send updates when the
+         * rendered data has changed.
+         *
+         * @param callback update callback
+         */
+        void setOnChangedListener(OnChangedListener callback);
+
+        /**
+         * Sets the widget's size.
+         *
+         * @param width width in pixels
+         * @param height height in pixels
+         */
+        void setSize(int width, int height);
+
+        /**
+         * Sets whether the widget should draw subtitles.
+         *
+         * @param visible true if subtitles should be drawn, false otherwise
+         */
+        void setVisible(boolean visible);
+
+        /**
+         * Renders subtitles onto a {@link Canvas}.
+         *
+         * @param c canvas on which to render subtitles
+         */
+        void draw(Canvas c);
+
+        /**
+         * Called when the widget is attached to a window.
+         */
+        void onAttachedToWindow();
+
+        /**
+         * Called when the widget is detached from a window.
+         */
+        void onDetachedFromWindow();
+
+        /**
+         * Callback used to send updates about changes to rendering data.
+         */
+        public interface OnChangedListener {
+            /**
+             * Called when the rendering data has changed.
+             *
+             * @param renderingWidget the widget whose data has changed
+             */
+            void onChanged(RenderingWidget renderingWidget);
+        }
+    }
+}
diff --git a/media/src/main/res/values/dimens.xml b/media/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..be50378
--- /dev/null
+++ b/media/src/main/res/values/dimens.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+    <!-- Shadow radius for video subtitles. -->
+    <dimen name="subtitle_shadow_radius">2dp</dimen>
+
+    <!-- Shadow offset for video subtitles. -->
+    <dimen name="subtitle_shadow_offset">2dp</dimen>
+
+    <!-- Outline width for video subtitles. -->
+    <dimen name="subtitle_outline_width">2dp</dimen>
+</resources>
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaController2Test_copied.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaController2Test_copied.java
new file mode 100644
index 0000000..a096160
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaController2Test_copied.java
@@ -0,0 +1,1512 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.os.ResultReceiver;
+import android.support.mediacompat.testlib.util.PollingCheck;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.NonNull;
+import androidx.media.AudioAttributesCompat;
+import androidx.media.MediaController2;
+import androidx.media.MediaController2.ControllerCallback;
+import androidx.media.MediaController2.PlaybackInfo;
+import androidx.media.MediaItem2;
+import androidx.media.MediaMetadata2;
+import androidx.media.MediaPlayerInterface;
+import androidx.media.MediaPlaylistAgent;
+import androidx.media.MediaSession2;
+import androidx.media.MediaSession2.ControllerInfo;
+import androidx.media.MediaSession2.SessionCallback;
+import androidx.media.Rating2;
+import androidx.media.SessionCommand2;
+import androidx.media.SessionCommandGroup2;
+import androidx.media.VolumeProviderCompat;
+import androidx.media.test.client.TestUtils.SyncHandler;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Tests {@link MediaController2}.
+ */
+// TODO(jaewan): Implement host-side test so controller and session can run in different processes.
+// TODO(jaewan): Fix flaky failure -- see MediaController2Impl.getController()
+// TODO(jaeawn): Revisit create/close session in the sHandler. It's no longer necessary.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@FlakyTest
+@Ignore
+public class MediaController2Test_copied extends MediaSession2TestBase {
+    private static final String TAG = "MediaController2Test_copied";
+
+    PendingIntent mIntent;
+    MediaSession2 mSession;
+    MediaController2 mController;
+    MockPlayer mPlayer;
+    MockPlaylistAgent mMockAgent;
+    AudioManager mAudioManager;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        final Intent sessionActivity = new Intent(mContext, MockActivity.class);
+        // Create this test specific MediaSession2 to use our own Handler.
+        mIntent = PendingIntent.getActivity(mContext, 0, sessionActivity, 0);
+
+        mPlayer = new MockPlayer(1);
+        mMockAgent = new MockPlaylistAgent();
+        mSession = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setPlaylistAgent(mMockAgent)
+                .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+                    @Override
+                    public SessionCommandGroup2 onConnect(MediaSession2 session,
+                            ControllerInfo controller) {
+                        if (Process.myUid() == controller.getUid()) {
+                            return super.onConnect(session, controller);
+                        }
+                        return null;
+                    }
+
+                    @Override
+                    public void onPlaylistMetadataChanged(MediaSession2 session,
+                            MediaPlaylistAgent playlistAgent,
+                            MediaMetadata2 metadata) {
+                        super.onPlaylistMetadataChanged(session, playlistAgent, metadata);
+                    }
+                })
+                .setSessionActivity(mIntent)
+                .setId(TAG).build();
+        mController = createController(mSession.getToken());
+        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        TestServiceRegistry.getInstance().setHandler(sHandler);
+    }
+
+    @After
+    @Override
+    public void cleanUp() throws Exception {
+        super.cleanUp();
+        if (mSession != null) {
+            mSession.close();
+        }
+        TestServiceRegistry.getInstance().cleanUp();
+    }
+
+    /**
+     * Test if the {@link MediaSession2TestBase.TestControllerCallback} wraps the callback proxy
+     * without missing any method.
+     */
+    @Test
+    public void testTestControllerCallback() {
+        prepareLooper();
+        Method[] methods = TestControllerCallback.class.getMethods();
+        assertNotNull(methods);
+        for (int i = 0; i < methods.length; i++) {
+            // For any methods in the controller callback, TestControllerCallback should have
+            // overriden the method and call matching API in the callback proxy.
+            assertNotEquals("TestControllerCallback should override " + methods[i]
+                            + " and call callback proxy",
+                    ControllerCallback.class, methods[i].getDeclaringClass());
+        }
+    }
+
+    @Test
+    public void testPlay() {
+        prepareLooper();
+        mController.play();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mPlayCalled);
+    }
+
+    @Test
+    public void testPause() {
+        prepareLooper();
+        mController.pause();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mPauseCalled);
+    }
+
+    @Test
+    public void testReset() {
+        prepareLooper();
+        mController.reset();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mResetCalled);
+    }
+
+    @Test
+    public void testPrepare() {
+        prepareLooper();
+        mController.prepare();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mPrepareCalled);
+    }
+
+    @Test
+    public void testSeekTo() {
+        prepareLooper();
+        final long seekPosition = 12125L;
+        mController.seekTo(seekPosition);
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mSeekToCalled);
+        assertEquals(seekPosition, mPlayer.mSeekPosition);
+    }
+
+    @Test
+    public void testGettersAfterConnected() throws InterruptedException {
+        prepareLooper();
+        final int state = MediaPlayerInterface.PLAYER_STATE_PLAYING;
+        final int bufferingState = MediaPlayerInterface.BUFFERING_STATE_BUFFERING_COMPLETE;
+        final long position = 150000;
+        final long bufferedPosition = 900000;
+        final float speed = 0.5f;
+        final long timeDiff = 102;
+        final MediaItem2 currentMediaItem = TestUtils.createMediaItemWithMetadata();
+
+        mPlayer.mLastPlayerState = state;
+        mPlayer.mLastBufferingState = bufferingState;
+        mPlayer.mCurrentPosition = position;
+        mPlayer.mBufferedPosition = bufferedPosition;
+        mPlayer.mPlaybackSpeed = speed;
+        mMockAgent.mCurrentMediaItem = currentMediaItem;
+
+        MediaController2 controller = createController(mSession.getToken());
+        // setTimeDiff is package-private, so cannot be called here.
+//        controller.setTimeDiff(timeDiff);
+        assertEquals(state, controller.getPlayerState());
+        assertEquals(bufferedPosition, controller.getBufferedPosition());
+        assertEquals(speed, controller.getPlaybackSpeed(), 0.0f);
+        assertEquals(position + (long) (speed * timeDiff), controller.getCurrentPosition());
+        assertEquals(currentMediaItem, controller.getCurrentMediaItem());
+    }
+
+    @Test
+    public void testUpdatePlayer() throws InterruptedException {
+        prepareLooper();
+        final int testState = MediaPlayerInterface.PLAYER_STATE_PLAYING;
+        final List<MediaItem2> testPlaylist = TestUtils.createPlaylist(3);
+        final AudioAttributesCompat testAudioAttributes = new AudioAttributesCompat.Builder()
+                .setLegacyStreamType(AudioManager.STREAM_RING).build();
+        final CountDownLatch latch = new CountDownLatch(3);
+        mController = createController(mSession.getToken(), true, new ControllerCallback() {
+            @Override
+            public void onPlayerStateChanged(MediaController2 controller, int state) {
+                assertEquals(mController, controller);
+                assertEquals(testState, state);
+                latch.countDown();
+            }
+
+            @Override
+            public void onPlaylistChanged(MediaController2 controller, List<MediaItem2> list,
+                    MediaMetadata2 metadata) {
+                assertEquals(mController, controller);
+                assertEquals(testPlaylist, list);
+                assertNull(metadata);
+                latch.countDown();
+            }
+
+            @Override
+            public void onPlaybackInfoChanged(MediaController2 controller, PlaybackInfo info) {
+                assertEquals(mController, controller);
+                assertEquals(testAudioAttributes, info.getAudioAttributes());
+                latch.countDown();
+            }
+        });
+
+        MockPlayer player = new MockPlayer(0);
+        player.mLastPlayerState = testState;
+        player.setAudioAttributes(testAudioAttributes);
+
+        MockPlaylistAgent agent = new MockPlaylistAgent();
+        agent.mPlaylist = testPlaylist;
+
+        mSession.updatePlayer(player, agent, null);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testGetSessionActivity() {
+        prepareLooper();
+        PendingIntent sessionActivity = mController.getSessionActivity();
+        assertEquals(mContext.getPackageName(), sessionActivity.getCreatorPackage());
+        assertEquals(Process.myUid(), sessionActivity.getCreatorUid());
+    }
+
+    @Test
+    public void testSetPlaylist() throws InterruptedException {
+        prepareLooper();
+        final List<MediaItem2> list = TestUtils.createPlaylist(2);
+        mController.setPlaylist(list, null /* Metadata */);
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        assertTrue(mMockAgent.mSetPlaylistCalled);
+        assertNull(mMockAgent.mMetadata);
+
+        assertNotNull(mMockAgent.mPlaylist);
+        assertEquals(list.size(), mMockAgent.mPlaylist.size());
+        for (int i = 0; i < list.size(); i++) {
+            // MediaController2.setPlaylist does not ensure the equality of the items.
+            assertEquals(list.get(i).getMediaId(), mMockAgent.mPlaylist.get(i).getMediaId());
+        }
+    }
+
+    /**
+     * This also tests {@link ControllerCallback#onPlaylistChanged(
+     * MediaController2, List, MediaMetadata2)}.
+     */
+    @Test
+    public void testGetPlaylist() throws InterruptedException {
+        prepareLooper();
+        final List<MediaItem2> testList = TestUtils.createPlaylist(2);
+        final AtomicReference<List<MediaItem2>> listFromCallback = new AtomicReference<>();
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onPlaylistChanged(MediaController2 controller,
+                    List<MediaItem2> playlist, MediaMetadata2 metadata) {
+                assertNotNull(playlist);
+                assertEquals(testList.size(), playlist.size());
+                for (int i = 0; i < playlist.size(); i++) {
+                    assertEquals(testList.get(i).getMediaId(), playlist.get(i).getMediaId());
+                }
+                listFromCallback.set(playlist);
+                latch.countDown();
+            }
+        };
+        final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+            @Override
+            public List<MediaItem2> getPlaylist() {
+                return testList;
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setId("testControllerCallback_onPlaylistChanged")
+                .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+                .setPlaylistAgent(agent)
+                .build()) {
+            MediaController2 controller = createController(
+                    session.getToken(), true, callback);
+            agent.notifyPlaylistChanged();
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            assertEquals(listFromCallback.get(), controller.getPlaylist());
+        }
+    }
+
+    @Test
+    public void testUpdatePlaylistMetadata() throws InterruptedException {
+        prepareLooper();
+        final MediaMetadata2 testMetadata = TestUtils.createMetadata();
+        mController.updatePlaylistMetadata(testMetadata);
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        assertTrue(mMockAgent.mUpdatePlaylistMetadataCalled);
+        assertNotNull(mMockAgent.mMetadata);
+        assertEquals(testMetadata.getMediaId(), mMockAgent.mMetadata.getMediaId());
+    }
+
+    @Test
+    public void testGetPlaylistMetadata() throws InterruptedException {
+        prepareLooper();
+        final MediaMetadata2 testMetadata = TestUtils.createMetadata();
+        final AtomicReference<MediaMetadata2> metadataFromCallback = new AtomicReference<>();
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onPlaylistMetadataChanged(MediaController2 controller,
+                    MediaMetadata2 metadata) {
+                assertNotNull(testMetadata);
+                assertEquals(testMetadata.getMediaId(), metadata.getMediaId());
+                metadataFromCallback.set(metadata);
+                latch.countDown();
+            }
+        };
+        final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+            @Override
+            public MediaMetadata2 getPlaylistMetadata() {
+                return testMetadata;
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setId("testGetPlaylistMetadata")
+                .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+                .setPlaylistAgent(agent)
+                .build()) {
+            MediaController2 controller = createController(session.getToken(), true, callback);
+            agent.notifyPlaylistMetadataChanged();
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            assertEquals(metadataFromCallback.get().getMediaId(),
+                    controller.getPlaylistMetadata().getMediaId());
+        }
+    }
+
+    @Test
+    public void testSetPlaybackSpeed() throws Exception {
+        prepareLooper();
+        final float speed = 1.5f;
+        mController.setPlaybackSpeed(speed);
+        assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertEquals(speed, mPlayer.mPlaybackSpeed, 0.0f);
+    }
+
+    /**
+     * Test whether {@link MediaSession2#setPlaylist(List, MediaMetadata2)} is notified
+     * through the
+     * {@link ControllerCallback#onPlaylistMetadataChanged(MediaController2, MediaMetadata2)}
+     * if the controller doesn't have {@link SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST} but
+     * {@link SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST_METADATA}.
+     */
+    @Test
+    public void testControllerCallback_onPlaylistMetadataChanged() throws InterruptedException {
+        prepareLooper();
+        final MediaItem2 item = TestUtils.createMediaItemWithMetadata();
+        final List<MediaItem2> list = TestUtils.createPlaylist(2);
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onPlaylistMetadataChanged(MediaController2 controller,
+                    MediaMetadata2 metadata) {
+                assertNotNull(metadata);
+                assertEquals(item.getMediaId(), metadata.getMediaId());
+                latch.countDown();
+            }
+        };
+        final SessionCallback sessionCallback = new SessionCallback() {
+            @Override
+            public SessionCommandGroup2 onConnect(MediaSession2 session,
+                    ControllerInfo controller) {
+                if (Process.myUid() == controller.getUid()) {
+                    SessionCommandGroup2 commands = new SessionCommandGroup2();
+                    commands.addCommand(new SessionCommand2(
+                              SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST_METADATA));
+                    return commands;
+                }
+                return super.onConnect(session, controller);
+            }
+        };
+        final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+            @Override
+            public MediaMetadata2 getPlaylistMetadata() {
+                return item.getMetadata();
+            }
+
+            @Override
+            public List<MediaItem2> getPlaylist() {
+                return list;
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setId("testControllerCallback_onPlaylistMetadataChanged")
+                .setSessionCallback(sHandlerExecutor, sessionCallback)
+                .setPlaylistAgent(agent)
+                .build()) {
+            MediaController2 controller = createController(session.getToken(), true, callback);
+            agent.notifyPlaylistMetadataChanged();
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+
+    @Test
+    public void testControllerCallback_onSeekCompleted() throws InterruptedException {
+        prepareLooper();
+        final long testSeekPosition = 400;
+        final long testPosition = 500;
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onSeekCompleted(MediaController2 controller, long position) {
+                // setTimeDiff is package-private, so cannot be called here.
+//                controller.setTimeDiff(Long.valueOf(0));
+                assertEquals(testSeekPosition, position);
+                assertEquals(testPosition, controller.getCurrentPosition());
+                latch.countDown();
+            }
+        };
+        final MediaController2 controller = createController(mSession.getToken(), true, callback);
+        mPlayer.mCurrentPosition = testPosition;
+        mPlayer.notifySeekCompleted(testSeekPosition);
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testControllerCallback_onBufferingStateChanged() throws InterruptedException {
+        prepareLooper();
+        final List<MediaItem2> testPlaylist = TestUtils.createPlaylist(3);
+        final MediaItem2 testItem = testPlaylist.get(0);
+        final int testBufferingState = MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_PLAYABLE;
+        final long testBufferingPosition = 500;
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onBufferingStateChanged(MediaController2 controller, MediaItem2 item,
+                    int state) {
+                // setTimeDiff is package-private, so cannot be called here.
+//                controller.setTimeDiff(Long.valueOf(0));
+                assertEquals(testItem, item);
+                assertEquals(testBufferingState, state);
+                assertEquals(testBufferingState, controller.getBufferingState());
+                assertEquals(testBufferingPosition, controller.getBufferedPosition());
+                latch.countDown();
+            }
+        };
+        final MediaController2 controller = createController(mSession.getToken(), true, callback);
+        mSession.setPlaylist(testPlaylist, null);
+        mPlayer.mBufferedPosition = testBufferingPosition;
+        mPlayer.notifyBufferingStateChanged(testItem.getDataSourceDesc(), testBufferingState);
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testControllerCallback_onPlayerStateChanged() throws InterruptedException {
+        prepareLooper();
+        final int testPlayerState = MediaPlayerInterface.PLAYER_STATE_PLAYING;
+        final long testPosition = 500;
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onPlayerStateChanged(MediaController2 controller, int state) {
+                // setTimeDiff is package-private, so cannot be called here.
+//                controller.setTimeDiff(Long.valueOf(0));
+                assertEquals(testPlayerState, state);
+                assertEquals(testPlayerState, controller.getPlayerState());
+                assertEquals(testPosition, controller.getCurrentPosition());
+                latch.countDown();
+            }
+        };
+        final MediaController2 controller = createController(mSession.getToken(), true, callback);
+        mPlayer.mCurrentPosition = testPosition;
+        mPlayer.notifyPlaybackState(testPlayerState);
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testAddPlaylistItem() throws InterruptedException {
+        prepareLooper();
+        final int testIndex = 12;
+        final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+        mController.addPlaylistItem(testIndex, testMediaItem);
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        assertTrue(mMockAgent.mAddPlaylistItemCalled);
+        assertEquals(testIndex, mMockAgent.mIndex);
+        // MediaController2.addPlaylistItem does not ensure the equality of the items.
+        assertEquals(testMediaItem.getMediaId(), mMockAgent.mItem.getMediaId());
+    }
+
+    @Test
+    public void testRemovePlaylistItem() throws InterruptedException {
+        prepareLooper();
+        mMockAgent.mPlaylist = TestUtils.createPlaylist(2);
+
+        // Recreate controller for sending removePlaylistItem.
+        // It's easier to ensure that MediaController2.getPlaylist() returns the playlist from the
+        // agent.
+        MediaController2 controller = createController(mSession.getToken());
+        MediaItem2 targetItem = controller.getPlaylist().get(0);
+        controller.removePlaylistItem(targetItem);
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        assertTrue(mMockAgent.mRemovePlaylistItemCalled);
+        assertEquals(targetItem, mMockAgent.mItem);
+    }
+
+    @Test
+    public void testReplacePlaylistItem() throws InterruptedException {
+        prepareLooper();
+        final int testIndex = 12;
+        final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+        mController.replacePlaylistItem(testIndex, testMediaItem);
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        assertTrue(mMockAgent.mReplacePlaylistItemCalled);
+        // MediaController2.replacePlaylistItem does not ensure the equality of the items.
+        assertEquals(testMediaItem.getMediaId(), mMockAgent.mItem.getMediaId());
+    }
+
+    @Test
+    public void testSkipToPreviousItem() throws InterruptedException {
+        prepareLooper();
+        mController.skipToPreviousItem();
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mMockAgent.mSkipToPreviousItemCalled);
+    }
+
+    @Test
+    public void testSkipToNextItem() throws InterruptedException {
+        prepareLooper();
+        mController.skipToNextItem();
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mMockAgent.mSkipToNextItemCalled);
+    }
+
+    @Test
+    public void testSkipToPlaylistItem() throws InterruptedException {
+        prepareLooper();
+        MediaController2 controller = createController(mSession.getToken());
+        MediaItem2 targetItem = TestUtils.createMediaItemWithMetadata();
+        controller.skipToPlaylistItem(targetItem);
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        assertTrue(mMockAgent.mSkipToPlaylistItemCalled);
+        assertEquals(targetItem, mMockAgent.mItem);
+    }
+
+    /**
+     * This also tests {@link ControllerCallback#onShuffleModeChanged(MediaController2, int)}.
+     */
+    @Test
+    public void testGetShuffleMode() throws InterruptedException {
+        prepareLooper();
+        final int testShuffleMode = MediaPlaylistAgent.SHUFFLE_MODE_GROUP;
+        final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+            @Override
+            public int getShuffleMode() {
+                return testShuffleMode;
+            }
+        };
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onShuffleModeChanged(MediaController2 controller, int shuffleMode) {
+                assertEquals(testShuffleMode, shuffleMode);
+                latch.countDown();
+            }
+        };
+        mSession.updatePlayer(mPlayer, agent, null);
+        MediaController2 controller = createController(mSession.getToken(), true, callback);
+        agent.notifyShuffleModeChanged();
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertEquals(testShuffleMode, controller.getShuffleMode());
+    }
+
+    @Test
+    public void testSetShuffleMode() throws InterruptedException {
+        prepareLooper();
+        final int testShuffleMode = MediaPlaylistAgent.SHUFFLE_MODE_GROUP;
+        mController.setShuffleMode(testShuffleMode);
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        assertTrue(mMockAgent.mSetShuffleModeCalled);
+        assertEquals(testShuffleMode, mMockAgent.mShuffleMode);
+    }
+
+    /**
+     * This also tests {@link ControllerCallback#onRepeatModeChanged(MediaController2, int)}.
+     */
+    @Test
+    public void testGetRepeatMode() throws InterruptedException {
+        prepareLooper();
+        final int testRepeatMode = MediaPlaylistAgent.REPEAT_MODE_GROUP;
+        final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+            @Override
+            public int getRepeatMode() {
+                return testRepeatMode;
+            }
+        };
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onRepeatModeChanged(MediaController2 controller, int repeatMode) {
+                assertEquals(testRepeatMode, repeatMode);
+                latch.countDown();
+            }
+        };
+        mSession.updatePlayer(mPlayer, agent, null);
+        MediaController2 controller = createController(mSession.getToken(), true, callback);
+        agent.notifyRepeatModeChanged();
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertEquals(testRepeatMode, controller.getRepeatMode());
+    }
+
+    @Test
+    public void testSetRepeatMode() throws InterruptedException {
+        prepareLooper();
+        final int testRepeatMode = MediaPlaylistAgent.REPEAT_MODE_GROUP;
+        mController.setRepeatMode(testRepeatMode);
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        assertTrue(mMockAgent.mSetRepeatModeCalled);
+        assertEquals(testRepeatMode, mMockAgent.mRepeatMode);
+    }
+
+    @Test
+    public void testSetVolumeTo() throws Exception {
+        prepareLooper();
+        final int maxVolume = 100;
+        final int currentVolume = 23;
+        final int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+        TestVolumeProvider volumeProvider =
+                new TestVolumeProvider(volumeControlType, maxVolume, currentVolume);
+
+        mSession.updatePlayer(new MockPlayer(0), null, volumeProvider);
+        final MediaController2 controller = createController(mSession.getToken(), true, null);
+
+        final int targetVolume = 50;
+        controller.setVolumeTo(targetVolume, 0 /* flags */);
+        assertTrue(volumeProvider.mLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertTrue(volumeProvider.mSetVolumeToCalled);
+        assertEquals(targetVolume, volumeProvider.mVolume);
+    }
+
+    @Test
+    public void testAdjustVolume() throws Exception {
+        prepareLooper();
+        final int maxVolume = 100;
+        final int currentVolume = 23;
+        final int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+        TestVolumeProvider volumeProvider =
+                new TestVolumeProvider(volumeControlType, maxVolume, currentVolume);
+
+        mSession.updatePlayer(new MockPlayer(0), null, volumeProvider);
+        final MediaController2 controller = createController(mSession.getToken(), true, null);
+
+        final int direction = AudioManager.ADJUST_RAISE;
+        controller.adjustVolume(direction, 0 /* flags */);
+        assertTrue(volumeProvider.mLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertTrue(volumeProvider.mAdjustVolumeCalled);
+        assertEquals(direction, volumeProvider.mDirection);
+    }
+
+    @Test
+    public void testSetVolumeWithLocalVolume() throws Exception {
+        prepareLooper();
+        if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
+            // This test is not eligible for this device.
+            return;
+        }
+
+        // Here, we intentionally choose STREAM_ALARM in order not to consider
+        // 'Do Not Disturb' or 'Volume limit'.
+        final int stream = AudioManager.STREAM_ALARM;
+        final int maxVolume = mAudioManager.getStreamMaxVolume(stream);
+        final int minVolume = 0;
+        if (maxVolume <= minVolume) {
+            return;
+        }
+
+        // Set stream of the session.
+        AudioAttributesCompat attrs = new AudioAttributesCompat.Builder()
+                .setLegacyStreamType(stream)
+                .build();
+        mPlayer.setAudioAttributes(attrs);
+        mSession.updatePlayer(mPlayer, null, null);
+
+        final int originalVolume = mAudioManager.getStreamVolume(stream);
+        final int targetVolume = originalVolume == minVolume
+                ? originalVolume + 1 : originalVolume - 1;
+
+        mController.setVolumeTo(targetVolume, AudioManager.FLAG_SHOW_UI);
+        new PollingCheck(WAIT_TIME_MS) {
+            @Override
+            protected boolean check() {
+                return targetVolume == mAudioManager.getStreamVolume(stream);
+            }
+        }.run();
+
+        // Set back to original volume.
+        mAudioManager.setStreamVolume(stream, originalVolume, 0 /* flags */);
+    }
+
+    @Test
+    public void testAdjustVolumeWithLocalVolume() throws Exception {
+        prepareLooper();
+        if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
+            // This test is not eligible for this device.
+            return;
+        }
+
+        // Here, we intentionally choose STREAM_ALARM in order not to consider
+        // 'Do Not Disturb' or 'Volume limit'.
+        final int stream = AudioManager.STREAM_ALARM;
+        final int maxVolume = mAudioManager.getStreamMaxVolume(stream);
+        final int minVolume = 0;
+        if (maxVolume <= minVolume) {
+            return;
+        }
+
+        // Set stream of the session.
+        AudioAttributesCompat attrs = new AudioAttributesCompat.Builder()
+                .setLegacyStreamType(stream)
+                .build();
+        mPlayer.setAudioAttributes(attrs);
+        mSession.updatePlayer(mPlayer, null, null);
+
+        final int originalVolume = mAudioManager.getStreamVolume(stream);
+        final int direction = originalVolume == minVolume
+                ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER;
+        final int targetVolume = originalVolume + direction;
+
+        mController.adjustVolume(direction, AudioManager.FLAG_SHOW_UI);
+        new PollingCheck(WAIT_TIME_MS) {
+            @Override
+            protected boolean check() {
+                return targetVolume == mAudioManager.getStreamVolume(stream);
+            }
+        }.run();
+
+        // Set back to original volume.
+        mAudioManager.setStreamVolume(stream, originalVolume, 0 /* flags */);
+    }
+
+    @Test
+    public void testGetPackageName() {
+        prepareLooper();
+        assertEquals(mContext.getPackageName(), mController.getSessionToken().getPackageName());
+    }
+
+    @Test
+    public void testSendCustomCommand() throws InterruptedException {
+        prepareLooper();
+        // TODO(jaewan): Need to revisit with the permission.
+        final SessionCommand2 testCommand =
+                new SessionCommand2(SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE);
+        final Bundle testArgs = new Bundle();
+        testArgs.putString("args", "testSendCustomCommand");
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onCustomCommand(MediaSession2 session, ControllerInfo controller,
+                    SessionCommand2 customCommand, Bundle args, ResultReceiver cb) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertEquals(testCommand, customCommand);
+                assertTrue(TestUtils.equals(testArgs, args));
+                assertNull(cb);
+                latch.countDown();
+            }
+        };
+        mSession.close();
+        mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback).setId(TAG).build();
+        final MediaController2 controller = createController(mSession.getToken());
+        controller.sendCustomCommand(testCommand, testArgs, null);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testControllerCallback_onConnected() throws InterruptedException {
+        prepareLooper();
+        // createController() uses controller callback to wait until the controller becomes
+        // available.
+        MediaController2 controller = createController(mSession.getToken());
+        assertNotNull(controller);
+    }
+
+    @Test
+    public void testControllerCallback_sessionRejects() throws InterruptedException {
+        prepareLooper();
+        final MediaSession2.SessionCallback sessionCallback = new SessionCallback() {
+            @Override
+            public SessionCommandGroup2 onConnect(MediaSession2 session,
+                    ControllerInfo controller) {
+                return null;
+            }
+        };
+        sHandler.postAndSync(new Runnable() {
+            @Override
+            public void run() {
+                mSession.close();
+                mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+                        .setSessionCallback(sHandlerExecutor, sessionCallback).build();
+            }
+        });
+        MediaController2 controller =
+                createController(mSession.getToken(), false, null);
+        assertNotNull(controller);
+        waitForConnect(controller, false);
+        waitForDisconnect(controller, true);
+    }
+
+    @Test
+    public void testControllerCallback_releaseSession() throws InterruptedException {
+        prepareLooper();
+        mSession.close();
+        waitForDisconnect(mController, true);
+    }
+
+    @Test
+    public void testControllerCallback_close() throws InterruptedException {
+        prepareLooper();
+        mController.close();
+        waitForDisconnect(mController, true);
+    }
+
+    @Test
+    public void testFastForward() throws InterruptedException {
+        prepareLooper();
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onFastForward(MediaSession2 session, ControllerInfo controller) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testFastForward").build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.fastForward();
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testRewind() throws InterruptedException {
+        prepareLooper();
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onRewind(MediaSession2 session, ControllerInfo controller) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testRewind").build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.rewind();
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testPlayFromSearch() throws InterruptedException {
+        prepareLooper();
+        final String request = "random query";
+        final Bundle bundle = new Bundle();
+        bundle.putString("key", "value");
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onPlayFromSearch(MediaSession2 session, ControllerInfo controller,
+                    String query, Bundle extras) {
+                super.onPlayFromSearch(session, controller, query, extras);
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertEquals(request, query);
+                assertTrue(TestUtils.equals(bundle, extras));
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testPlayFromSearch").build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.playFromSearch(request, bundle);
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testPlayFromUri() throws InterruptedException {
+        prepareLooper();
+        final Uri request = Uri.parse("foo://boo");
+        final Bundle bundle = new Bundle();
+        bundle.putString("key", "value");
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onPlayFromUri(MediaSession2 session, ControllerInfo controller, Uri uri,
+                    Bundle extras) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertEquals(request, uri);
+                assertTrue(TestUtils.equals(bundle, extras));
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testPlayFromUri").build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.playFromUri(request, bundle);
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testPlayFromMediaId() throws InterruptedException {
+        prepareLooper();
+        final String request = "media_id";
+        final Bundle bundle = new Bundle();
+        bundle.putString("key", "value");
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onPlayFromMediaId(MediaSession2 session, ControllerInfo controller,
+                    String mediaId, Bundle extras) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertEquals(request, mediaId);
+                assertTrue(TestUtils.equals(bundle, extras));
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testPlayFromMediaId").build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.playFromMediaId(request, bundle);
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testPrepareFromSearch() throws InterruptedException {
+        prepareLooper();
+        final String request = "random query";
+        final Bundle bundle = new Bundle();
+        bundle.putString("key", "value");
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onPrepareFromSearch(MediaSession2 session, ControllerInfo controller,
+                    String query, Bundle extras) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertEquals(request, query);
+                assertTrue(TestUtils.equals(bundle, extras));
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testPrepareFromSearch").build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.prepareFromSearch(request, bundle);
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testPrepareFromUri() throws InterruptedException {
+        prepareLooper();
+        final Uri request = Uri.parse("foo://boo");
+        final Bundle bundle = new Bundle();
+        bundle.putString("key", "value");
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onPrepareFromUri(MediaSession2 session, ControllerInfo controller, Uri uri,
+                    Bundle extras) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertEquals(request, uri);
+                assertTrue(TestUtils.equals(bundle, extras));
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testPrepareFromUri").build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.prepareFromUri(request, bundle);
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testPrepareFromMediaId() throws InterruptedException {
+        prepareLooper();
+        final String request = "media_id";
+        final Bundle bundle = new Bundle();
+        bundle.putString("key", "value");
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onPrepareFromMediaId(MediaSession2 session, ControllerInfo controller,
+                    String mediaId, Bundle extras) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertEquals(request, mediaId);
+                assertTrue(TestUtils.equals(bundle, extras));
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testPrepareFromMediaId").build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.prepareFromMediaId(request, bundle);
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testSetRating() throws InterruptedException {
+        prepareLooper();
+        final int ratingType = Rating2.RATING_5_STARS;
+        final float ratingValue = 3.5f;
+        final Rating2 rating = Rating2.newStarRating(ratingType, ratingValue);
+        final String mediaId = "media_id";
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onSetRating(MediaSession2 session, ControllerInfo controller,
+                    String mediaIdOut, Rating2 ratingOut) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertEquals(mediaId, mediaIdOut);
+                assertEquals(rating, ratingOut);
+                latch.countDown();
+            }
+        };
+
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testSetRating").build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.setRating(mediaId, rating);
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testIsConnected() throws InterruptedException {
+        prepareLooper();
+        assertTrue(mController.isConnected());
+        sHandler.postAndSync(new Runnable() {
+            @Override
+            public void run() {
+                mSession.close();
+            }
+        });
+        waitForDisconnect(mController, true);
+        assertFalse(mController.isConnected());
+    }
+
+    /**
+     * Test potential deadlock for calls between controller and session.
+     */
+    @Test
+    public void testDeadlock() throws InterruptedException {
+        prepareLooper();
+        sHandler.postAndSync(new Runnable() {
+            @Override
+            public void run() {
+                mSession.close();
+                mSession = null;
+            }
+        });
+
+        // Two more threads are needed not to block test thread nor test wide thread (sHandler).
+        final HandlerThread sessionThread = new HandlerThread("testDeadlock_session");
+        final HandlerThread testThread = new HandlerThread("testDeadlock_test");
+        sessionThread.start();
+        testThread.start();
+        final SyncHandler sessionHandler = new SyncHandler(sessionThread.getLooper());
+        final Handler testHandler = new Handler(testThread.getLooper());
+        final CountDownLatch latch = new CountDownLatch(1);
+        try {
+            final MockPlayer player = new MockPlayer(0);
+            sessionHandler.postAndSync(new Runnable() {
+                @Override
+                public void run() {
+                    mSession = new MediaSession2.Builder(mContext)
+                            .setPlayer(mPlayer)
+                            .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+                            .setId("testDeadlock").build();
+                }
+            });
+            final MediaController2 controller = createController(mSession.getToken());
+            testHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    final int state = MediaPlayerInterface.PLAYER_STATE_ERROR;
+                    for (int i = 0; i < 100; i++) {
+                        // triggers call from session to controller.
+                        player.notifyPlaybackState(state);
+                        // triggers call from controller to session.
+                        controller.play();
+
+                        // Repeat above
+                        player.notifyPlaybackState(state);
+                        controller.pause();
+                        player.notifyPlaybackState(state);
+                        controller.reset();
+                        player.notifyPlaybackState(state);
+                        controller.skipToNextItem();
+                        player.notifyPlaybackState(state);
+                        controller.skipToPreviousItem();
+                    }
+                    // This may hang if deadlock happens.
+                    latch.countDown();
+                }
+            });
+            assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } finally {
+            if (mSession != null) {
+                sessionHandler.postAndSync(new Runnable() {
+                    @Override
+                    public void run() {
+                        // Clean up here because sessionHandler will be removed afterwards.
+                        mSession.close();
+                        mSession = null;
+                    }
+                });
+            }
+
+            if (Build.VERSION.SDK_INT >= 18) {
+                sessionThread.quitSafely();
+                testThread.quitSafely();
+            } else {
+                sessionThread.quit();
+                testThread.quit();
+            }
+        }
+    }
+
+    // Temporaily commenting out, since we don't have the Mock services yet.
+//    @Test
+//    public void testGetServiceToken() {
+//        prepareLooper();
+//        SessionToken2 token = TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID);
+//        assertNotNull(token);
+//        assertEquals(mContext.getPackageName(), token.getPackageName());
+//        assertEquals(MockMediaSessionService2.ID, token.getId());
+//        assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+//    }
+//
+//    @Test
+//    public void testConnectToService_sessionService() throws InterruptedException {
+//        prepareLooper();
+//        testConnectToService(MockMediaSessionService2.ID);
+//    }
+//
+//    @Test
+//    public void testConnectToService_libraryService() throws InterruptedException {
+//        prepareLooper();
+//        testConnectToService(MockMediaLibraryService2.ID);
+//    }
+//
+//    public void testConnectToService(String id) throws InterruptedException {
+//        prepareLooper();
+//        final CountDownLatch latch = new CountDownLatch(1);
+//        final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
+//            @Override
+//            public SessionCommandGroup2 onConnect(@NonNull MediaSession2 session,
+//                    @NonNull ControllerInfo controller) {
+//                if (Process.myUid() == controller.getUid()) {
+//                    if (mSession != null) {
+//                        mSession.close();
+//                    }
+//                    mSession = session;
+//                    mPlayer = (MockPlayer) session.getPlayer();
+//                    assertEquals(mContext.getPackageName(), controller.getPackageName());
+//                    assertFalse(controller.isTrusted());
+//                    latch.countDown();
+//                }
+//                return super.onConnect(session, controller);
+//            }
+//        };
+//        TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
+//
+//        final SessionCommand2 testCommand = new SessionCommand2("testConnectToService", null);
+//        final CountDownLatch controllerLatch = new CountDownLatch(1);
+//        mController = createController(TestUtils.getServiceToken(mContext, id), true,
+//                new ControllerCallback() {
+//                    @Override
+//                    public void onCustomCommand(MediaController2 controller,
+//                            SessionCommand2 command, Bundle args, ResultReceiver receiver) {
+//                        if (testCommand.equals(command)) {
+//                            controllerLatch.countDown();
+//                        }
+//                    }
+//                }
+//        );
+//        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+//
+//        // Test command from controller to session service.
+//        mController.play();
+//        assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+//        assertTrue(mPlayer.mPlayCalled);
+//
+//        // Test command from session service to controller.
+//        mSession.sendCustomCommand(testCommand, null);
+//        assertTrue(controllerLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+//    }
+
+    @Test
+    public void testControllerAfterSessionIsGone_session() throws InterruptedException {
+        prepareLooper();
+        testControllerAfterSessionIsClosed(mSession.getToken().getId());
+    }
+
+    // Temporaily commenting out, since we don't have the Mock services yet.
+//    @Test
+//    public void testControllerAfterSessionIsClosed_sessionService() throws InterruptedException {
+//        prepareLooper();
+//        testConnectToService(MockMediaSessionService2.ID);
+//        testControllerAfterSessionIsClosed(MockMediaSessionService2.ID);
+//    }
+
+    @Test
+    public void testSubscribeRouteInfo() throws InterruptedException {
+        prepareLooper();
+        final TestSessionCallback callback = new TestSessionCallback() {
+            @Override
+            public void onSubscribeRoutesInfo(@NonNull MediaSession2 session,
+                    @NonNull ControllerInfo controller) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                mLatch.countDown();
+            }
+
+            @Override
+            public void onUnsubscribeRoutesInfo(@NonNull MediaSession2 session,
+                    @NonNull ControllerInfo controller) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                mLatch.countDown();
+            }
+        };
+        mSession.close();
+        mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback).setId(TAG).build();
+        final MediaController2 controller = createController(mSession.getToken());
+
+        callback.resetLatchCount(1);
+        controller.subscribeRoutesInfo();
+        assertTrue(callback.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        callback.resetLatchCount(1);
+        controller.unsubscribeRoutesInfo();
+        assertTrue(callback.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testSelectRouteInfo() throws InterruptedException {
+        prepareLooper();
+        final Bundle testRoute = new Bundle();
+        testRoute.putString("id", "testRoute");
+        final TestSessionCallback callback = new TestSessionCallback() {
+            @Override
+            public void onSelectRoute(@NonNull MediaSession2 session,
+                    @NonNull ControllerInfo controller, @NonNull Bundle route) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertTrue(TestUtils.equals(route, testRoute));
+                mLatch.countDown();
+            }
+        };
+        mSession.close();
+        mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback).setId(TAG).build();
+        final MediaController2 controller = createController(mSession.getToken());
+
+        callback.resetLatchCount(1);
+        controller.selectRoute(testRoute);
+        assertTrue(callback.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testClose_beforeConnected() throws InterruptedException {
+        prepareLooper();
+        MediaController2 controller =
+                createController(mSession.getToken(), false, null);
+        controller.close();
+    }
+
+    @Test
+    public void testClose_twice() {
+        prepareLooper();
+        mController.close();
+        mController.close();
+    }
+
+    @Test
+    public void testClose_session() throws InterruptedException {
+        prepareLooper();
+        final String id = mSession.getToken().getId();
+        mController.close();
+        // close is done immediately for session.
+        testNoInteraction();
+
+        // Test whether the controller is notified about later close of the session or
+        // re-creation.
+        testControllerAfterSessionIsClosed(id);
+    }
+
+    // Temporaily commenting out, since we don't have the Mock services yet.
+//    @Test
+//    public void testClose_sessionService() throws InterruptedException {
+//        prepareLooper();
+//        testCloseFromService(MockMediaSessionService2.ID);
+//    }
+//
+//    @Test
+//    public void testClose_libraryService() throws InterruptedException {
+//        prepareLooper();
+//        testCloseFromService(MockMediaLibraryService2.ID);
+//    }
+//
+//    private void testCloseFromService(String id) throws InterruptedException {
+//        final CountDownLatch latch = new CountDownLatch(1);
+//        TestServiceRegistry.getInstance().setSessionServiceCallback(new SessionServiceCallback() {
+//            @Override
+//            public void onCreated() {
+//                // Do nothing.
+//            }
+//
+//            @Override
+//            public void onDestroyed() {
+//                latch.countDown();
+//            }
+//        });
+//        mController = createController(TestUtils.getServiceToken(mContext, id));
+//        mController.close();
+//        // Wait until close triggers onDestroy() of the session service.
+//        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+//        assertNull(TestServiceRegistry.getInstance().getServiceInstance());
+//        testNoInteraction();
+//
+//        // Test whether the controller is notified about later close of the session or
+//        // re-creation.
+//        testControllerAfterSessionIsClosed(id);
+//    }
+
+    private void testControllerAfterSessionIsClosed(final String id) throws InterruptedException {
+        // This cause session service to be died.
+        mSession.close();
+        waitForDisconnect(mController, true);
+        testNoInteraction();
+
+        // Ensure that the controller cannot use newly create session with the same ID.
+        // Recreated session has different session stub, so previously created controller
+        // shouldn't be available.
+        mSession = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+                .setId(id).build();
+        testNoInteraction();
+    }
+
+    // Test that mSession and mController doesn't interact.
+    // Note that this method can be called after the mSession is died, so mSession may not have
+    // valid player.
+    private void testNoInteraction() throws InterruptedException {
+        // TODO: check that calls from the controller to session shouldn't be delivered.
+
+        // Calls from the session to controller shouldn't be delivered.
+        final CountDownLatch latch = new CountDownLatch(1);
+        setRunnableForOnCustomCommand(mController, new Runnable() {
+            @Override
+            public void run() {
+                latch.countDown();
+            }
+        });
+        SessionCommand2 customCommand = new SessionCommand2("testNoInteraction", null);
+        mSession.sendCustomCommand(customCommand, null);
+        assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        setRunnableForOnCustomCommand(mController, null);
+    }
+
+    // TODO(jaewan): Add  test for service connect rejection, when we differentiate session
+    //               active/inactive and connection accept/refuse
+
+    class TestVolumeProvider extends VolumeProviderCompat {
+        final CountDownLatch mLatch = new CountDownLatch(1);
+        boolean mSetVolumeToCalled;
+        boolean mAdjustVolumeCalled;
+        int mVolume;
+        int mDirection;
+
+        TestVolumeProvider(int controlType, int maxVolume, int currentVolume) {
+            super(controlType, maxVolume, currentVolume);
+        }
+
+        @Override
+        public void onSetVolumeTo(int volume) {
+            mSetVolumeToCalled = true;
+            mVolume = volume;
+            mLatch.countDown();
+        }
+
+        @Override
+        public void onAdjustVolume(int direction) {
+            mAdjustVolumeCalled = true;
+            mDirection = direction;
+            mLatch.countDown();
+        }
+    }
+
+    class TestSessionCallback extends SessionCallback {
+        CountDownLatch mLatch;
+
+        void resetLatchCount(int count) {
+            mLatch = new CountDownLatch(count);
+        }
+    }
+}
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaSession2TestBase.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaSession2TestBase.java
new file mode 100644
index 0000000..a8da0b1
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaSession2TestBase.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.client;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.support.test.InstrumentationRegistry;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.media.MediaController2;
+import androidx.media.MediaController2.ControllerCallback;
+import androidx.media.MediaItem2;
+import androidx.media.MediaMetadata2;
+import androidx.media.MediaSession2.CommandButton;
+import androidx.media.SessionCommand2;
+import androidx.media.SessionCommandGroup2;
+import androidx.media.SessionToken2;
+import androidx.media.test.client.TestUtils.SyncHandler;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Base class for session test.
+ * <p>
+ * For all subclasses, all individual tests should begin with the {@link #prepareLooper()}. See
+ * {@link #prepareLooper} for details.
+ */
+abstract class MediaSession2TestBase {
+    // Expected success
+    static final int WAIT_TIME_MS = 1000;
+
+    // Expected timeout
+    static final int TIMEOUT_MS = 500;
+
+    static SyncHandler sHandler;
+    static Executor sHandlerExecutor;
+
+    Context mContext;
+    private List<MediaController2> mControllers = new ArrayList<>();
+
+    interface TestControllerInterface {
+        ControllerCallback getCallback();
+    }
+
+    interface TestControllerCallbackInterface {
+        void waitForConnect(boolean expect) throws InterruptedException;
+        void waitForDisconnect(boolean expect) throws InterruptedException;
+        void setRunnableForOnCustomCommand(Runnable runnable);
+    }
+
+    /**
+     * All tests methods should start with this.
+     * <p>
+     * MediaControllerCompat, which is wrapped by the MediaSession2, can be only created by the
+     * thread whose Looper is prepared. However, when the presubmit tests runs on the server,
+     * test runs with the {@link org.junit.internal.runners.statements.FailOnTimeout} which creates
+     * dedicated thread for running test methods while methods annotated with @After or @Before
+     * runs on the different thread. This ensures that the current Looper is prepared.
+     * <p>
+     * To address the issue .
+     */
+    public static void prepareLooper() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+    }
+
+    @BeforeClass
+    public static void setUpThread() {
+        synchronized (MediaSession2TestBase.class) {
+            if (sHandler != null) {
+                return;
+            }
+            prepareLooper();
+            HandlerThread handlerThread = new HandlerThread("MediaSession2TestBase");
+            handlerThread.start();
+            sHandler = new SyncHandler(handlerThread.getLooper());
+            sHandlerExecutor = new Executor() {
+                @Override
+                public void execute(Runnable runnable) {
+                    SyncHandler handler;
+                    synchronized (MediaSession2TestBase.class) {
+                        handler = sHandler;
+                    }
+                    if (handler != null) {
+                        handler.post(runnable);
+                    }
+                }
+            };
+        }
+    }
+
+    @AfterClass
+    public static void cleanUpThread() {
+        synchronized (MediaSession2TestBase.class) {
+            if (sHandler == null) {
+                return;
+            }
+            if (Build.VERSION.SDK_INT >= 18) {
+                sHandler.getLooper().quitSafely();
+            } else {
+                sHandler.getLooper().quit();
+            }
+            sHandler = null;
+            sHandlerExecutor = null;
+        }
+    }
+
+    @CallSuper
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getTargetContext();
+    }
+
+    @CallSuper
+    public void cleanUp() throws Exception {
+        for (int i = 0; i < mControllers.size(); i++) {
+            mControllers.get(i).close();
+        }
+    }
+
+    final MediaController2 createController(SessionToken2 token) throws InterruptedException {
+        return createController(token, true, null);
+    }
+
+    final MediaController2 createController(@NonNull SessionToken2 token,
+            boolean waitForConnect, @Nullable ControllerCallback callback)
+            throws InterruptedException {
+        TestControllerInterface instance = onCreateController(token, callback);
+        if (!(instance instanceof MediaController2)) {
+            throw new RuntimeException("Test has a bug. Expected MediaController2 but returned "
+                    + instance);
+        }
+        MediaController2 controller = (MediaController2) instance;
+        mControllers.add(controller);
+        if (waitForConnect) {
+            waitForConnect(controller, true);
+        }
+        return controller;
+    }
+
+    private static TestControllerCallbackInterface getTestControllerCallbackInterface(
+            MediaController2 controller) {
+        if (!(controller instanceof TestControllerInterface)) {
+            throw new RuntimeException("Test has a bug. Expected controller implemented"
+                    + " TestControllerInterface but got " + controller);
+        }
+        ControllerCallback callback = ((TestControllerInterface) controller).getCallback();
+        if (!(callback instanceof TestControllerCallbackInterface)) {
+            throw new RuntimeException("Test has a bug. Expected controller with callback "
+                    + " implemented TestControllerCallbackInterface but got " + controller);
+        }
+        return (TestControllerCallbackInterface) callback;
+    }
+
+    public static void waitForConnect(MediaController2 controller, boolean expected)
+            throws InterruptedException {
+        getTestControllerCallbackInterface(controller).waitForConnect(expected);
+    }
+
+    public static void waitForDisconnect(MediaController2 controller, boolean expected)
+            throws InterruptedException {
+        getTestControllerCallbackInterface(controller).waitForDisconnect(expected);
+    }
+
+    public static void setRunnableForOnCustomCommand(MediaController2 controller,
+            Runnable runnable) {
+        getTestControllerCallbackInterface(controller).setRunnableForOnCustomCommand(runnable);
+    }
+
+    TestControllerInterface onCreateController(final @NonNull SessionToken2 token,
+            @Nullable ControllerCallback callback) throws InterruptedException {
+        final ControllerCallback controllerCallback =
+                callback != null ? callback : new ControllerCallback() {};
+        final AtomicReference<TestControllerInterface> controller = new AtomicReference<>();
+        sHandler.postAndSync(new Runnable() {
+            @Override
+            public void run() {
+                // Create controller on the test handler, for changing MediaBrowserCompat's Handler
+                // Looper. Otherwise, MediaBrowserCompat will post all the commands to the handler
+                // and commands wouldn't be run if tests codes waits on the test handler.
+                controller.set(new TestMediaController(
+                        mContext, token, new TestControllerCallback(controllerCallback)));
+            }
+        });
+        return controller.get();
+    }
+
+    // TODO(jaewan): (Can be Post-P): Deprecate this
+    public static class TestControllerCallback extends MediaController2.ControllerCallback
+            implements TestControllerCallbackInterface {
+        public final ControllerCallback mCallbackProxy;
+        public final CountDownLatch connectLatch = new CountDownLatch(1);
+        public final CountDownLatch disconnectLatch = new CountDownLatch(1);
+        @GuardedBy("this")
+        private Runnable mOnCustomCommandRunnable;
+
+        TestControllerCallback(@NonNull ControllerCallback callbackProxy) {
+            if (callbackProxy == null) {
+                throw new IllegalArgumentException("Callback proxy shouldn't be null. Test bug");
+            }
+            mCallbackProxy = callbackProxy;
+        }
+
+        @CallSuper
+        @Override
+        public void onConnected(MediaController2 controller, SessionCommandGroup2 commands) {
+            connectLatch.countDown();
+        }
+
+        @CallSuper
+        @Override
+        public void onDisconnected(MediaController2 controller) {
+            disconnectLatch.countDown();
+        }
+
+        @Override
+        public void waitForConnect(boolean expect) throws InterruptedException {
+            if (expect) {
+                assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            } else {
+                assertFalse(connectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            }
+        }
+
+        @Override
+        public void waitForDisconnect(boolean expect) throws InterruptedException {
+            if (expect) {
+                assertTrue(disconnectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            } else {
+                assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            }
+        }
+
+        @Override
+        public void onCustomCommand(MediaController2 controller, SessionCommand2 command,
+                Bundle args, ResultReceiver receiver) {
+            mCallbackProxy.onCustomCommand(controller, command, args, receiver);
+            synchronized (this) {
+                if (mOnCustomCommandRunnable != null) {
+                    mOnCustomCommandRunnable.run();
+                }
+            }
+        }
+
+        @Override
+        public void onPlaybackInfoChanged(MediaController2 controller,
+                MediaController2.PlaybackInfo info) {
+            mCallbackProxy.onPlaybackInfoChanged(controller, info);
+        }
+
+        @Override
+        public void onCustomLayoutChanged(MediaController2 controller, List<CommandButton> layout) {
+            mCallbackProxy.onCustomLayoutChanged(controller, layout);
+        }
+
+        @Override
+        public void onAllowedCommandsChanged(MediaController2 controller,
+                SessionCommandGroup2 commands) {
+            mCallbackProxy.onAllowedCommandsChanged(controller, commands);
+        }
+
+        @Override
+        public void onPlayerStateChanged(MediaController2 controller, int state) {
+            mCallbackProxy.onPlayerStateChanged(controller, state);
+        }
+
+        @Override
+        public void onSeekCompleted(MediaController2 controller, long position) {
+            mCallbackProxy.onSeekCompleted(controller, position);
+        }
+
+        @Override
+        public void onPlaybackSpeedChanged(MediaController2 controller, float speed) {
+            mCallbackProxy.onPlaybackSpeedChanged(controller, speed);
+        }
+
+        @Override
+        public void onBufferingStateChanged(MediaController2 controller, MediaItem2 item,
+                int state) {
+            mCallbackProxy.onBufferingStateChanged(controller, item, state);
+        }
+
+        @Override
+        public void onError(MediaController2 controller, int errorCode, Bundle extras) {
+            mCallbackProxy.onError(controller, errorCode, extras);
+        }
+
+        @Override
+        public void onCurrentMediaItemChanged(MediaController2 controller, MediaItem2 item) {
+            mCallbackProxy.onCurrentMediaItemChanged(controller, item);
+        }
+
+        @Override
+        public void onPlaylistChanged(MediaController2 controller,
+                List<MediaItem2> list, MediaMetadata2 metadata) {
+            mCallbackProxy.onPlaylistChanged(controller, list, metadata);
+        }
+
+        @Override
+        public void onPlaylistMetadataChanged(MediaController2 controller,
+                MediaMetadata2 metadata) {
+            mCallbackProxy.onPlaylistMetadataChanged(controller, metadata);
+        }
+
+        @Override
+        public void onShuffleModeChanged(MediaController2 controller, int shuffleMode) {
+            mCallbackProxy.onShuffleModeChanged(controller, shuffleMode);
+        }
+
+        @Override
+        public void onRepeatModeChanged(MediaController2 controller, int repeatMode) {
+            mCallbackProxy.onRepeatModeChanged(controller, repeatMode);
+        }
+
+        @Override
+        public void setRunnableForOnCustomCommand(Runnable runnable) {
+            synchronized (this) {
+                mOnCustomCommandRunnable = runnable;
+            }
+        }
+
+        @Override
+        public void onRoutesInfoChanged(@NonNull MediaController2 controller,
+                @Nullable List<Bundle> routes) {
+            mCallbackProxy.onRoutesInfoChanged(controller, routes);
+        }
+    }
+
+    public class TestMediaController extends MediaController2 implements TestControllerInterface {
+        private final ControllerCallback mCallback;
+
+        TestMediaController(@NonNull Context context, @NonNull SessionToken2 token,
+                @NonNull ControllerCallback callback) {
+            super(context, token, sHandlerExecutor, callback);
+            mCallback = callback;
+        }
+
+        @Override
+        public ControllerCallback getCallback() {
+            return mCallback;
+        }
+    }
+}
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MockActivity.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MockActivity.java
new file mode 100644
index 0000000..1bcb93a
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MockActivity.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.client;
+
+import android.app.Activity;
+
+public class MockActivity extends Activity {
+}
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MockPlayer.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MockPlayer.java
new file mode 100644
index 0000000..7a61184
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MockPlayer.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.client;
+
+import androidx.annotation.NonNull;
+import androidx.collection.ArrayMap;
+import androidx.media.AudioAttributesCompat;
+import androidx.media.DataSourceDesc;
+import androidx.media.MediaPlayerInterface;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+
+/**
+ * A mock implementation of {@link MediaPlayerInterface} for testing.
+ */
+public class MockPlayer extends MediaPlayerInterface {
+    public final CountDownLatch mCountDownLatch;
+
+    public boolean mPlayCalled;
+    public boolean mPauseCalled;
+    public boolean mResetCalled;
+    public boolean mPrepareCalled;
+    public boolean mSeekToCalled;
+    public boolean mSetPlaybackSpeedCalled;
+    public long mSeekPosition;
+    public long mCurrentPosition;
+    public long mBufferedPosition;
+    public float mPlaybackSpeed = 1.0f;
+    public @PlayerState int mLastPlayerState;
+    public @BuffState int mLastBufferingState;
+    public long mDuration;
+
+    public ArrayMap<PlayerEventCallback, Executor> mCallbacks = new ArrayMap<>();
+
+    private AudioAttributesCompat mAudioAttributes;
+
+    public MockPlayer(int count) {
+        mCountDownLatch = (count > 0) ? new CountDownLatch(count) : null;
+    }
+
+    @Override
+    public void close() {
+        // no-op
+    }
+
+    @Override
+    public void reset() {
+        mResetCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void play() {
+        mPlayCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void pause() {
+        mPauseCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void prepare() {
+        mPrepareCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void seekTo(long pos) {
+        mSeekToCalled = true;
+        mSeekPosition = pos;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void skipToNext() {
+        // No-op. This skipToNext() means 'skip to next item in the setNextDataSources()'
+    }
+
+    @Override
+    public int getPlayerState() {
+        return mLastPlayerState;
+    }
+
+    @Override
+    public long getCurrentPosition() {
+        return mCurrentPosition;
+    }
+
+    @Override
+    public long getBufferedPosition() {
+        return mBufferedPosition;
+    }
+
+    @Override
+    public float getPlaybackSpeed() {
+        return mPlaybackSpeed;
+    }
+
+    @Override
+    public int getBufferingState() {
+        return mLastBufferingState;
+    }
+
+    @Override
+    public long getDuration() {
+        return mDuration;
+    }
+
+    @Override
+    public void registerPlayerEventCallback(@NonNull Executor executor,
+            @NonNull PlayerEventCallback callback) {
+        if (callback == null || executor == null) {
+            throw new IllegalArgumentException("callback=" + callback + " executor=" + executor);
+        }
+        mCallbacks.put(callback, executor);
+    }
+
+    @Override
+    public void unregisterPlayerEventCallback(@NonNull PlayerEventCallback callback) {
+        mCallbacks.remove(callback);
+    }
+
+    public void notifyPlaybackState(final int state) {
+        mLastPlayerState = state;
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onPlayerStateChanged(MockPlayer.this, state);
+                }
+            });
+        }
+    }
+
+    public void notifyCurrentDataSourceChanged(final DataSourceDesc dsd) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onCurrentDataSourceChanged(MockPlayer.this, dsd);
+                }
+            });
+        }
+    }
+
+    public void notifyMediaPrepared(final DataSourceDesc dsd) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onMediaPrepared(MockPlayer.this, dsd);
+                }
+            });
+        }
+    }
+
+    public void notifyBufferingStateChanged(final DataSourceDesc dsd,
+            final @BuffState int buffState) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onBufferingStateChanged(MockPlayer.this, dsd, buffState);
+                }
+            });
+        }
+    }
+
+    public void notifyPlaybackSpeedChanged(final float speed) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onPlaybackSpeedChanged(MockPlayer.this, speed);
+                }
+            });
+        }
+    }
+
+    public void notifySeekCompleted(final long position) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onSeekCompleted(MockPlayer.this, position);
+                }
+            });
+        }
+    }
+
+    public void notifyError(int what) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            // TODO: Uncomment or remove
+            //executor.execute(() -> callback.onError(null, what, 0));
+        }
+    }
+
+    @Override
+    public void setAudioAttributes(AudioAttributesCompat attributes) {
+        mAudioAttributes = attributes;
+    }
+
+    @Override
+    public AudioAttributesCompat getAudioAttributes() {
+        return mAudioAttributes;
+    }
+
+    @Override
+    public void setDataSource(@NonNull DataSourceDesc dsd) {
+        // TODO: Implement this
+    }
+
+    @Override
+    public void setNextDataSource(@NonNull DataSourceDesc dsd) {
+        // TODO: Implement this
+    }
+
+    @Override
+    public void setNextDataSources(@NonNull List<DataSourceDesc> dsds) {
+        // TODO: Implement this
+    }
+
+    @Override
+    public DataSourceDesc getCurrentDataSource() {
+        // TODO: Implement this
+        return null;
+    }
+
+    @Override
+    public void loopCurrent(boolean loop) {
+        // TODO: implement this
+    }
+
+    @Override
+    public void setPlaybackSpeed(float speed) {
+        mSetPlaybackSpeedCalled = true;
+        mPlaybackSpeed = speed;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void setPlayerVolume(float volume) {
+        // TODO: implement this
+    }
+
+    @Override
+    public float getPlayerVolume() {
+        // TODO: implement this
+        return -1;
+    }
+}
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MockPlaylistAgent.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MockPlaylistAgent.java
new file mode 100644
index 0000000..1ed6cd7
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MockPlaylistAgent.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.client;
+
+import androidx.media.MediaItem2;
+import androidx.media.MediaMetadata2;
+import androidx.media.MediaPlaylistAgent;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * A mock implementation of {@link MediaPlaylistAgent} for testing.
+ * <p>
+ * Do not use mockito for {@link MediaPlaylistAgent}. Instead, use this.
+ * Mocks created from mockito should not be shared across different threads.
+ */
+public class MockPlaylistAgent extends MediaPlaylistAgent {
+    public final CountDownLatch mCountDownLatch = new CountDownLatch(1);
+
+    public List<MediaItem2> mPlaylist;
+    public MediaMetadata2 mMetadata;
+    public MediaItem2 mCurrentMediaItem;
+    public MediaItem2 mItem;
+    public int mIndex = -1;
+    public @RepeatMode int mRepeatMode = -1;
+    public @ShuffleMode int mShuffleMode = -1;
+
+    public boolean mSetPlaylistCalled;
+    public boolean mUpdatePlaylistMetadataCalled;
+    public boolean mAddPlaylistItemCalled;
+    public boolean mRemovePlaylistItemCalled;
+    public boolean mReplacePlaylistItemCalled;
+    public boolean mSkipToPlaylistItemCalled;
+    public boolean mSkipToPreviousItemCalled;
+    public boolean mSkipToNextItemCalled;
+    public boolean mSetRepeatModeCalled;
+    public boolean mSetShuffleModeCalled;
+
+    @Override
+    public List<MediaItem2> getPlaylist() {
+        return mPlaylist;
+    }
+
+    @Override
+    public void setPlaylist(List<MediaItem2> list, MediaMetadata2 metadata) {
+        mSetPlaylistCalled = true;
+        mPlaylist = list;
+        mMetadata = metadata;
+        mCountDownLatch.countDown();
+    }
+
+    @Override
+    public MediaMetadata2 getPlaylistMetadata() {
+        return mMetadata;
+    }
+
+    @Override
+    public void updatePlaylistMetadata(MediaMetadata2 metadata) {
+        mUpdatePlaylistMetadataCalled = true;
+        mMetadata = metadata;
+        mCountDownLatch.countDown();
+    }
+
+    @Override
+    public MediaItem2 getCurrentMediaItem() {
+        return mCurrentMediaItem;
+    }
+
+    @Override
+    public void addPlaylistItem(int index, MediaItem2 item) {
+        mAddPlaylistItemCalled = true;
+        mIndex = index;
+        mItem = item;
+        mCountDownLatch.countDown();
+    }
+
+    @Override
+    public void removePlaylistItem(MediaItem2 item) {
+        mRemovePlaylistItemCalled = true;
+        mItem = item;
+        mCountDownLatch.countDown();
+    }
+
+    @Override
+    public void replacePlaylistItem(int index, MediaItem2 item) {
+        mReplacePlaylistItemCalled = true;
+        mIndex = index;
+        mItem = item;
+        mCountDownLatch.countDown();
+    }
+
+    @Override
+    public void skipToPlaylistItem(MediaItem2 item) {
+        mSkipToPlaylistItemCalled = true;
+        mItem = item;
+        mCountDownLatch.countDown();
+    }
+
+    @Override
+    public void skipToPreviousItem() {
+        mSkipToPreviousItemCalled = true;
+        mCountDownLatch.countDown();
+    }
+
+    @Override
+    public void skipToNextItem() {
+        mSkipToNextItemCalled = true;
+        mCountDownLatch.countDown();
+    }
+
+    @Override
+    public int getRepeatMode() {
+        return mRepeatMode;
+    }
+
+    @Override
+    public void setRepeatMode(int repeatMode) {
+        mSetRepeatModeCalled = true;
+        mRepeatMode = repeatMode;
+        mCountDownLatch.countDown();
+    }
+
+    @Override
+    public int getShuffleMode() {
+        return mShuffleMode;
+    }
+
+    @Override
+    public void setShuffleMode(int shuffleMode) {
+        mSetShuffleModeCalled = true;
+        mShuffleMode = shuffleMode;
+        mCountDownLatch.countDown();
+    }
+}
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestHelper.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestHelper.java
new file mode 100644
index 0000000..76c7758
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestHelper.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.client;
+
+import static androidx.media.test.lib.TestHelperUtil.ACTION_TEST_HELPER;
+import static androidx.media.test.lib.TestHelperUtil.SERVICE_TEST_HELPER_COMPONENT_NAME;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.support.mediacompat.testlib.ITestHelperForServiceApp;
+import android.util.Log;
+
+import androidx.media.MediaSession2;
+import androidx.media.SessionToken2;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Interacts with service app's TestHelperService.
+ */
+public class TestHelper {
+
+    private static final String TAG = "TestHelper";
+
+    private Context mContext;
+    private ServiceConnection mServiceConnection;
+    private ITestHelperForServiceApp mBinder;
+
+    private CountDownLatch mCountDownLatch;
+
+    public TestHelper(Context context) {
+        mContext = context;
+        mCountDownLatch = new CountDownLatch(1);
+    }
+
+    /**
+     * Connects to service app's TestHelperService. Should NOT be called in main thread.
+     *
+     * @return true if connected successfully, false if failed to connect.
+     */
+    public boolean connect(int timeoutMs) {
+        mServiceConnection = new MyServiceConnection();
+
+        final Intent intent = new Intent(ACTION_TEST_HELPER);
+        intent.setComponent(SERVICE_TEST_HELPER_COMPONENT_NAME);
+
+        boolean bound = false;
+        try {
+            bound = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+        } catch (Exception ex) {
+            Log.e(TAG, "Failed binding to the test helper service of service app");
+        }
+
+        if (bound) {
+            try {
+                mCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException ex) {
+                Log.e(TAG, "InterruptedException while waiting for onServiceConnected.", ex);
+            }
+        }
+        return mBinder != null;
+    }
+
+    /**
+     * Create a session2 in the service app, and gets its token.
+     *
+     * @return A {@link SessionToken2} object if succeeded, {@code null} if failed.
+     */
+    public SessionToken2 getSessionToken2(String testName) {
+        SessionToken2 token = null;
+        try {
+            Bundle bundle = mBinder.getSessionToken2(testName);
+            if (bundle != null) {
+                bundle.setClassLoader(MediaSession2.class.getClassLoader());
+            }
+            token = SessionToken2.fromBundle(bundle);
+        } catch (RemoteException ex) {
+            Log.e(TAG, "Failed to get session token. testName=" + testName);
+        }
+        return token;
+    }
+
+    // These methods will run on main thread.
+    class MyServiceConnection implements ServiceConnection {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            Log.d(TAG, "Connected to service app's TestHelperService.");
+            mBinder = ITestHelperForServiceApp.Stub.asInterface(service);
+            mCountDownLatch.countDown();
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            Log.d(TAG, "Disconnected from the service.");
+        }
+    }
+}
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestHelperTest.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestHelperTest.java
new file mode 100644
index 0000000..af5b1ed
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestHelperTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.client;
+
+import static android.support.mediacompat.testlib.VersionConstants.KEY_SERVICE_VERSION;
+import static android.support.mediacompat.testlib.VersionConstants.VERSION_TOT;
+import static android.support.mediacompat.testlib.util.IntentUtil.SERVICE_PACKAGE_NAME;
+import static android.support.test.InstrumentationRegistry.getArguments;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.os.Looper;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.media.MediaController2;
+import androidx.media.SessionToken2;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Executor;
+
+/** Test {@link TestHelper}. */
+@RunWith(AndroidJUnit4.class)
+public class TestHelperTest {
+    private static final int TIME_OUT_MS = 3000;
+
+    private Context mContext;
+    private TestHelper mTestHelper;
+    private String mServiceVersion;
+
+    @Before
+    public void setUp() {
+        // The version of the service app is provided through the instrumentation arguments.
+        mServiceVersion = getArguments().getString(KEY_SERVICE_VERSION, "");
+        if (!VERSION_TOT.equals(mServiceVersion)) {
+            return;
+        }
+
+        mContext = InstrumentationRegistry.getTargetContext();
+        mTestHelper = new TestHelper(mContext);
+        boolean connected = mTestHelper.connect(TIME_OUT_MS);
+        if (!connected) {
+            fail("Failed to connect to Test helper service.");
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testGettingToken() {
+        if (!VERSION_TOT.equals(mServiceVersion)) {
+            return;
+        }
+        SessionToken2 token = mTestHelper.getSessionToken2("testGettingToken");
+        assertNotNull(token);
+        assertEquals(SERVICE_PACKAGE_NAME, token.getPackageName());
+    }
+
+    @Test
+    @SmallTest
+    public void testCreatingController() {
+        if (!VERSION_TOT.equals(mServiceVersion)) {
+            return;
+        }
+        Looper.prepare();
+        SessionToken2 token = mTestHelper.getSessionToken2("testCreatingController");
+        assertNotNull(token);
+        MediaController2 controller = new MediaController2(mContext, token, new Executor() {
+            @Override
+            public void execute(Runnable command) {
+                command.run();
+            }
+        }, new MediaController2.ControllerCallback() {});
+    }
+}
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestServiceRegistry.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestServiceRegistry.java
new file mode 100644
index 0000000..8c87073
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestServiceRegistry.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.client;
+
+import static org.junit.Assert.fail;
+
+import android.os.Handler;
+
+import androidx.annotation.GuardedBy;
+import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
+import androidx.media.MediaSessionService2;
+import androidx.media.test.client.TestUtils.SyncHandler;
+
+/**
+ * Keeps the instance of currently running {@link MockMediaSessionService2}. And also provides
+ * a way to control them in one place.
+ * <p>
+ * It only support only one service at a time.
+ */
+public class TestServiceRegistry {
+    @GuardedBy("TestServiceRegistry.class")
+    private static TestServiceRegistry sInstance;
+    @GuardedBy("TestServiceRegistry.class")
+    private MediaSessionService2 mService;
+    @GuardedBy("TestServiceRegistry.class")
+    private SyncHandler mHandler;
+    @GuardedBy("TestServiceRegistry.class")
+    private MediaLibrarySessionCallback mSessionCallback;
+    @GuardedBy("TestServiceRegistry.class")
+    private SessionServiceCallback mSessionServiceCallback;
+
+    /**
+     * Callback for session service's lifecyle (onCreate() / onDestroy())
+     */
+    public interface SessionServiceCallback {
+        void onCreated();
+        void onDestroyed();
+    }
+
+    public static TestServiceRegistry getInstance() {
+        synchronized (TestServiceRegistry.class) {
+            if (sInstance == null) {
+                sInstance = new TestServiceRegistry();
+            }
+            return sInstance;
+        }
+    }
+
+    public void setHandler(Handler handler) {
+        synchronized (TestServiceRegistry.class) {
+            mHandler = new SyncHandler(handler.getLooper());
+        }
+    }
+
+    public Handler getHandler() {
+        synchronized (TestServiceRegistry.class) {
+            return mHandler;
+        }
+    }
+
+    public void setSessionServiceCallback(SessionServiceCallback sessionServiceCallback) {
+        synchronized (TestServiceRegistry.class) {
+            mSessionServiceCallback = sessionServiceCallback;
+        }
+    }
+
+    public void setSessionCallback(MediaLibrarySessionCallback sessionCallback) {
+        synchronized (TestServiceRegistry.class) {
+            mSessionCallback = sessionCallback;
+        }
+    }
+
+    public MediaLibrarySessionCallback getSessionCallback() {
+        synchronized (TestServiceRegistry.class) {
+            return mSessionCallback;
+        }
+    }
+
+    public void setServiceInstance(MediaSessionService2 service) {
+        synchronized (TestServiceRegistry.class) {
+            if (mService != null) {
+                fail("Previous service instance is still running. Clean up manually to ensure"
+                        + " previoulsy running service doesn't break current test");
+            }
+            mService = service;
+            if (mSessionServiceCallback != null) {
+                mSessionServiceCallback.onCreated();
+            }
+        }
+    }
+
+    public MediaSessionService2 getServiceInstance() {
+        synchronized (TestServiceRegistry.class) {
+            return mService;
+        }
+    }
+
+    public void cleanUp() {
+        synchronized (TestServiceRegistry.class) {
+            if (mService != null) {
+                // TODO(jaewan): Remove this, and override SessionService#onDestroy() to do this
+                mService.getSession().close();
+                // stopSelf() would not kill service while the binder connection established by
+                // bindService() exists, and close() above will do the job instead.
+                // So stopSelf() isn't really needed, but just for sure.
+                mService.stopSelf();
+                mService = null;
+            }
+            if (mHandler != null) {
+                mHandler.removeCallbacksAndMessages(null);
+            }
+            mSessionCallback = null;
+            if (mSessionServiceCallback != null) {
+                mSessionServiceCallback.onDestroyed();
+                mSessionServiceCallback = null;
+            }
+        }
+    }
+}
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestUtils.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestUtils.java
new file mode 100644
index 0000000..3cd7f68
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestUtils.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.client;
+
+import static org.junit.Assert.assertTrue;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.core.util.ObjectsCompat;
+import androidx.media.DataSourceDesc;
+import androidx.media.MediaItem2;
+import androidx.media.MediaMetadata2;
+
+import java.io.FileDescriptor;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Utilities for tests.
+ */
+public final class TestUtils {
+    private static final int WAIT_TIME_MS = 1000;
+    private static final int WAIT_SERVICE_TIME_MS = 5000;
+
+    // Temporaily commenting out, since we don't have the Mock services yet.
+//    /**
+//     * Finds the session with id in this test package.
+//     *
+//     * @param context
+//     * @param id
+//     * @return
+//     */
+//    public static SessionToken2 getServiceToken(Context context, String id) {
+//        switch (id) {
+//            case MockMediaSessionService2.ID:
+//                return new SessionToken2(context, new ComponentName(
+//                        context.getPackageName(), MockMediaSessionService2.class.getName()));
+//            case MockMediaLibraryService2.ID:
+//                return new SessionToken2(context, new ComponentName(
+//                        context.getPackageName(), MockMediaLibraryService2.class.getName()));
+//        }
+//        fail("Unknown id=" + id);
+//        return null;
+//    }
+
+    /**
+     * Compares contents of two bundles.
+     *
+     * @param a a bundle
+     * @param b another bundle
+     * @return {@code true} if two bundles are the same. {@code false} otherwise. This may be
+     *     incorrect if any bundle contains a bundle.
+     */
+    public static boolean equals(Bundle a, Bundle b) {
+        return contains(a, b) && contains(b, a);
+    }
+
+    /**
+     * Checks whether a Bundle contains another bundle.
+     *
+     * @param a a bundle
+     * @param b another bundle
+     * @return {@code true} if a contains b. {@code false} otherwise. This may be incorrect if any
+     *      bundle contains a bundle.
+     */
+    public static boolean contains(Bundle a, Bundle b) {
+        if (a == b) {
+            return true;
+        }
+        if (a == null || b == null) {
+            return b == null;
+        }
+        if (!a.keySet().containsAll(b.keySet())) {
+            return false;
+        }
+        for (String key : b.keySet()) {
+            if (!ObjectsCompat.equals(a.get(key), b.get(key))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Create a playlist for testing purpose
+     * <p>
+     * Caller's method name will be used for prefix of each media item's media id.
+     *
+     * @param size list size
+     * @return the newly created playlist
+     */
+    public static List<MediaItem2> createPlaylist(int size) {
+        final List<MediaItem2> list = new ArrayList<>();
+        String caller = Thread.currentThread().getStackTrace()[1].getMethodName();
+        for (int i = 0; i < size; i++) {
+            list.add(new MediaItem2.Builder(MediaItem2.FLAG_PLAYABLE)
+                    .setMediaId(caller + "_item_" + (size + 1))
+                    .setDataSourceDesc(createDSD()).build());
+        }
+        return list;
+    }
+
+    /**
+     * Create a media item with the metadata for testing purpose.
+     *
+     * @return the newly created media item
+     * @see #createMetadata()
+     */
+    public static MediaItem2 createMediaItemWithMetadata() {
+        return new MediaItem2.Builder(MediaItem2.FLAG_PLAYABLE)
+                .setMetadata(createMetadata()).setDataSourceDesc(createDSD()).build();
+    }
+
+    /**
+     * Create a media metadata for testing purpose.
+     * <p>
+     * Caller's method name will be used for the media id.
+     *
+     * @return the newly created media item
+     */
+    public static MediaMetadata2 createMetadata() {
+        String mediaId = Thread.currentThread().getStackTrace()[1].getMethodName();
+        return new MediaMetadata2.Builder()
+                .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId).build();
+    }
+
+    private static DataSourceDesc createDSD() {
+        return new DataSourceDesc.Builder().setDataSource(new FileDescriptor()).build();
+    }
+
+    /**
+     * Create a bundle for testing purpose.
+     *
+     * @return the newly created bundle.
+     */
+    public static Bundle createTestBundle() {
+        Bundle bundle = new Bundle();
+        bundle.putString("test_key", "test_value");
+        return bundle;
+    }
+
+    /**
+     * Handler that always waits until the Runnable finishes.
+     */
+    public static class SyncHandler extends Handler {
+        public SyncHandler(Looper looper) {
+            super(looper);
+        }
+
+        public void postAndSync(final Runnable runnable) throws InterruptedException {
+            if (getLooper() == Looper.myLooper()) {
+                runnable.run();
+            } else {
+                final CountDownLatch latch = new CountDownLatch(1);
+                post(new Runnable() {
+                    @Override
+                    public void run() {
+                        runnable.run();
+                        latch.countDown();
+                    }
+                });
+                assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            }
+        }
+    }
+}
diff --git a/media/version-compat-tests/current/service/src/androidTest/AndroidManifest.xml b/media/version-compat-tests/current/service/src/androidTest/AndroidManifest.xml
index 13c22ae..0ca78fc 100644
--- a/media/version-compat-tests/current/service/src/androidTest/AndroidManifest.xml
+++ b/media/version-compat-tests/current/service/src/androidTest/AndroidManifest.xml
@@ -41,5 +41,12 @@
                 <action android:name="android.media.browse.MediaBrowserService"/>
             </intent-filter>
         </service>
+
+        <service android:name="androidx.media.test.service.TestHelperService">
+            <intent-filter>
+                <!-- Keep sync with TestHelperUtil.java -->
+                <action android:name="androidx.media.test.action.TEST_HELPER"/>
+            </intent-filter>
+        </service>
     </application>
 </manifest>
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java b/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
index 3519f2f..82e55ed 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
@@ -85,6 +85,7 @@
 import android.os.Parcel;
 import android.os.ResultReceiver;
 import android.os.SystemClock;
+import android.support.mediacompat.testlib.util.PollingCheck;
 import android.support.test.filters.MediumTest;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
@@ -722,7 +723,80 @@
 
     @Test
     @SmallTest
-    public void testVolumeControl() throws Exception {
+    public void testSetVolumeWithLocalVolume() throws Exception {
+        if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
+            // This test is not eligible for this device.
+            return;
+        }
+
+        // Here, we intentionally choose STREAM_ALARM in order not to consider
+        // 'Do Not Disturb' or 'Volume limit'.
+        final int stream = AudioManager.STREAM_ALARM;
+        final int maxVolume = mAudioManager.getStreamMaxVolume(stream);
+        final int minVolume = 0;
+        if (maxVolume <= minVolume) {
+            return;
+        }
+
+        mSession.setPlaybackToLocal(stream);
+
+        final int originalVolume = mAudioManager.getStreamVolume(stream);
+        final int targetVolume = originalVolume == minVolume
+                ? originalVolume + 1 : originalVolume - 1;
+
+        callMediaControllerMethod(SET_VOLUME_TO, targetVolume, getContext(),
+                mSession.getSessionToken());
+        new PollingCheck(TIME_OUT_MS) {
+            @Override
+            protected boolean check() {
+                return targetVolume == mAudioManager.getStreamVolume(stream);
+            }
+        }.run();
+
+        // Set back to original volume.
+        mAudioManager.setStreamVolume(stream, originalVolume, 0 /* flags */);
+    }
+
+    @Test
+    @SmallTest
+    public void testAdjustVolumeWithLocalVolume() throws Exception {
+        if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
+            // This test is not eligible for this device.
+            return;
+        }
+
+        // Here, we intentionally choose STREAM_ALARM in order not to consider
+        // 'Do Not Disturb' or 'Volume limit'.
+        final int stream = AudioManager.STREAM_ALARM;
+        final int maxVolume = mAudioManager.getStreamMaxVolume(stream);
+        final int minVolume = 0;
+        if (maxVolume <= minVolume) {
+            return;
+        }
+
+        mSession.setPlaybackToLocal(stream);
+
+        final int originalVolume = mAudioManager.getStreamVolume(stream);
+        final int direction = originalVolume == minVolume
+                ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER;
+        final int targetVolume = originalVolume + direction;
+
+        callMediaControllerMethod(ADJUST_VOLUME, direction, getContext(),
+                mSession.getSessionToken());
+        new PollingCheck(TIME_OUT_MS) {
+            @Override
+            protected boolean check() {
+                return targetVolume == mAudioManager.getStreamVolume(stream);
+            }
+        }.run();
+
+        // Set back to original volume.
+        mAudioManager.setStreamVolume(stream, originalVolume, 0 /* flags */);
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoteVolumeControl() throws Exception {
         if (android.os.Build.VERSION.SDK_INT < 27) {
             // This test causes an Exception on System UI in API < 27.
             return;
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MockPlayer.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MockPlayer.java
new file mode 100644
index 0000000..14f1a96
--- /dev/null
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MockPlayer.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.service;
+
+import androidx.annotation.NonNull;
+import androidx.collection.ArrayMap;
+import androidx.media.AudioAttributesCompat;
+import androidx.media.DataSourceDesc;
+import androidx.media.MediaPlayerInterface;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+
+/**
+ * A mock implementation of {@link MediaPlayerInterface} for testing.
+ */
+public class MockPlayer extends MediaPlayerInterface {
+    public final CountDownLatch mCountDownLatch;
+
+    public boolean mPlayCalled;
+    public boolean mPauseCalled;
+    public boolean mResetCalled;
+    public boolean mPrepareCalled;
+    public boolean mSeekToCalled;
+    public boolean mSetPlaybackSpeedCalled;
+    public long mSeekPosition;
+    public long mCurrentPosition;
+    public long mBufferedPosition;
+    public float mPlaybackSpeed = 1.0f;
+    public @PlayerState int mLastPlayerState;
+    public @BuffState int mLastBufferingState;
+    public long mDuration;
+
+    public ArrayMap<PlayerEventCallback, Executor> mCallbacks = new ArrayMap<>();
+
+    private AudioAttributesCompat mAudioAttributes;
+
+    public MockPlayer(int count) {
+        mCountDownLatch = (count > 0) ? new CountDownLatch(count) : null;
+    }
+
+    @Override
+    public void close() {
+        // no-op
+    }
+
+    @Override
+    public void reset() {
+        mResetCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void play() {
+        mPlayCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void pause() {
+        mPauseCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void prepare() {
+        mPrepareCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void seekTo(long pos) {
+        mSeekToCalled = true;
+        mSeekPosition = pos;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void skipToNext() {
+        // No-op. This skipToNext() means 'skip to next item in the setNextDataSources()'
+    }
+
+    @Override
+    public int getPlayerState() {
+        return mLastPlayerState;
+    }
+
+    @Override
+    public long getCurrentPosition() {
+        return mCurrentPosition;
+    }
+
+    @Override
+    public long getBufferedPosition() {
+        return mBufferedPosition;
+    }
+
+    @Override
+    public float getPlaybackSpeed() {
+        return mPlaybackSpeed;
+    }
+
+    @Override
+    public int getBufferingState() {
+        return mLastBufferingState;
+    }
+
+    @Override
+    public long getDuration() {
+        return mDuration;
+    }
+
+    @Override
+    public void registerPlayerEventCallback(@NonNull Executor executor,
+            @NonNull PlayerEventCallback callback) {
+        if (callback == null || executor == null) {
+            throw new IllegalArgumentException("callback=" + callback + " executor=" + executor);
+        }
+        mCallbacks.put(callback, executor);
+    }
+
+    @Override
+    public void unregisterPlayerEventCallback(@NonNull PlayerEventCallback callback) {
+        mCallbacks.remove(callback);
+    }
+
+    public void notifyPlaybackState(final int state) {
+        mLastPlayerState = state;
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onPlayerStateChanged(MockPlayer.this, state);
+                }
+            });
+        }
+    }
+
+    public void notifyCurrentDataSourceChanged(final DataSourceDesc dsd) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onCurrentDataSourceChanged(MockPlayer.this, dsd);
+                }
+            });
+        }
+    }
+
+    public void notifyMediaPrepared(final DataSourceDesc dsd) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onMediaPrepared(MockPlayer.this, dsd);
+                }
+            });
+        }
+    }
+
+    public void notifyBufferingStateChanged(final DataSourceDesc dsd,
+            final @BuffState int buffState) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onBufferingStateChanged(MockPlayer.this, dsd, buffState);
+                }
+            });
+        }
+    }
+
+    public void notifyPlaybackSpeedChanged(final float speed) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onPlaybackSpeedChanged(MockPlayer.this, speed);
+                }
+            });
+        }
+    }
+
+    public void notifySeekCompleted(final long position) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onSeekCompleted(MockPlayer.this, position);
+                }
+            });
+        }
+    }
+
+    public void notifyError(int what) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            // TODO: Uncomment or remove
+            //executor.execute(() -> callback.onError(null, what, 0));
+        }
+    }
+
+    @Override
+    public void setAudioAttributes(AudioAttributesCompat attributes) {
+        mAudioAttributes = attributes;
+    }
+
+    @Override
+    public AudioAttributesCompat getAudioAttributes() {
+        return mAudioAttributes;
+    }
+
+    @Override
+    public void setDataSource(@NonNull DataSourceDesc dsd) {
+        // TODO: Implement this
+    }
+
+    @Override
+    public void setNextDataSource(@NonNull DataSourceDesc dsd) {
+        // TODO: Implement this
+    }
+
+    @Override
+    public void setNextDataSources(@NonNull List<DataSourceDesc> dsds) {
+        // TODO: Implement this
+    }
+
+    @Override
+    public DataSourceDesc getCurrentDataSource() {
+        // TODO: Implement this
+        return null;
+    }
+
+    @Override
+    public void loopCurrent(boolean loop) {
+        // TODO: implement this
+    }
+
+    @Override
+    public void setPlaybackSpeed(float speed) {
+        mSetPlaybackSpeedCalled = true;
+        mPlaybackSpeed = speed;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void setPlayerVolume(float volume) {
+        // TODO: implement this
+    }
+
+    @Override
+    public float getPlayerVolume() {
+        // TODO: implement this
+        return -1;
+    }
+}
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/TestHelperService.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/TestHelperService.java
new file mode 100644
index 0000000..8df40b9
--- /dev/null
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/TestHelperService.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.service;
+
+import static androidx.media.test.lib.TestHelperUtil.ACTION_TEST_HELPER;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.support.mediacompat.testlib.ITestHelperForServiceApp;
+
+import androidx.media.MediaSession2;
+
+/**
+ * A Service that creates {@link MediaSession2} and calls its methods according to the client app's
+ * requests.
+ */
+public class TestHelperService extends Service {
+
+    MediaSession2 mSession2;
+    ServiceBinder mBinder;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mBinder = new ServiceBinder();
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        if (ACTION_TEST_HELPER.equals(intent.getAction())) {
+            return mBinder;
+        }
+        return null;
+    }
+
+    private class ServiceBinder extends ITestHelperForServiceApp.Stub {
+        @Override
+        public Bundle getSessionToken2(String testName) throws RemoteException {
+            if (Looper.myLooper() == null) {
+                Looper.prepare();
+            }
+
+            // TODO: Create the right session according to testName, and return its token here.
+            mSession2 = new MediaSession2.Builder(TestHelperService.this)
+                    .setPlayer(new MockPlayer(0))
+                    .build();
+            return mSession2.getToken().toBundle();
+        }
+
+        @Override
+        public void callMediaSession2Method(int method, Bundle args) throws RemoteException {
+            // TODO: Call appropriate method (mSession2.~~~)
+        }
+
+        // TODO: call(MediaPlayerBase/Agent)Method may be also needed.
+        // If so, do not create mPlayer/mAgent, but get them from mSession.getPlayer()/getAgent().
+    }
+}
diff --git a/media/version-compat-tests/lib/src/main/aidl/android/support/mediacompat/testlib/ITestHelperForServiceApp.aidl b/media/version-compat-tests/lib/src/main/aidl/android/support/mediacompat/testlib/ITestHelperForServiceApp.aidl
new file mode 100644
index 0000000..d12e8ca
--- /dev/null
+++ b/media/version-compat-tests/lib/src/main/aidl/android/support/mediacompat/testlib/ITestHelperForServiceApp.aidl
@@ -0,0 +1,25 @@
+/*
+ * 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 android.support.mediacompat.testlib;
+
+import android.os.Bundle;
+
+interface ITestHelperForServiceApp {
+
+    Bundle getSessionToken2(String testName);
+    void callMediaSession2Method(int method, in Bundle args);
+}
diff --git a/media/version-compat-tests/lib/src/main/java/androidx/media/test/lib/TestHelperUtil.java b/media/version-compat-tests/lib/src/main/java/androidx/media/test/lib/TestHelperUtil.java
new file mode 100644
index 0000000..9cc68b5
--- /dev/null
+++ b/media/version-compat-tests/lib/src/main/java/androidx/media/test/lib/TestHelperUtil.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.lib;
+
+import android.content.ComponentName;
+import android.support.mediacompat.testlib.util.IntentUtil;
+
+/**
+ * Methods and constants used for calling methods between client and service apps by using
+ * TestHelper/TestHelperService.
+ */
+public class TestHelperUtil {
+    public static final ComponentName SERVICE_TEST_HELPER_COMPONENT_NAME = new ComponentName(
+            IntentUtil.SERVICE_PACKAGE_NAME, "androidx.media.test.service.TestHelperService");
+
+    public static final String ACTION_TEST_HELPER = "androidx.media.action.test.TEST_HELPER";
+
+    private TestHelperUtil() {
+    }
+}
diff --git a/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java b/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java
index f30163d..0033665 100644
--- a/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java
+++ b/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java
@@ -150,7 +150,7 @@
     public AsyncPagedListDiffer(@NonNull RecyclerView.Adapter adapter,
             @NonNull DiffUtil.ItemCallback<T> diffCallback) {
         mUpdateCallback = new AdapterListUpdateCallback(adapter);
-        mConfig = new AsyncDifferConfig.Builder<T>(diffCallback).build();
+        mConfig = new AsyncDifferConfig.Builder<>(diffCallback).build();
     }
 
     @SuppressWarnings("WeakerAccess")
@@ -227,6 +227,7 @@
      *
      * @param pagedList The new PagedList.
      */
+    @SuppressWarnings("ReferenceEquality")
     public void submitList(final PagedList<T> pagedList) {
         if (pagedList != null) {
             if (mPagedList == null && mSnapshot == null) {
diff --git a/palette/ktx/OWNERS b/palette/ktx/OWNERS
new file mode 100644
index 0000000..e450f4c
--- /dev/null
+++ b/palette/ktx/OWNERS
@@ -0,0 +1 @@
+jakew@google.com
diff --git a/palette/ktx/build.gradle b/palette/ktx/build.gradle
new file mode 100644
index 0000000..e5d5d43
--- /dev/null
+++ b/palette/ktx/build.gradle
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+    id("org.jetbrains.kotlin.android")
+}
+
+android {
+    buildTypes {
+        debug {
+            testCoverageEnabled = false // Breaks Kotlin compiler.
+        }
+    }
+}
+
+dependencies {
+    api(project(":palette"))
+    api(KOTLIN_STDLIB)
+    androidTestImplementation(JUNIT)
+    androidTestImplementation(TEST_RUNNER_TMP, libs.exclude_for_espresso)
+}
+
+supportLibrary {
+    name = "Palette Kotlin Extensions"
+    publish = true
+    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenGroup = LibraryGroups.PALETTE
+    inceptionYear = "2018"
+    description = "Kotlin extensions for 'palette' artifact"
+}
diff --git a/palette/ktx/src/androidTest/java/androidx/palette/graphics/PaletteTest.kt b/palette/ktx/src/androidTest/java/androidx/palette/graphics/PaletteTest.kt
new file mode 100644
index 0000000..c108493
--- /dev/null
+++ b/palette/ktx/src/androidTest/java/androidx/palette/graphics/PaletteTest.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.palette.graphics
+
+import android.graphics.Bitmap
+import android.graphics.Bitmap.Config.ARGB_8888
+import android.graphics.Canvas
+import android.graphics.Color.RED
+import androidx.palette.graphics.Target.VIBRANT
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertSame
+import org.junit.Test
+
+class PaletteTest {
+    @Test fun bitmapBuild() {
+        val bitmap = Bitmap.createBitmap(10, 10, ARGB_8888)
+        // There's no easy way to test that the palette was created from our Bitmap.
+        assertNotNull(bitmap.buildPalette())
+    }
+
+    @Test fun operatorGet() {
+        val bitmap = Bitmap.createBitmap(10, 10, ARGB_8888).apply {
+            Canvas(this).drawColor(RED)
+        }
+        val palette = Palette.from(bitmap).generate()
+        assertSame(palette.getSwatchForTarget(VIBRANT), palette[VIBRANT])
+    }
+}
diff --git a/palette/ktx/src/main/AndroidManifest.xml b/palette/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..f25fda5
--- /dev/null
+++ b/palette/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest package="androidx.palette.ktx"/>
diff --git a/palette/ktx/src/main/java/androidx/palette/graphics/Palette.kt b/palette/ktx/src/main/java/androidx/palette/graphics/Palette.kt
new file mode 100644
index 0000000..58da09a
--- /dev/null
+++ b/palette/ktx/src/main/java/androidx/palette/graphics/Palette.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.palette.graphics
+
+import android.graphics.Bitmap
+
+/**
+ * Create a [Palette.Builder] from this bitmap.
+ *
+ * @see Palette.from
+ */
+inline fun Bitmap.buildPalette() = Palette.Builder(this)
+
+/**
+ * Returns the selected swatch for the given target from the palette, or `null` if one
+ * could not be found.
+ *
+ * @see Palette.getSwatchForTarget
+ */
+inline operator fun Palette.get(target: Target): Palette.Swatch? = getSwatchForTarget(target)
diff --git a/persistence/db/ktx/OWNERS b/persistence/db/ktx/OWNERS
new file mode 100644
index 0000000..e450f4c
--- /dev/null
+++ b/persistence/db/ktx/OWNERS
@@ -0,0 +1 @@
+jakew@google.com
diff --git a/persistence/db/ktx/build.gradle b/persistence/db/ktx/build.gradle
new file mode 100644
index 0000000..676445b
--- /dev/null
+++ b/persistence/db/ktx/build.gradle
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SupportLibraryExtension
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    api(project(":sqlite:sqlite"))
+    api(KOTLIN_STDLIB)
+
+    testImplementation(JUNIT)
+    testImplementation(MOCKITO_CORE)
+}
+
+supportLibrary {
+    name = "Android DB KTX"
+    publish = true
+    mavenVersion = LibraryVersions.ROOM
+    mavenGroup = LibraryGroups.PERSISTENCE
+    inceptionYear = "2018"
+    description = "Kotlin extensions for DB"
+    url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/persistence/db/ktx/src/main/AndroidManifest.xml b/persistence/db/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..f4d66ba
--- /dev/null
+++ b/persistence/db/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<!--
+  ~ Copyright (C) 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest package="androidx.sqlite.db.ktx"/>
diff --git a/persistence/db/ktx/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.kt b/persistence/db/ktx/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.kt
new file mode 100644
index 0000000..ba7b338
--- /dev/null
+++ b/persistence/db/ktx/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+package androidx.sqlite.db
+
+/**
+ * Run [body] in a transaction marking it as successful if it completes without exception.
+ *
+ * @param exclusive Run in `EXCLUSIVE` mode when true, `IMMEDIATE` mode otherwise.
+ */
+inline fun <T> SupportSQLiteDatabase.transaction(
+    exclusive: Boolean = true,
+    body: SupportSQLiteDatabase.() -> T
+): T {
+    if (exclusive) {
+        beginTransaction()
+    } else {
+        beginTransactionNonExclusive()
+    }
+    try {
+        val result = body()
+        setTransactionSuccessful()
+        return result
+    } finally {
+        endTransaction()
+    }
+}
diff --git a/persistence/db/ktx/src/test/java/androidx/sqlite/db/SupportSQLiteDatabaseTest.kt b/persistence/db/ktx/src/test/java/androidx/sqlite/db/SupportSQLiteDatabaseTest.kt
new file mode 100644
index 0000000..76698d3
--- /dev/null
+++ b/persistence/db/ktx/src/test/java/androidx/sqlite/db/SupportSQLiteDatabaseTest.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+
+package androidx.sqlite.db
+
+import org.junit.Assert.fail
+import org.junit.Test
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+class SupportSQLiteDatabaseTest {
+    @Test fun exclusiveDefault() {
+        val db = mock(SupportSQLiteDatabase::class.java)
+        db.transaction {}
+        verify(db).beginTransaction()
+    }
+
+    @Test fun exclusiveFalse() {
+        val db = mock(SupportSQLiteDatabase::class.java)
+        db.transaction(exclusive = false) {}
+        verify(db).beginTransactionNonExclusive()
+    }
+
+    @Test fun exclusiveTrue() {
+        val db = mock(SupportSQLiteDatabase::class.java)
+        db.transaction(exclusive = true) {}
+        verify(db).beginTransaction()
+    }
+
+    @Test fun bodyNormalCallsSuccessAndEnd() {
+        val db = mock(SupportSQLiteDatabase::class.java)
+        db.transaction {}
+        verify(db).setTransactionSuccessful()
+        verify(db).endTransaction()
+    }
+
+    @Suppress("UNREACHABLE_CODE") // A programming error might not invoke the lambda.
+    @Test fun bodyThrowsDoesNotCallSuccess() {
+        val db = mock(SupportSQLiteDatabase::class.java)
+        try {
+            db.transaction {
+                throw IllegalStateException()
+            }
+            fail()
+        } catch (e: IllegalStateException) {
+        }
+        verify(db, times(0)).setTransactionSuccessful()
+        verify(db).endTransaction()
+    }
+}
diff --git a/preference/src/main/java/androidx/preference/PreferenceCategory.java b/preference/src/main/java/androidx/preference/PreferenceCategory.java
index 3585bd9..d00d959 100644
--- a/preference/src/main/java/androidx/preference/PreferenceCategory.java
+++ b/preference/src/main/java/androidx/preference/PreferenceCategory.java
@@ -20,6 +20,7 @@
 import android.util.AttributeSet;
 
 import androidx.core.content.res.TypedArrayUtils;
+import androidx.core.os.BuildCompat;
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat;
 
@@ -66,21 +67,30 @@
     }
 
     @Override
+    public void onBindViewHolder(PreferenceViewHolder holder) {
+        super.onBindViewHolder(holder);
+        if (BuildCompat.isAtLeastP()) {
+            holder.itemView.setAccessibilityHeading(true);
+        }
+    }
+
+    @Override
     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfoCompat info) {
         super.onInitializeAccessibilityNodeInfo(info);
+        if (!BuildCompat.isAtLeastP()) {
+            CollectionItemInfoCompat existingItemInfo = info.getCollectionItemInfo();
+            if (existingItemInfo == null) {
+                return;
+            }
 
-        CollectionItemInfoCompat existingItemInfo = info.getCollectionItemInfo();
-        if (existingItemInfo == null) {
-            return;
+            final CollectionItemInfoCompat newItemInfo = CollectionItemInfoCompat.obtain(
+                    existingItemInfo.getRowIndex(),
+                    existingItemInfo.getRowSpan(),
+                    existingItemInfo.getColumnIndex(),
+                    existingItemInfo.getColumnSpan(),
+                    true /* heading */,
+                    existingItemInfo.isSelected());
+            info.setCollectionItemInfo(newItemInfo);
         }
-
-        final CollectionItemInfoCompat newItemInfo = CollectionItemInfoCompat.obtain(
-                existingItemInfo.getRowIndex(),
-                existingItemInfo.getRowSpan(),
-                existingItemInfo.getColumnIndex(),
-                existingItemInfo.getColumnSpan(),
-                true /* heading */,
-                existingItemInfo.isSelected());
-        info.setCollectionItemInfo(newItemInfo);
     }
 }
diff --git a/room/common/src/main/java/androidx/room/ForeignKey.java b/room/common/src/main/java/androidx/room/ForeignKey.java
index 847941a..54b9252 100644
--- a/room/common/src/main/java/androidx/room/ForeignKey.java
+++ b/room/common/src/main/java/androidx/room/ForeignKey.java
@@ -13,11 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package androidx.room;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 import androidx.annotation.IntDef;
 
+import java.lang.annotation.Retention;
+
 /**
  * Declares a foreign key on another {@link Entity}.
  * <p>
@@ -161,6 +164,7 @@
      * {@link #onUpdate()}.
      */
     @IntDef({NO_ACTION, RESTRICT, SET_NULL, SET_DEFAULT, CASCADE})
+    @Retention(SOURCE)
     @interface Action {
     }
 }
diff --git a/samples/SupportCarDemos/src/main/AndroidManifest.xml b/samples/SupportCarDemos/src/main/AndroidManifest.xml
index 3de2cac..c0c2d83 100644
--- a/samples/SupportCarDemos/src/main/AndroidManifest.xml
+++ b/samples/SupportCarDemos/src/main/AndroidManifest.xml
@@ -139,6 +139,17 @@
             <meta-data android:name="android.support.PARENT_ACTIVITY"
                        android:value=".SupportCarDemoActivity" />
         </activity>
+
+        <activity android:name=".AppBarActivity"
+            android:label="App Bar"
+            android:parentActivityName=".SupportCarDemoActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+            <meta-data android:name="android.support.PARENT_ACTIVITY"
+                android:value=".SupportCarDemoActivity" />
+        </activity>
     </application>
 </manifest>
 
diff --git a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/AppBarActivity.java b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/AppBarActivity.java
new file mode 100644
index 0000000..3919c5a
--- /dev/null
+++ b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/AppBarActivity.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.androidx.car;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.Menu;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.car.widget.ListItem;
+import androidx.car.widget.ListItemAdapter;
+import androidx.car.widget.ListItemProvider;
+import androidx.car.widget.PagedListView;
+import androidx.car.widget.TextListItem;
+
+/**
+ * Demo activity for creating {@code App Bar} with {@link Toolbar}.
+ */
+public class AppBarActivity extends AppCompatActivity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_app_bar);
+        Toolbar toolbar = findViewById(R.id.car_toolbar);
+        setSupportActionBar(toolbar);
+        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+        getSupportActionBar().setTitle("Title");
+        getSupportActionBar().setSubtitle("Subtitle");
+
+        setUpList();
+    }
+
+    private void setUpList() {
+        PagedListView list = findViewById(R.id.list);
+        list.setAdapter(new ListItemAdapter(this, new SampleProvider(this, 50)));
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.demo_menu, menu);
+        return super.onCreateOptionsMenu(menu);
+    }
+
+    private static class SampleProvider extends ListItemProvider {
+
+        private int mCount;
+        private Context mContext;
+
+        SampleProvider(Context context, int count) {
+            mContext = context;
+            mCount = count;
+        }
+
+        @Override
+        public ListItem get(int position) {
+            if (position < 0 || position >= mCount) {
+                throw new IndexOutOfBoundsException();
+            }
+            TextListItem item = new TextListItem(mContext);
+            item.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon, false);
+            item.setTitle("title");
+            return item;
+        }
+
+        @Override
+        public int size() {
+            return mCount;
+        }
+    }
+}
diff --git a/samples/SupportCarDemos/src/main/res/layout/activity_app_bar.xml b/samples/SupportCarDemos/src/main/res/layout/activity_app_bar.xml
new file mode 100644
index 0000000..74dcbc3
--- /dev/null
+++ b/samples/SupportCarDemos/src/main/res/layout/activity_app_bar.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+    <androidx.appcompat.widget.Toolbar
+        android:id="@+id/car_toolbar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="@color/car_card" />
+
+    <androidx.car.widget.PagedListView
+        android:id="@+id/list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:gutter="both" />
+</LinearLayout>
diff --git a/samples/SupportCarDemos/src/main/res/menu/demo_menu.xml b/samples/SupportCarDemos/src/main/res/menu/demo_menu.xml
new file mode 100644
index 0000000..4b52bd9
--- /dev/null
+++ b/samples/SupportCarDemos/src/main/res/menu/demo_menu.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <item
+        android:id="@+id/action_icon"
+        android:icon="@drawable/pressed_icon"
+        android:title="Action1 with icon"
+        app:showAsAction="always|withText"/>
+
+    <item
+        android:id="@+id/action_overflow"
+        android:title="Action2"
+        app:showAsAction="always|withText"/>
+
+    <item
+        android:id="@+id/action_overflow"
+        android:title="Action3"
+        app:showAsAction="never"/>
+</menu>
\ No newline at end of file
diff --git a/samples/SupportMediaDemos/build.gradle b/samples/SupportMediaDemos/build.gradle
index bf23177..fecf2a5 100644
--- a/samples/SupportMediaDemos/build.gradle
+++ b/samples/SupportMediaDemos/build.gradle
@@ -25,7 +25,7 @@
 
 android {
     defaultConfig {
-        minSdkVersion 19
-        targetSdkVersion 26
+        minSdkVersion = 19
+        targetSdkVersion = 26
     }
 }
diff --git a/samples/SupportMediaDemos/src/main/AndroidManifest.xml b/samples/SupportMediaDemos/src/main/AndroidManifest.xml
index 267eafd..b686fcd 100644
--- a/samples/SupportMediaDemos/src/main/AndroidManifest.xml
+++ b/samples/SupportMediaDemos/src/main/AndroidManifest.xml
@@ -15,14 +15,17 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     package="com.example.androidx.media">
+
+    <uses-sdk tools:overrideLibrary="androidx.media.widget" />
+
     <application android:label="Video View Test">
 
         <!-- Video Selection Activity -->
         <activity android:name="VideoSelector"
-            android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
-            android:configChanges="orientation|screenSize"
-            >
+            android:theme="@style/Theme.AppCompat"
+            android:configChanges="orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
@@ -31,8 +34,8 @@
 
         <!-- Video playback activity -->
         <activity android:name="VideoViewTest"
-            android:configChanges="orientation|screenSize"
-            >
+            android:theme="@style/Theme.AppCompat"
+            android:configChanges="orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW"/>
             </intent-filter>
diff --git a/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoViewTest.java b/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoViewTest.java
index d640303..2289769 100644
--- a/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoViewTest.java
+++ b/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoViewTest.java
@@ -29,6 +29,7 @@
 import android.support.v4.media.session.PlaybackStateCompat;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
@@ -36,6 +37,7 @@
 import android.view.WindowManager;
 import android.widget.Toast;
 
+import androidx.fragment.app.FragmentActivity;
 import androidx.media.widget.MediaControlView2;
 import androidx.media.widget.VideoView2;
 
@@ -43,7 +45,7 @@
  * Test application for VideoView2/MediaControlView2
  */
 @SuppressLint("NewApi")
-public class VideoViewTest extends Activity {
+public class VideoViewTest extends FragmentActivity {
     public static final String LOOPING_EXTRA_NAME =
             "com.example.androidx.media.VideoViewTest.IsLooping";
     public static final String USE_TEXTURE_VIEW_EXTRA_NAME =
@@ -71,6 +73,7 @@
         setContentView(R.layout.video_activity);
 
         mVideoView = findViewById(R.id.video_view);
+        mVideoView.setActivity(this);
 
         String errorString = null;
         Intent intent = getIntent();
@@ -82,12 +85,11 @@
             if (mUseTextureView) {
                 mVideoView.setViewType(VideoView2.VIEW_TYPE_TEXTUREVIEW);
             }
-
-            mVideoView.setFullScreenRequestListener(new FullScreenRequestListener());
             mVideoView.setVideoUri(contentUri);
 
             mMediaControlView = new MediaControlView2(this);
             mVideoView.setMediaControlView2(mMediaControlView, 2000);
+            mMediaControlView.setOnFullScreenListener(new FullScreenListener());
         }
         if (errorString != null) {
             showErrorDialog(errorString);
@@ -179,9 +181,10 @@
         }
     };
 
-    private class FullScreenRequestListener implements VideoView2.OnFullScreenRequestListener {
+    private class FullScreenListener
+            implements MediaControlView2.OnFullScreenListener {
         @Override
-        public void onFullScreenRequest(View view, boolean fullScreen) {
+        public void onFullScreen(View view, boolean fullScreen) {
             // TODO: Remove bottom controls after adding back button functionality.
             if (mPrevHeight == 0 && mPrevWidth == 0) {
                 ViewGroup.LayoutParams params = mVideoView.getLayoutParams();
@@ -223,6 +226,7 @@
     public static class MyVideoView extends VideoView2 {
         private float mDX;
         private float mDY;
+        private Activity mActivity;
 
         public MyVideoView(Context context) {
             super(context);
@@ -255,6 +259,18 @@
             }
             return super.onTouchEvent(ev);
         }
+
+        @Override
+        public boolean onKeyDown(int keyCode, KeyEvent event)  {
+            if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
+                mActivity.finish();
+            }
+            return true;
+        }
+
+        public void setActivity(Activity activity) {
+            mActivity = activity;
+        }
     }
 
     @Override
diff --git a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java
index 0beb745..afa5415 100644
--- a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java
+++ b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java
@@ -65,9 +65,27 @@
     public static final String ACTION_TOAST_RANGE_VALUE =
             "com.example.androidx.slice.action.TOAST_RANGE_VALUE";
 
-    public static final String[] URI_PATHS = {"message", "wifi", "note", "ride", "toggle",
-            "toggle2", "contact", "gallery", "weather", "reservation", "loadlist", "loadgrid",
-            "inputrange", "range", "contact2", "subscription"};
+    public static final String[] URI_PATHS = {
+            "message",
+            "wifi",
+            "note",
+            "ride",
+            "toggle",
+            "toggle2",
+            "toggletester",
+            "contact",
+            "contact2",
+            "gallery",
+            "gallery2",
+            "weather",
+            "reservation",
+            "loadlist",
+            "loadgrid",
+            "inputrange",
+            "range",
+            "subscription",
+            "singleitems",
+    };
 
     /**
      * @return Uri with the provided path
@@ -116,12 +134,16 @@
                 return createCustomToggleSlice(sliceUri);
             case "/toggle2":
                 return createTwoCustomToggleSlices(sliceUri);
+            case "/toggletester":
+                return createdToggleTesterSlice(sliceUri);
             case "/contact":
                 return createContact(sliceUri);
             case "/contact2":
                 return createContact2(sliceUri);
             case "/gallery":
-                return createGallery(sliceUri);
+                return createGallery(sliceUri, true /* showHeader */);
+            case "/gallery2":
+                return createGallery(sliceUri, false /* showHeader */);
             case "/weather":
                 return createWeather(sliceUri);
             case "/reservation":
@@ -136,6 +158,8 @@
                 return createDownloadProgressRange(sliceUri);
             case "/subscription":
                 return createCatSlice(sliceUri, false /* customSeeMore */);
+            case "/singleitems":
+                return createSingleSlice(sliceUri);
         }
         throw new IllegalArgumentException("Unknown uri " + sliceUri);
     }
@@ -181,58 +205,41 @@
                 .build();
     }
 
-    private Slice createGallery(Uri sliceUri) {
+    private Slice createGallery(Uri sliceUri, boolean showHeader) {
         SliceAction primaryAction = new SliceAction(
                 getBroadcastIntent(ACTION_TOAST, "open photo album"),
                 IconCompat.createWithResource(getContext(), R.drawable.slices_1),
                 LARGE_IMAGE,
                 "Open photo album");
-        return new ListBuilder(getContext(), sliceUri, INFINITY)
-                .setColor(0xff4285F4)
-                .addRow(b -> b
-                        .setTitle("Family trip to Hawaii")
-                        .setSubtitle("Sep 30, 2017 - Oct 2, 2017")
-                        .setPrimaryAction(primaryAction))
-                .addAction(new SliceAction(
-                        getBroadcastIntent(ACTION_TOAST, "cast photo album"),
-                        IconCompat.createWithResource(getContext(), R.drawable.ic_cast),
-                        "Cast photo album"))
-                .addAction(new SliceAction(
-                        getBroadcastIntent(ACTION_TOAST, "share photo album"),
-                        IconCompat.createWithResource(getContext(), R.drawable.ic_share),
-                        "Share photo album"))
-                .addGridRow(b -> b
-                        .addCell(cb -> cb
-                                .addImage(IconCompat.createWithResource(getContext(),
-                                        R.drawable.slices_1),
-                                        LARGE_IMAGE))
-                        .addCell(cb -> cb
-                                .addImage(IconCompat.createWithResource(getContext(),
-                                        R.drawable.slices_2),
-                                        LARGE_IMAGE))
-                        .addCell(cb -> cb
-                                .addImage(IconCompat.createWithResource(getContext(),
-                                        R.drawable.slices_3),
-                                        LARGE_IMAGE))
-                        .addCell(cb -> cb
-                                .addImage(IconCompat.createWithResource(getContext(),
-                                        R.drawable.slices_4),
-                                        LARGE_IMAGE))
-                        .addCell(cb -> cb
-                                .addImage(IconCompat.createWithResource(getContext(),
-                                        R.drawable.slices_2),
-                                        LARGE_IMAGE))
-                        .addCell(cb -> cb
-                                .addImage(IconCompat.createWithResource(getContext(),
-                                        R.drawable.slices_3),
-                                        LARGE_IMAGE))
-                        .addCell(cb -> cb
-                                .addImage(IconCompat.createWithResource(getContext(),
-                                        R.drawable.slices_4),
-                                        LARGE_IMAGE))
-                        .setSeeMoreAction(getBroadcastIntent(ACTION_TOAST, "see your gallery"))
-                        .setContentDescription("Images from your trip to Hawaii"))
-                .build();
+        ListBuilder lb = new ListBuilder(getContext(), sliceUri, INFINITY)
+                .setAccentColor(0xff4285F4);
+        if (showHeader) {
+            lb.addRow(b -> b
+                    .setTitle("Family trip to Hawaii")
+                    .setSubtitle("Sep 30, 2017 - Oct 2, 2017")
+                    .setPrimaryAction(primaryAction))
+                    .addAction(new SliceAction(
+                            getBroadcastIntent(ACTION_TOAST, "cast photo album"),
+                            IconCompat.createWithResource(getContext(), R.drawable.ic_cast),
+                            "Cast photo album"))
+                    .addAction(new SliceAction(
+                            getBroadcastIntent(ACTION_TOAST, "share photo album"),
+                            IconCompat.createWithResource(getContext(), R.drawable.ic_share),
+                            "Share photo album"));
+        }
+        int[] galleryResId = new int[] {R.drawable.slices_1, R.drawable.slices_2,
+                R.drawable.slices_3, R.drawable.slices_4};
+        int imageCount = 7;
+        GridRowBuilder grb = new GridRowBuilder(lb);
+        for (int i = 0; i < imageCount; i++) {
+            IconCompat ic = IconCompat.createWithResource(getContext(),
+                    galleryResId[i % galleryResId.length]);
+            grb.addCell(cb -> cb.addImage(ic, LARGE_IMAGE));
+        }
+        grb.setPrimaryAction(primaryAction)
+                .setSeeMoreAction(getBroadcastIntent(ACTION_TOAST, "see your gallery"))
+                .setContentDescription("Images from your trip to Hawaii");
+        return lb.addGridRow(grb).build();
     }
 
     private Slice createCatSlice(Uri sliceUri, boolean customSeeMore) {
@@ -280,7 +287,7 @@
         ListBuilder b = new ListBuilder(getContext(), sliceUri, INFINITY);
         ListBuilder.RowBuilder rb = new ListBuilder.RowBuilder(b);
         GridRowBuilder gb = new GridRowBuilder(b);
-        return b.setColor(0xff3949ab)
+        return b.setAccentColor(0xff3949ab)
                 .addRow(rb
                         .setTitle("Mady Pitza")
                         .setSubtitle("Frequently contacted contact")
@@ -322,7 +329,7 @@
                 R.drawable.mady), SMALL_IMAGE, "Mady");
 
         return new ListBuilder(getContext(), sliceUri, INFINITY)
-                .setColor(0xff3949ab)
+                .setAccentColor(0xff3949ab)
                 .setHeader(b -> b
                         .setTitle("Mady Pitza")
                         .setSummary("Called " + lastCalledString)
@@ -377,7 +384,7 @@
     private Slice createNoteSlice(Uri sliceUri) {
         // TODO: Remote input.
         return new ListBuilder(getContext(), sliceUri, INFINITY)
-                .setColor(0xfff4b400)
+                .setAccentColor(0xfff4b400)
                 .addRow(b -> b.setTitle("Create new note"))
                 .addAction(new SliceAction(getBroadcastIntent(ACTION_TOAST, "create note"),
                         IconCompat.createWithResource(getContext(), R.drawable.ic_create),
@@ -393,7 +400,7 @@
 
     private Slice createReservationSlice(Uri sliceUri) {
         return new ListBuilder(getContext(), sliceUri, INFINITY)
-                .setColor(0xffFF5252)
+                .setAccentColor(0xffFF5252)
                 .setHeader(b -> b
                         .setTitle("Upcoming trip to Seattle")
                         .setSubtitle("Feb 1 - 19 | 2 guests"))
@@ -431,8 +438,8 @@
 
         SliceAction primaryAction = new SliceAction(getBroadcastIntent(ACTION_TOAST, "get ride"),
                 IconCompat.createWithResource(getContext(), R.drawable.ic_car), "Get Ride");
-        return new ListBuilder(getContext(), sliceUri, -TimeUnit.MINUTES.toMillis(2))
-                .setColor(0xff0F9D58)
+        return new ListBuilder(getContext(), sliceUri, TimeUnit.SECONDS.toMillis(10))
+                .setAccentColor(0xff0F9D58)
                 .setHeader(b -> b
                         .setTitle("Get ride")
                         .setSubtitle(headerSubtitle)
@@ -455,7 +462,7 @@
 
     private Slice createCustomToggleSlice(Uri sliceUri) {
         return new ListBuilder(getContext(), sliceUri, INFINITY)
-                .setColor(0xffff4081)
+                .setAccentColor(0xffff4081)
                 .addRow(b -> b
                         .setTitle("Custom toggle")
                         .setSubtitle("It can support two states")
@@ -468,7 +475,7 @@
 
     private Slice createTwoCustomToggleSlices(Uri sliceUri) {
         return new ListBuilder(getContext(), sliceUri, INFINITY)
-                .setColor(0xffff4081)
+                .setAccentColor(0xffff4081)
                 .addRow(b -> b
                         .setTitle("2 toggles")
                         .setSubtitle("each supports two states")
@@ -514,7 +521,7 @@
         String sliceCDString = wifiEnabled ? "Wifi connected to " + state
                 : "Wifi disconnected, 10 networks available";
         ListBuilder lb = new ListBuilder(getContext(), sliceUri, INFINITY)
-                .setColor(0xff4285f4)
+                .setAccentColor(0xff4285f4)
                 .setHeader(b -> b
                         .setTitle("Wi-fi")
                         .setSubtitle(state)
@@ -570,7 +577,7 @@
                 new SliceAction(getBroadcastIntent(ACTION_TOAST, "open star rating"),
                         icon, "Rate");
         return new ListBuilder(getContext(), sliceUri, INFINITY)
-                .setColor(0xffff4081)
+                .setAccentColor(0xffff4081)
                 .addInputRange(c -> c
                         .setTitle("Star rating")
                         .setSubtitle("Rate from 5 to 10 because it's weird")
@@ -590,7 +597,7 @@
                 new SliceAction(
                         getBroadcastIntent(ACTION_TOAST, "open download"), icon, "Download");
         return new ListBuilder(getContext(), sliceUri, INFINITY)
-                .setColor(0xffff4081)
+                .setAccentColor(0xffff4081)
                 .addRange(c -> c
                         .setTitle("Download progress")
                         .setSubtitle("Download is happening")
@@ -600,6 +607,116 @@
                 .build();
     }
 
+    private Slice createdToggleTesterSlice(Uri uri) {
+        IconCompat star = IconCompat.createWithResource(getContext(), R.drawable.toggle_star);
+        IconCompat icon = IconCompat.createWithResource(getContext(), R.drawable.ic_star_on);
+
+        SliceAction primaryAction = new SliceAction(
+                getBroadcastIntent(ACTION_TOAST, "primary action"), icon, "Primary action");
+        SliceAction toggleAction = new SliceAction(
+                getBroadcastIntent(ACTION_TOAST, "star note"), star, "Star note", false);
+        SliceAction toggleAction2 = new SliceAction(
+                getBroadcastIntent(ACTION_TOAST, "star note 2"), star, "Star note 2", true);
+        SliceAction toggleAction3 = new SliceAction(
+                getBroadcastIntent(ACTION_TOAST, "star note 3"), star, "Star note 3", false);
+
+        ListBuilder lb = new ListBuilder(getContext(), uri, INFINITY);
+
+        // Primary action toggle
+        ListBuilder.RowBuilder primaryToggle = new ListBuilder.RowBuilder(lb);
+        primaryToggle.setTitle("Primary action is a toggle")
+                .setPrimaryAction(toggleAction);
+
+        // End toggle + normal primary action
+        ListBuilder.RowBuilder endToggle = new ListBuilder.RowBuilder(lb);
+        endToggle.setTitle("Only end toggles")
+                .setSubtitle("Normal primary action")
+                .setPrimaryAction(primaryAction)
+                .addEndItem(toggleAction)
+                .addEndItem(toggleAction2);
+
+        // Start toggle + normal primary
+        ListBuilder.RowBuilder startToggle = new ListBuilder.RowBuilder(lb);
+        startToggle.setTitle("One start toggle")
+                .setTitleItem(toggleAction)
+                .setSubtitle("Normal primary action")
+                .setPrimaryAction(primaryAction);
+
+        // Start + end toggles + normal primary action
+        ListBuilder.RowBuilder someToggles = new ListBuilder.RowBuilder(lb);
+        someToggles.setTitleItem(toggleAction)
+                .setPrimaryAction(primaryAction)
+                .setTitle("Start & end toggles")
+                .setSubtitle("Normal primary action")
+                .addEndItem(toggleAction2)
+                .addEndItem(toggleAction3);
+
+        // Start toggle ONLY
+        ListBuilder.RowBuilder startToggleOnly = new ListBuilder.RowBuilder(lb);
+        startToggleOnly.setTitle("Start action is a toggle")
+                .setSubtitle("No other actions")
+                .setTitleItem(toggleAction);
+
+        // End toggle ONLY
+        ListBuilder.RowBuilder endToggleOnly = new ListBuilder.RowBuilder(lb);
+        endToggleOnly.setTitle("End action is a toggle")
+                .setSubtitle("No other actions")
+                .addEndItem(toggleAction);
+
+        // All toggles: end item should be ignored / replaced with primary action
+        ListBuilder.RowBuilder muchToggles = new ListBuilder.RowBuilder(lb);
+        muchToggles.setTitleItem(toggleAction)
+                .setTitle("All toggles")
+                .setSubtitle("Even the primary action")
+                .setPrimaryAction(toggleAction2)
+                .addEndItem(toggleAction3);
+
+        lb.addRow(primaryToggle);
+        lb.addRow(endToggleOnly);
+        lb.addRow(endToggle);
+        lb.addRow(startToggleOnly);
+        lb.addRow(startToggle);
+        lb.addRow(someToggles);
+        lb.addRow(muchToggles);
+        return lb.build();
+    }
+
+    private Slice createSingleSlice(Uri uri) {
+        IconCompat ic2 = IconCompat.createWithResource(getContext(), R.drawable.ic_create);
+        IconCompat image = IconCompat.createWithResource(getContext(), R.drawable.cat_3);
+        IconCompat toggle = IconCompat.createWithResource(getContext(), R.drawable.toggle_star);
+        SliceAction toggleAction = new SliceAction(
+                getBroadcastIntent(ACTION_TOAST, "toggle action"), toggle, "toggle", false);
+        SliceAction simpleAction = new SliceAction(
+                getBroadcastIntent(ACTION_TOAST, "icon action"), ic2, "icon");
+        ListBuilder lb = new ListBuilder(getContext(), uri, INFINITY);
+        return lb.addRow(new ListBuilder.RowBuilder(lb)
+                .setTitle("Single title"))
+                .addRow(new ListBuilder.RowBuilder(lb)
+                        .setSubtitle("Single subtitle"))
+                 //Time stamps
+                .addRow(new ListBuilder.RowBuilder(lb)
+                        .setTitleItem(System.currentTimeMillis()))
+                .addRow(new ListBuilder.RowBuilder(lb)
+                        .addEndItem(System.currentTimeMillis()))
+                // Toggle actions
+                .addRow(new ListBuilder.RowBuilder(lb)
+                        .setTitleItem(toggleAction))
+                .addRow(new ListBuilder.RowBuilder(lb)
+                        .addEndItem(toggleAction))
+                // Icon actions
+                .addRow(new ListBuilder.RowBuilder(lb)
+                        .setTitleItem(simpleAction))
+                .addRow(new ListBuilder.RowBuilder(lb)
+                        .addEndItem(simpleAction))
+                // Images
+                .addRow(new ListBuilder.RowBuilder(lb)
+                        .setTitleItem(image, SMALL_IMAGE))
+                .addRow(new ListBuilder.RowBuilder(lb)
+                        .addEndItem(image, SMALL_IMAGE))
+                .build();
+    }
+
     private Handler mHandler = new Handler();
     private SparseArray<String> mListSummaries = new SparseArray<>();
     private long mListLastUpdate;
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 0a855a5..7b9d3d6 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;
 
@@ -47,6 +49,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;
@@ -65,7 +68,7 @@
 
     private static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI";
     private static final boolean TEST_INTENT = false;
-    private static final boolean TEST_THEMES = false;
+    private static final boolean TEST_THEMES = true;
     private static final boolean SCROLLING_ENABLED = true;
 
     private ArrayList<Uri> mSliceUris = new ArrayList<Uri>();
@@ -218,7 +221,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/samples/SupportSliceDemos/src/main/res/values/styles.xml b/samples/SupportSliceDemos/src/main/res/values/styles.xml
index d276a41..fcc4b1f 100644
--- a/samples/SupportSliceDemos/src/main/res/values/styles.xml
+++ b/samples/SupportSliceDemos/src/main/res/values/styles.xml
@@ -34,21 +34,7 @@
     <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light"/>
 
     <style name="CustomSliceView" parent="Widget.SliceView">
-        <item name="tintColor">@color/tintColor</item>
-
-        <item name="titleColor">@color/textColor</item>
-        <item name="subtitleColor">@color/textColor</item>
-
-        <item name="headerTitleSize">@dimen/textSize</item>
-        <item name="headerSubtitleSize">@dimen/textSize</item>
-        <item name="titleSize">@dimen/textSize</item>
-        <item name="subtitleSize">@dimen/textSize</item>
-        <item name="gridTitleSize">@dimen/textSize</item>
-        <item name="gridSubtitleSize">@dimen/smallTextSize</item>
-
-        <item name="android:paddingLeft">8dp</item>
-        <item name="android:paddingTop">8dp</item>
-        <item name="android:paddingRight">8dp</item>
-        <item name="android:paddingBottom">8dp</item>
+        <item name="gridTopPadding">4dp</item>
+        <item name="gridBottomPadding">4dp</item>
     </style>
 </resources>
diff --git a/settings.gradle b/settings.gradle
index 8a3a7fe..d969d88 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -40,9 +40,11 @@
 includeProject(":car", "car")
 includeProject(":cardview", "cardview")
 includeProject(":collection", "collection")
+includeProject(":collection-ktx", "collection/ktx")
 includeProject(":contentpaging", "content")
 includeProject(":coordinatorlayout", "coordinatorlayout")
 includeProject(":core", "compat")
+includeProject(":core-ktx", "core/ktx")
 includeProject(":cursoradapter", "cursoradapter")
 includeProject(":customview", "customview")
 includeProject(":documentfile", "documentfile")
@@ -69,6 +71,7 @@
 includeProject(":media-widget", "media-widget")
 includeProject(":mediarouter", "mediarouter")
 includeProject(":palette", "palette")
+includeProject(":palette-ktx", "palette/ktx")
 includeProject(":percentlayout", "percent")
 includeProject(":preference", "preference")
 includeProject(":print", "print")
@@ -79,6 +82,7 @@
 includeProject(":slice-view", "slices/view")
 includeProject(":slice-builders", "slices/builders")
 includeProject(":slidingpanelayout", "slidingpanelayout")
+includeProject(":fragment-ktx", "fragment/ktx")
 includeProject(":swiperefreshlayout", "swiperefreshlayout")
 includeProject(":textclassifier", "textclassifier")
 includeProject(":transition", "transition")
@@ -138,6 +142,7 @@
 /////////////////////////////
 
 includeProject(":internal-testutils", "testutils")
+includeProject(":internal-testutils-ktx", "testutils-ktx")
 
 /////////////////////////////
 //
diff --git a/slices/builders/api/current.txt b/slices/builders/api/current.txt
index 1041eb9..f191aed 100644
--- a/slices/builders/api/current.txt
+++ b/slices/builders/api/current.txt
@@ -77,7 +77,8 @@
     method public deprecated androidx.slice.builders.ListBuilder addSeeMoreAction(android.app.PendingIntent);
     method public deprecated androidx.slice.builders.ListBuilder addSeeMoreRow(androidx.slice.builders.ListBuilder.RowBuilder);
     method public deprecated androidx.slice.builders.ListBuilder addSeeMoreRow(androidx.core.util.Consumer<androidx.slice.builders.ListBuilder.RowBuilder>);
-    method public androidx.slice.builders.ListBuilder setColor(int);
+    method public androidx.slice.builders.ListBuilder setAccentColor(int);
+    method public deprecated androidx.slice.builders.ListBuilder setColor(int);
     method public androidx.slice.builders.ListBuilder setHeader(androidx.slice.builders.ListBuilder.HeaderBuilder);
     method public androidx.slice.builders.ListBuilder setHeader(androidx.core.util.Consumer<androidx.slice.builders.ListBuilder.HeaderBuilder>);
     method public androidx.slice.builders.ListBuilder setKeywords(java.util.List<java.lang.String>);
diff --git a/slices/builders/build.gradle b/slices/builders/build.gradle
index ec85148..91c7599 100644
--- a/slices/builders/build.gradle
+++ b/slices/builders/build.gradle
@@ -37,4 +37,5 @@
     description = "A set of builders to create templates using SliceProvider APIs"
     minSdkVersion = 19
     failOnUncheckedWarnings = false
+    failOnDeprecationWarnings = false
 }
diff --git a/slices/builders/src/main/java/androidx/slice/builders/ListBuilder.java b/slices/builders/src/main/java/androidx/slice/builders/ListBuilder.java
index 1c81ebd..76b9e08 100644
--- a/slices/builders/src/main/java/androidx/slice/builders/ListBuilder.java
+++ b/slices/builders/src/main/java/androidx/slice/builders/ListBuilder.java
@@ -82,9 +82,10 @@
  * <ul>
  *     <li>{@link androidx.slice.widget.SliceView#MODE_SHORTCUT} - The primary {@link SliceAction}
  *     of the slice is used your primary action should contain an image and title representative
- *     of your slice. If providing a tintable icon, use {@link #setColor(int)} to specify the color.
- *     If a header has been specified for the list, the primary action associated with it will be
- *     used, otherwise it will be the primary action associated with the first row of the list.
+ *     of your slice. If providing a tintable icon, use {@link #setAccentColor(int)} to specify the
+ *     color. If a header has been specified for the list, the primary action associated with it
+ *     will be used, otherwise it will be the primary action associated with the first row of the
+ *     list.
  *     </li>
  *     <li>{@link androidx.slice.widget.SliceView#MODE_SMALL} - Only a single row of content is
  *     displayed in small format. If a header has been specified it will be displayed. If no header
@@ -310,6 +311,15 @@
     }
 
     /**
+     * @deprecated TO BE REMOVED; use {@link #setAccentColor(int)} instead.
+     */
+    @Deprecated
+    @NonNull
+    public ListBuilder setColor(@ColorInt int color) {
+        return setAccentColor(color);
+    }
+
+    /**
      * Sets the color to use on tintable items within the list builder.
      * Things that might be tinted are:
      * <ul>
@@ -322,7 +332,7 @@
      * </ul>
      */
     @NonNull
-    public ListBuilder setColor(@ColorInt int color) {
+    public ListBuilder setAccentColor(@ColorInt int color) {
         mImpl.setColor(color);
         return this;
     }
diff --git a/slices/builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java b/slices/builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java
index 714b570..16471ed 100644
--- a/slices/builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java
+++ b/slices/builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java
@@ -18,11 +18,9 @@
 
 import static android.app.slice.Slice.HINT_HORIZONTAL;
 import static android.app.slice.Slice.HINT_LARGE;
-import static android.app.slice.Slice.HINT_LIST_ITEM;
 import static android.app.slice.Slice.HINT_NO_TINT;
 import static android.app.slice.Slice.HINT_PARTIAL;
 import static android.app.slice.Slice.HINT_SEE_MORE;
-import static android.app.slice.Slice.HINT_SHORTCUT;
 import static android.app.slice.Slice.HINT_TITLE;
 import static android.app.slice.Slice.SUBTYPE_CONTENT_DESCRIPTION;
 
@@ -59,23 +57,12 @@
     /**
      */
     @Override
-    @NonNull
-    public Slice build() {
-        Slice.Builder sb = new Slice.Builder(getBuilder())
-                .addHints(HINT_HORIZONTAL, HINT_LIST_ITEM);
-        sb.addSubSlice(getBuilder().addHints(HINT_HORIZONTAL, HINT_LIST_ITEM).build());
-        if (mPrimaryAction != null) {
-            Slice.Builder actionBuilder = new Slice.Builder(getBuilder())
-                    .addHints(HINT_SHORTCUT, HINT_TITLE);
-            sb.addSubSlice(mPrimaryAction.buildSlice(actionBuilder));
-        }
-        return sb.build();
-    }
-
-    /**
-     */
-    @Override
     public void apply(Slice.Builder builder) {
+        builder.addHints(HINT_HORIZONTAL);
+        if (mPrimaryAction != null) {
+            Slice.Builder actionBuilder = new Slice.Builder(getBuilder()).addHints(HINT_TITLE);
+            builder.addSubSlice(mPrimaryAction.buildSlice(actionBuilder));
+        }
     }
 
     /**
@@ -96,7 +83,7 @@
      */
     @Override
     public void addCell(TemplateBuilderImpl builder) {
-        getBuilder().addSubSlice(builder.getBuilder().addHints(HINT_LIST_ITEM).build());
+        builder.apply(getBuilder());
     }
 
 
@@ -105,7 +92,7 @@
     @Override
     public void setSeeMoreCell(@NonNull TemplateBuilderImpl builder) {
         builder.getBuilder().addHints(HINT_SEE_MORE);
-        getBuilder().addSubSlice(builder.build());
+        builder.apply(getBuilder());
     }
 
     /**
@@ -184,8 +171,8 @@
         @Override
         public void addTitleText(@Nullable CharSequence text, boolean isLoading) {
             @Slice.SliceHint String[] hints = isLoading
-                    ? new String[] {HINT_PARTIAL, HINT_LARGE}
-                    : new String[] {HINT_LARGE};
+                    ? new String[] {HINT_PARTIAL, HINT_TITLE}
+                    : new String[] {HINT_TITLE};
             getBuilder().addText(text, null, hints);
         }
 
@@ -236,20 +223,12 @@
         @RestrictTo(LIBRARY)
         @Override
         public void apply(Slice.Builder b) {
-        }
-
-        /**
-         */
-        @Override
-        @NonNull
-        public Slice build() {
+            getBuilder().addHints(HINT_HORIZONTAL);
             if (mContentIntent != null) {
-                return new Slice.Builder(getBuilder())
-                        .addHints(HINT_HORIZONTAL)
-                        .addAction(mContentIntent, getBuilder().build(), null)
-                        .build();
+                b.addAction(mContentIntent, getBuilder().build(), null);
+            } else {
+                b.addSubSlice(getBuilder().build());
             }
-            return getBuilder().addHints(HINT_HORIZONTAL).build();
         }
     }
 }
diff --git a/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderV1Impl.java b/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderV1Impl.java
index c771b62..8b16abe 100644
--- a/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderV1Impl.java
+++ b/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderV1Impl.java
@@ -76,7 +76,7 @@
      */
     @Override
     public void apply(Slice.Builder builder) {
-        builder.addTimestamp(System.currentTimeMillis(), SUBTYPE_MILLIS, HINT_LAST_UPDATED);
+        builder.addLong(System.currentTimeMillis(), SUBTYPE_MILLIS, HINT_LAST_UPDATED);
         if (mSliceHeader != null) {
             builder.addSubSlice(mSliceHeader);
         }
@@ -95,6 +95,7 @@
     @NonNull
     @Override
     public void addRow(@NonNull TemplateBuilderImpl builder) {
+        builder.getBuilder().addHints(HINT_LIST_ITEM);
         getBuilder().addSubSlice(builder.build());
     }
 
@@ -103,6 +104,7 @@
     @NonNull
     @Override
     public void addGridRow(@NonNull TemplateBuilderImpl builder) {
+        builder.getBuilder().addHints(HINT_LIST_ITEM);
         getBuilder().addSubSlice(builder.build());
     }
 
@@ -558,7 +560,6 @@
                         getBuilder()).addHints(HINT_TITLE, HINT_SHORTCUT);
                 b.addSubSlice(mPrimaryAction.buildSlice(sb), null);
             }
-            b.addHints(HINT_LIST_ITEM);
         }
     }
 
diff --git a/slices/core/api/current.txt b/slices/core/api/current.txt
index bbaa3a8..c6077bf 100644
--- a/slices/core/api/current.txt
+++ b/slices/core/api/current.txt
@@ -17,21 +17,34 @@
     method public java.util.List<java.lang.String> getHints();
     method public androidx.core.graphics.drawable.IconCompat getIcon();
     method public int getInt();
+    method public long getLong();
     method public androidx.slice.Slice getSlice();
     method public java.lang.String getSubType();
     method public java.lang.CharSequence getText();
-    method public long getTimestamp();
+    method public deprecated long getTimestamp();
     method public boolean hasHint(java.lang.String);
   }
 
-  public abstract class SliceProvider extends android.content.ContentProvider {
+  public abstract class SliceProvider extends android.content.ContentProvider implements androidx.core.app.CoreComponentFactory.CompatWrapped {
+    ctor public SliceProvider(java.lang.String...);
     ctor public SliceProvider();
+    method public final int bulkInsert(android.net.Uri, android.content.ContentValues[]);
+    method public final android.net.Uri canonicalize(android.net.Uri);
+    method public final int delete(android.net.Uri, java.lang.String, java.lang.String[]);
+    method public final java.lang.String getType(android.net.Uri);
+    method public java.lang.Object getWrapper();
+    method public final android.net.Uri insert(android.net.Uri, android.content.ContentValues);
     method public abstract androidx.slice.Slice onBindSlice(android.net.Uri);
+    method public final boolean onCreate();
     method public abstract boolean onCreateSliceProvider();
     method public java.util.Collection<android.net.Uri> onGetSliceDescendants(android.net.Uri);
     method public android.net.Uri onMapIntentToUri(android.content.Intent);
     method public void onSlicePinned(android.net.Uri);
     method public void onSliceUnpinned(android.net.Uri);
+    method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String);
+    method public final android.database.Cursor query(android.net.Uri, java.lang.String[], android.os.Bundle, android.os.CancellationSignal);
+    method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, android.os.CancellationSignal);
+    method public final int update(android.net.Uri, android.content.ContentValues, java.lang.String, java.lang.String[]);
   }
 
 }
diff --git a/slices/core/src/androidTest/java/androidx/slice/SliceTestProvider.java b/slices/core/src/androidTest/java/androidx/slice/SliceTestProvider.java
index d6f1628..519e5b8 100644
--- a/slices/core/src/androidTest/java/androidx/slice/SliceTestProvider.java
+++ b/slices/core/src/androidTest/java/androidx/slice/SliceTestProvider.java
@@ -33,6 +33,7 @@
 
     @Override
     public boolean onCreateSliceProvider() {
+        getContext().getPackageName();
         return true;
     }
 
diff --git a/slices/core/src/androidTest/java/androidx/slice/compat/CompatPermissionManagerTest.java b/slices/core/src/androidTest/java/androidx/slice/compat/CompatPermissionManagerTest.java
new file mode 100644
index 0000000..552df8c
--- /dev/null
+++ b/slices/core/src/androidTest/java/androidx/slice/compat/CompatPermissionManagerTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.PackageManager;
+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.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CompatPermissionManagerTest {
+
+    private final Context mContext = InstrumentationRegistry.getContext();
+
+    @Test
+    public void testAutoGrant() {
+        final Uri uri = new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_CONTENT)
+                .authority("my.authority")
+                .path("my_path")
+                .build();
+        final String testPermission = "android.permission.SOME_PERMISSION";
+
+        final int grantedPid = Process.myPid();
+        final int grantedUid = Process.myUid();
+
+        final int nonGrantedPid = grantedPid + 1;
+        final int nonGrantedUid = grantedUid + 1;
+
+        Context permContext = new ContextWrapper(mContext) {
+            @Override
+            public int checkPermission(String permission, int pid, int uid) {
+                if (testPermission.equals(permission)) {
+                    if (grantedUid == uid) {
+                        return PackageManager.PERMISSION_GRANTED;
+                    } else if (nonGrantedUid == uid) {
+                        return PackageManager.PERMISSION_DENIED;
+                    }
+                }
+                return super.checkPermission(permission, pid, uid);
+            }
+
+            @Override
+            public PackageManager getPackageManager() {
+                PackageManager pm = spy(super.getPackageManager());
+                when(pm.getPackagesForUid(grantedUid)).thenReturn(new String[] { "grant_pkg"});
+                when(pm.getPackagesForUid(nonGrantedUid)).thenReturn(new String[] { "other_pkg"});
+                return pm;
+            }
+        };
+        CompatPermissionManager manager = spy(new CompatPermissionManager(permContext, "nothing", 0,
+                new String[] {testPermission}));
+
+        assertEquals(PERMISSION_DENIED, manager.checkSlicePermission(uri,
+                nonGrantedPid, nonGrantedUid));
+
+        assertEquals(PERMISSION_GRANTED, manager.checkSlicePermission(uri, grantedPid, grantedUid));
+        verify(manager).grantSlicePermission(eq(uri), eq("grant_pkg"));
+
+    }
+
+}
diff --git a/slices/core/src/androidTest/java/androidx/slice/compat/SliceProviderCompatTest.java b/slices/core/src/androidTest/java/androidx/slice/compat/SliceProviderCompatTest.java
new file mode 100644
index 0000000..cc4ba49
--- /dev/null
+++ b/slices/core/src/androidTest/java/androidx/slice/compat/SliceProviderCompatTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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 static androidx.slice.compat.SliceProviderCompat.EXTRA_BIND_URI;
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_SLICE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.slice.Slice;
+import androidx.slice.SliceProvider;
+import androidx.slice.SliceSpec;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SliceProviderCompatTest {
+
+    private final Context mContext = InstrumentationRegistry.getContext();
+
+    @Test
+    public void testBindWithPermission() {
+        Uri uri = new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_CONTENT)
+                .authority("my.authority")
+                .path("my_path")
+                .build();
+        Slice s = new Slice.Builder(uri)
+                .addText("", null)
+                .build();
+
+        SliceProvider provider = spy(new SliceProviderImpl());
+        CompatPermissionManager permissions = mock(CompatPermissionManager.class);
+        when(permissions.checkSlicePermission(any(Uri.class), anyInt(), anyInt()))
+                .thenReturn(PERMISSION_GRANTED);
+
+        when(provider.onBindSlice(eq(uri))).thenReturn(s);
+        SliceProviderCompat compat = new SliceProviderCompat(provider, permissions,
+                mContext) {
+            @Override
+            public String getCallingPackage() {
+                return mContext.getPackageName();
+            }
+        };
+
+        Bundle b = new Bundle();
+        b.putParcelable(EXTRA_BIND_URI, uri);
+        SliceProviderCompat.addSpecs(b, Collections.<SliceSpec>emptySet());
+
+        Bundle result = compat.call(SliceProviderCompat.METHOD_SLICE, null, b);
+        assertEquals(s.toString(), new Slice(result.getBundle(EXTRA_SLICE)).toString());
+    }
+
+    @Test
+    public void testBindWithoutPermission() {
+        Uri uri = new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_CONTENT)
+                .authority("my.authority")
+                .path("my_path")
+                .build();
+        Slice s = new Slice.Builder(uri)
+                .addText("", null)
+                .build();
+
+        SliceProvider provider = spy(new SliceProviderImpl());
+        CompatPermissionManager permissions = mock(CompatPermissionManager.class);
+        when(permissions.checkSlicePermission(any(Uri.class), anyInt(), anyInt()))
+                .thenReturn(PERMISSION_DENIED);
+
+        when(provider.onBindSlice(eq(uri))).thenReturn(s);
+        SliceProviderCompat compat = new SliceProviderCompat(provider, permissions,
+                mContext) {
+            @Override
+            public String getCallingPackage() {
+                return mContext.getPackageName();
+            }
+        };
+
+        Bundle b = new Bundle();
+        b.putParcelable(EXTRA_BIND_URI, uri);
+        SliceProviderCompat.addSpecs(b, Collections.<SliceSpec>emptySet());
+
+        Bundle result = compat.call(SliceProviderCompat.METHOD_SLICE, null, b);
+        assertNotEquals(s.toString(), new Slice(result.getBundle(EXTRA_SLICE)).toString());
+    }
+
+    public static class SliceProviderImpl extends SliceProvider {
+
+        @Override
+        public boolean onCreateSliceProvider() {
+            return true;
+        }
+
+        @Override
+        public Slice onBindSlice(Uri sliceUri) {
+            return null;
+        }
+    }
+}
diff --git a/slices/core/src/main/java/androidx/slice/Slice.java b/slices/core/src/main/java/androidx/slice/Slice.java
index d291ad2..8ce99da 100644
--- a/slices/core/src/main/java/androidx/slice/Slice.java
+++ b/slices/core/src/main/java/androidx/slice/Slice.java
@@ -31,6 +31,7 @@
 import static android.app.slice.SliceItem.FORMAT_ACTION;
 import static android.app.slice.SliceItem.FORMAT_IMAGE;
 import static android.app.slice.SliceItem.FORMAT_INT;
+import static android.app.slice.SliceItem.FORMAT_LONG;
 import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
 import static android.app.slice.SliceItem.FORMAT_SLICE;
 import static android.app.slice.SliceItem.FORMAT_TEXT;
@@ -58,6 +59,7 @@
 import androidx.annotation.StringDef;
 import androidx.core.graphics.drawable.IconCompat;
 import androidx.core.os.BuildCompat;
+import androidx.core.util.Consumer;
 import androidx.slice.compat.SliceProviderCompat;
 
 import java.util.ArrayList;
@@ -285,6 +287,19 @@
         }
 
         /**
+         * Add an action to the slice being constructed
+         * @param subType Optional template-specific type information
+         * @see {@link SliceItem#getSubType()}
+         */
+        public Slice.Builder addAction(@NonNull Consumer<Uri> action,
+                @NonNull Slice s, @Nullable String subType) {
+            @SliceHint String[] hints = s != null
+                    ? s.getHints().toArray(new String[s.getHints().size()]) : new String[0];
+            mItems.add(new SliceItem(action, s, FORMAT_ACTION, subType, hints));
+            return this;
+        }
+
+        /**
          * Add text to the slice being constructed
          * @param subType Optional template-specific type information
          * @see {@link SliceItem#getSubType()}
@@ -377,6 +392,19 @@
          * @param subType Optional template-specific type information
          * @see {@link SliceItem#getSubType()}
          */
+        public Slice.Builder addLong(long time, @Nullable String subType,
+                @SliceHint String... hints) {
+            mItems.add(new SliceItem(time, FORMAT_LONG, subType, hints));
+            return this;
+        }
+
+        /**
+         * Add a timestamp to the slice being constructed
+         * @param subType Optional template-specific type information
+         * @see {@link SliceItem#getSubType()}
+         * @deprecated TO BE REMOVED
+         */
+        @Deprecated
         public Slice.Builder addTimestamp(long time, @Nullable String subType,
                 @SliceHint String... hints) {
             mItems.add(new SliceItem(time, FORMAT_TIMESTAMP, subType, hints));
@@ -427,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 == null || 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 a5741f0..964c5df 100644
--- a/slices/core/src/main/java/androidx/slice/SliceItem.java
+++ b/slices/core/src/main/java/androidx/slice/SliceItem.java
@@ -19,13 +19,19 @@
 import static android.app.slice.SliceItem.FORMAT_ACTION;
 import static android.app.slice.SliceItem.FORMAT_IMAGE;
 import static android.app.slice.SliceItem.FORMAT_INT;
+import static android.app.slice.SliceItem.FORMAT_LONG;
 import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
 import static android.app.slice.SliceItem.FORMAT_SLICE;
 import static android.app.slice.SliceItem.FORMAT_TEXT;
 import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
 
+import static androidx.slice.Slice.addHints;
+
 import android.app.PendingIntent;
 import android.app.RemoteInput;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.text.TextUtils;
@@ -36,6 +42,7 @@
 import androidx.annotation.RestrictTo.Scope;
 import androidx.annotation.StringDef;
 import androidx.core.graphics.drawable.IconCompat;
+import androidx.core.util.Consumer;
 import androidx.core.util.Pair;
 
 import java.util.Arrays;
@@ -71,7 +78,7 @@
      */
     @RestrictTo(Scope.LIBRARY)
     @StringDef({FORMAT_SLICE, FORMAT_TEXT, FORMAT_IMAGE, FORMAT_ACTION, FORMAT_INT,
-            FORMAT_TIMESTAMP, FORMAT_REMOTE_INPUT})
+            FORMAT_TIMESTAMP, FORMAT_REMOTE_INPUT, FORMAT_LONG})
     public @interface SliceType {
     }
 
@@ -111,7 +118,16 @@
     @RestrictTo(Scope.LIBRARY)
     public SliceItem(PendingIntent intent, Slice slice, String format, String subType,
             @Slice.SliceHint String[] hints) {
-        this(new Pair<>(intent, slice), format, subType, hints);
+        this(new Pair<Object, Slice>(intent, slice), format, subType, hints);
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY)
+    public SliceItem(Consumer<Uri> action, Slice slice, String format, String subType,
+            @Slice.SliceHint String[] hints) {
+        this(new Pair<Object, Slice>(action, slice), format, subType, hints);
     }
 
     /**
@@ -188,7 +204,20 @@
      * SliceItem
      */
     public PendingIntent getAction() {
-        return ((Pair<PendingIntent, Slice>) mObj).first;
+        return (PendingIntent) ((Pair<Object, Slice>) mObj).first;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public void fireAction(Context context, Intent i) throws PendingIntent.CanceledException {
+        Object action = ((Pair<Object, Slice>) mObj).first;
+        if (action instanceof PendingIntent) {
+            ((PendingIntent) action).send(context, 0, i, null, null);
+        } else {
+            ((Consumer<Uri>) action).accept(getSlice().getUri());
+        }
     }
 
     /**
@@ -215,15 +244,23 @@
      */
     public Slice getSlice() {
         if (FORMAT_ACTION.equals(getFormat())) {
-            return ((Pair<PendingIntent, Slice>) mObj).second;
+            return ((Pair<Object, Slice>) mObj).second;
         }
         return (Slice) mObj;
     }
 
     /**
-     * @return The timestamp held by this {@link android.app.slice.SliceItem#FORMAT_TIMESTAMP}
+     * @return The long held by this {@link android.app.slice.SliceItem#FORMAT_LONG}
      * SliceItem
      */
+    public long getLong() {
+        return (Long) mObj;
+    }
+
+    /**
+     * @deprecated TO BE REMOVED
+     */
+    @Deprecated
     public long getTimestamp() {
         return (Long) mObj;
     }
@@ -301,8 +338,8 @@
                 dest.putParcelable(OBJ, ((Slice) obj).toBundle());
                 break;
             case FORMAT_ACTION:
-                dest.putParcelable(OBJ, ((Pair<PendingIntent, Slice>) obj).first);
-                dest.putBundle(OBJ_2, ((Pair<PendingIntent, Slice>) obj).second.toBundle());
+                dest.putParcelable(OBJ, (PendingIntent) ((Pair<Object, Slice>) obj).first);
+                dest.putBundle(OBJ_2, ((Pair<Object, Slice>) obj).second.toBundle());
                 break;
             case FORMAT_TEXT:
                 dest.putCharSequence(OBJ, (CharSequence) obj);
@@ -328,7 +365,7 @@
                 return in.getCharSequence(OBJ);
             case FORMAT_ACTION:
                 return new Pair<>(
-                        (PendingIntent) in.getParcelable(OBJ),
+                        in.getParcelable(OBJ),
                         new Slice(in.getBundle(OBJ_2)));
             case FORMAT_INT:
                 return in.getInt(OBJ);
@@ -377,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 8dc3b99..9c604eb 100644
--- a/slices/core/src/main/java/androidx/slice/SliceProvider.java
+++ b/slices/core/src/main/java/androidx/slice/SliceProvider.java
@@ -15,21 +15,44 @@
  */
 package androidx.slice;
 
+import static android.app.slice.Slice.HINT_SHORTCUT;
+import static android.app.slice.Slice.HINT_TITLE;
+import static android.app.slice.SliceProvider.SLICE_TYPE;
+
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_BIND_URI;
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_PKG;
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_PROVIDER_PKG;
+import static androidx.slice.compat.SliceProviderCompat.PERMS_PREFIX;
+import static androidx.slice.core.SliceHints.HINT_PERMISSION_REQUEST;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
 import android.content.ContentProvider;
 import android.content.ContentResolver;
+import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.ProviderInfo;
+import android.content.pm.PackageManager;
 import android.database.ContentObserver;
+import android.database.Cursor;
 import android.net.Uri;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Process;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.app.CoreComponentFactory;
 import androidx.core.os.BuildCompat;
-import androidx.slice.compat.ContentProviderWrapper;
+import androidx.slice.compat.CompatPermissionManager;
 import androidx.slice.compat.SliceProviderCompat;
 import androidx.slice.compat.SliceProviderWrapperContainer;
+import androidx.slice.core.R;
 
 import java.util.Collection;
 import java.util.Collections;
@@ -74,19 +97,37 @@
  *
  * @see android.app.slice.Slice
  */
-public abstract class SliceProvider extends ContentProviderWrapper {
+public abstract class SliceProvider extends ContentProvider implements
+        CoreComponentFactory.CompatWrapped {
 
     private static Set<SliceSpec> sSpecs;
 
-    @Override
-    public void attachInfo(Context context, ProviderInfo info) {
-        ContentProvider impl;
-        if (BuildCompat.isAtLeastP()) {
-            impl = new SliceProviderWrapperContainer.SliceProviderWrapper(this);
-        } else {
-            impl = new SliceProviderCompat(this);
-        }
-        super.attachInfo(context, info, impl);
+    private static final String TAG = "SliceProvider";
+
+    private static final boolean DEBUG = false;
+    private final String[] mAutoGrantPermissions;
+
+    private SliceProviderCompat mCompat;
+
+
+    /**
+     * A version of constructing a SliceProvider that allows autogranting slice permissions
+     * to apps that hold specific platform permissions.
+     * <p>
+     * When an app tries to bind a slice from this provider that it does not have access to,
+     * This provider will check if the caller holds permissions to any of the autoGrantPermissions
+     * specified, if they do they will be granted persisted uri access to all slices of this
+     * provider.
+     *
+     * @param autoGrantPermissions List of permissions that holders are auto-granted access
+     *                             to slices.
+     */
+    public SliceProvider(@NonNull String... autoGrantPermissions) {
+        mAutoGrantPermissions = autoGrantPermissions;
+    }
+
+    public SliceProvider() {
+        mAutoGrantPermissions = new String[0];
     }
 
     /**
@@ -106,6 +147,106 @@
      */
     public abstract boolean onCreateSliceProvider();
 
+    @Override
+    public Object getWrapper() {
+        if (BuildCompat.isAtLeastP()) {
+            return new SliceProviderWrapperContainer.SliceProviderWrapper(this,
+                    mAutoGrantPermissions);
+        }
+        return null;
+    }
+
+    @Override
+    public final boolean onCreate() {
+        if (!BuildCompat.isAtLeastP()) {
+            mCompat = new SliceProviderCompat(this,
+                    onCreatePermissionManager(mAutoGrantPermissions), getContext());
+        }
+        return onCreateSliceProvider();
+    }
+
+    /**
+     * @hide
+     * @param autoGrantPermissions
+     */
+    @VisibleForTesting
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    protected CompatPermissionManager onCreatePermissionManager(
+            String[] autoGrantPermissions) {
+        return new CompatPermissionManager(getContext(), PERMS_PREFIX + getClass().getName(),
+                Process.myUid(), autoGrantPermissions);
+    }
+
+    @Override
+    public final String getType(Uri uri) {
+        if (DEBUG) Log.d(TAG, "getFormat " + uri);
+        return SLICE_TYPE;
+    }
+
+    @Override
+    public Bundle call(String method, String arg, Bundle extras) {
+        return mCompat != null ? mCompat.call(method, arg, extras) : null;
+    }
+
+    /**
+     * Generate a slice that contains a permission request.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static Slice createPermissionSlice(Context context, Uri sliceUri,
+            String callingPackage) {
+        Slice.Builder parent = new Slice.Builder(sliceUri);
+
+        Slice.Builder action = new Slice.Builder(parent)
+                .addHints(HINT_TITLE, HINT_SHORTCUT)
+                .addAction(createPermissionIntent(context, sliceUri, callingPackage),
+                        new Slice.Builder(parent).build(), null);
+
+        parent.addSubSlice(new Slice.Builder(sliceUri.buildUpon().appendPath("permission").build())
+                .addText(getPermissionString(context, callingPackage), null)
+                .addSubSlice(action.build())
+                .build());
+
+        return parent.addHints(HINT_PERMISSION_REQUEST).build();
+    }
+
+    /**
+     * Create a PendingIntent pointing at the permission dialog.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static PendingIntent createPermissionIntent(Context context, Uri sliceUri,
+            String callingPackage) {
+        Intent intent = new Intent();
+        intent.setComponent(new ComponentName(context.getPackageName(),
+                "androidx.slice.compat.SlicePermissionActivity"));
+        intent.putExtra(EXTRA_BIND_URI, sliceUri);
+        intent.putExtra(EXTRA_PKG, callingPackage);
+        intent.putExtra(EXTRA_PROVIDER_PKG, context.getPackageName());
+        // Unique pending intent.
+        intent.setData(sliceUri.buildUpon().appendQueryParameter("package", callingPackage)
+                .build());
+
+        return PendingIntent.getActivity(context, 0, intent, 0);
+    }
+
+    /**
+     * Get string describing permission request.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static CharSequence getPermissionString(Context context, String callingPackage) {
+        PackageManager pm = context.getPackageManager();
+        try {
+            return context.getString(R.string.abc_slices_permission_request,
+                    pm.getApplicationInfo(callingPackage, 0).loadLabel(pm),
+                    context.getApplicationInfo().loadLabel(pm));
+        } catch (PackageManager.NameNotFoundException e) {
+            // This shouldn't be possible since the caller is verified.
+            throw new RuntimeException("Unknown calling app", e);
+        }
+    }
+
     /**
      * Implemented to create a slice.
      * <p>
@@ -181,6 +322,61 @@
         return Collections.emptyList();
     }
 
+    @Nullable
+    @Override
+    public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
+            @Nullable String selection, @Nullable String[] selectionArgs,
+            @Nullable String sortOrder) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    @RequiresApi(28)
+    public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
+            @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    @RequiresApi(16)
+    public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
+            @Nullable String selection, @Nullable String[] selectionArgs,
+            @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public final Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+        return null;
+    }
+
+    @Override
+    public final int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
+        return 0;
+    }
+
+    @Override
+    public final int delete(@NonNull Uri uri, @Nullable String selection,
+            @Nullable String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public final int update(@NonNull Uri uri, @Nullable ContentValues values,
+            @Nullable String selection, @Nullable String[] selectionArgs) {
+        return 0;
+    }
+
+    @Nullable
+    @Override
+    @RequiresApi(19)
+    public final Uri canonicalize(@NonNull Uri url) {
+        return null;
+    }
+
     /**
      * @hide
      */
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..8321a05
--- /dev/null
+++ b/slices/core/src/main/java/androidx/slice/compat/CompatPermissionManager.java
@@ -0,0 +1,220 @@
+/*
+ * 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.annotation.RestrictTo;
+import androidx.collection.ArraySet;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class CompatPermissionManager {
+
+    private static final String TAG = "CompatPermissionManager";
+    public static final String ALL_SUFFIX = "_all";
+
+    private final Context mContext;
+    private final String mPrefsName;
+    private final int mMyUid;
+    private final String[] mAutoGrantPermissions;
+
+    public CompatPermissionManager(Context context, String prefsName, int myUid,
+            String[] autoGrantPermissions) {
+        mContext = context;
+        mPrefsName = prefsName;
+        mMyUid = myUid;
+        mAutoGrantPermissions = autoGrantPermissions;
+    }
+
+    private SharedPreferences getPrefs() {
+        return mContext.getSharedPreferences(mPrefsName, Context.MODE_PRIVATE);
+    }
+
+    public int checkSlicePermission(Uri uri, int pid, int uid) {
+        if (uid == mMyUid) {
+            return PERMISSION_GRANTED;
+        }
+        String[] pkgs = mContext.getPackageManager().getPackagesForUid(uid);
+        for (String pkg : pkgs) {
+            if (checkSlicePermission(uri, pkg) == PERMISSION_GRANTED) {
+                return PERMISSION_GRANTED;
+            }
+        }
+        for (String autoGrantPermission : mAutoGrantPermissions) {
+            if (mContext.checkPermission(autoGrantPermission, pid, uid) == PERMISSION_GRANTED) {
+                for (String pkg : pkgs) {
+                    grantSlicePermission(uri, pkg);
+                }
+                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/CompatPinnedList.java b/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java
index 56b8122..da602f5 100644
--- a/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java
+++ b/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java
@@ -28,6 +28,8 @@
 import androidx.core.util.ObjectsCompat;
 import androidx.slice.SliceSpec;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -70,6 +72,22 @@
         return prefs;
     }
 
+    /**
+     * Get pinned specs
+     */
+    public List<Uri> getPinnedSlices() {
+        List<Uri> pinned = new ArrayList<>();
+        for (String key : getPrefs().getAll().keySet()) {
+            if (key.startsWith(PIN_PREFIX)) {
+                Uri uri = Uri.parse(key.substring(PIN_PREFIX.length()));
+                if (!getPins(uri).isEmpty()) {
+                    pinned.add(uri);
+                }
+            }
+        }
+        return pinned;
+    }
+
     private Set<String> getPins(Uri uri) {
         return getPrefs().getStringSet(PIN_PREFIX + uri.toString(), new ArraySet<String>());
     }
@@ -85,8 +103,8 @@
         if (TextUtils.isEmpty(specNamesStr) || TextUtils.isEmpty(specRevsStr)) {
             return new ArraySet<>();
         }
-        String[] specNames = specNamesStr.split(",");
-        String[] specRevs = specRevsStr.split(",");
+        String[] specNames = specNamesStr.split(",", -1);
+        String[] specRevs = specRevsStr.split(",", -1);
         if (specNames.length != specRevs.length) {
             return new ArraySet<>();
         }
diff --git a/slices/core/src/main/java/androidx/slice/compat/ContentProviderWrapper.java b/slices/core/src/main/java/androidx/slice/compat/ContentProviderWrapper.java
deleted file mode 100644
index 45c6ffc..0000000
--- a/slices/core/src/main/java/androidx/slice/compat/ContentProviderWrapper.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.slice.compat;
-
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.pm.ProviderInfo;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.CancellationSignal;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.RestrictTo.Scope;
-
-/**
- * @hide
- */
-// TODO: Remove as soon as we have better systems in place for this.
-@RestrictTo(Scope.LIBRARY)
-public class ContentProviderWrapper extends ContentProvider {
-
-    private ContentProvider mImpl;
-
-    /**
-     * Triggers an attach with the object to wrap.
-     */
-    public void attachInfo(Context context, ProviderInfo info, ContentProvider impl) {
-        mImpl = impl;
-        super.attachInfo(context, info);
-        mImpl.attachInfo(context, info);
-    }
-
-    @Override
-    public final boolean onCreate() {
-        return mImpl.onCreate();
-    }
-
-    @Nullable
-    @Override
-    public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
-            @Nullable String selection, @Nullable String[] selectionArgs,
-            @Nullable String sortOrder) {
-        return mImpl.query(uri, projection, selection, selectionArgs, sortOrder);
-    }
-
-    @Nullable
-    @Override
-    @RequiresApi(28)
-    public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
-            @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal) {
-        return mImpl.query(uri, projection, queryArgs, cancellationSignal);
-    }
-
-    @Nullable
-    @Override
-    @RequiresApi(16)
-    public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
-            @Nullable String selection, @Nullable String[] selectionArgs,
-            @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal) {
-        return mImpl.query(uri, projection, selection, selectionArgs, sortOrder,
-                cancellationSignal);
-    }
-
-    @Nullable
-    @Override
-    public final String getType(@NonNull Uri uri) {
-        return mImpl.getType(uri);
-    }
-
-    @Nullable
-    @Override
-    public final Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
-        return mImpl.insert(uri, values);
-    }
-
-    @Override
-    public final int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
-        return mImpl.bulkInsert(uri, values);
-    }
-
-    @Override
-    public final int delete(@NonNull Uri uri, @Nullable String selection,
-            @Nullable String[] selectionArgs) {
-        return mImpl.delete(uri, selection, selectionArgs);
-    }
-
-    @Override
-    public final int update(@NonNull Uri uri, @Nullable ContentValues values,
-            @Nullable String selection, @Nullable String[] selectionArgs) {
-        return mImpl.update(uri, values, selection, selectionArgs);
-    }
-
-    @Nullable
-    @Override
-    public final Bundle call(@NonNull String method, @Nullable String arg,
-            @Nullable Bundle extras) {
-        return mImpl.call(method, arg, extras);
-    }
-
-    @Nullable
-    @Override
-    @RequiresApi(19)
-    public final Uri canonicalize(@NonNull Uri url) {
-        return mImpl.canonicalize(url);
-    }
-}
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..5fa33ae 100644
--- a/slices/core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
+++ b/slices/core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
@@ -20,16 +20,20 @@
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
 import android.content.DialogInterface.OnDismissListener;
-import android.content.Intent;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.net.Uri;
 import android.os.Bundle;
+import android.text.Html;
+import android.text.TextPaint;
+import android.text.TextUtils;
 import android.util.Log;
 import android.widget.TextView;
 
 import androidx.annotation.RestrictTo;
 import androidx.appcompat.app.AlertDialog;
+import androidx.core.text.BidiFormatter;
 import androidx.slice.core.R;
 
 /**
@@ -40,6 +44,8 @@
 public class SlicePermissionActivity extends Activity implements OnClickListener,
         OnDismissListener {
 
+    private static final float MAX_LABEL_SIZE_PX = 500f;
+
     private static final String TAG = "SlicePermissionActivity";
 
     private Uri mUri;
@@ -56,8 +62,12 @@
 
         try {
             PackageManager pm = getPackageManager();
-            CharSequence app1 = pm.getApplicationInfo(mCallingPkg, 0).loadLabel(pm);
-            CharSequence app2 = pm.getApplicationInfo(mProviderPkg, 0).loadLabel(pm);
+            CharSequence app1 = BidiFormatter.getInstance().unicodeWrap(
+                    loadSafeLabel(pm, pm.getApplicationInfo(mCallingPkg, 0))
+                    .toString());
+            CharSequence app2 = BidiFormatter.getInstance().unicodeWrap(
+                    loadSafeLabel(pm, pm.getApplicationInfo(mProviderPkg, 0))
+                    .toString());
             AlertDialog dialog = new AlertDialog.Builder(this)
                     .setTitle(getString(R.string.abc_slice_permission_title, app1, app2))
                     .setView(R.layout.abc_slice_permission_request)
@@ -75,14 +85,50 @@
         }
     }
 
+    // Based on loadSafeLabel in PackageitemInfo
+    private CharSequence loadSafeLabel(PackageManager pm, ApplicationInfo appInfo) {
+        // loadLabel() always returns non-null
+        String label = appInfo.loadLabel(pm).toString();
+        // strip HTML tags to avoid <br> and other tags overwriting original message
+        String labelStr = Html.fromHtml(label).toString();
+
+        // If the label contains new line characters it may push the UI
+        // down to hide a part of it. Labels shouldn't have new line
+        // characters, so just truncate at the first time one is seen.
+        final int labelLength = labelStr.length();
+        int offset = 0;
+        while (offset < labelLength) {
+            final int codePoint = labelStr.codePointAt(offset);
+            final int type = Character.getType(codePoint);
+            if (type == Character.LINE_SEPARATOR
+                    || type == Character.CONTROL
+                    || type == Character.PARAGRAPH_SEPARATOR) {
+                labelStr = labelStr.substring(0, offset);
+                break;
+            }
+            // replace all non-break space to " " in order to be trimmed
+            if (type == Character.SPACE_SEPARATOR) {
+                labelStr = labelStr.substring(0, offset) + " " + labelStr.substring(offset
+                        + Character.charCount(codePoint));
+            }
+            offset += Character.charCount(codePoint);
+        }
+
+        labelStr = labelStr.trim();
+        if (labelStr.isEmpty()) {
+            return appInfo.packageName;
+        }
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(42);
+
+        return TextUtils.ellipsize(labelStr, paint, MAX_LABEL_SIZE_PX, TextUtils.TruncateAt.END);
+    }
+
     @Override
     public void onClick(DialogInterface dialog, int which) {
         if (which == DialogInterface.BUTTON_POSITIVE) {
-            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 57f070d..22e832e 100644
--- a/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
+++ b/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
@@ -15,35 +15,30 @@
  */
 package androidx.slice.compat;
 
-import static android.app.slice.Slice.HINT_SHORTCUT;
-import static android.app.slice.Slice.HINT_TITLE;
+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.slice.core.SliceHints.HINT_PERMISSION_REQUEST;
+import static androidx.core.content.PermissionChecker.PERMISSION_DENIED;
+import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED;
 
-import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.content.ContentProvider;
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
-import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
-import android.database.Cursor;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.CancellationSignal;
 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.os.StrictMode.ThreadPolicy;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -54,8 +49,6 @@
 import androidx.slice.Slice;
 import androidx.slice.SliceProvider;
 import androidx.slice.SliceSpec;
-import androidx.slice.core.R;
-import androidx.slice.core.SliceHints;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -67,11 +60,14 @@
  * @hide
  */
 @RestrictTo(Scope.LIBRARY)
-public class SliceProviderCompat extends ContentProvider {
+public class SliceProviderCompat {
+    public static final String PERMS_PREFIX = "slice_perms_";
+    private static final String TAG = "SliceProviderCompat";
+    private static final String DATA_PREFIX = "slice_data_";
+    private static final String ALL_FILES = DATA_PREFIX + "all_slice_files";
 
-    private static final String TAG = "SliceProvider";
+    private static final long SLICE_BIND_ANR = 2000;
 
-    public static final String EXTRA_BIND_URI = "slice_uri";
     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";
@@ -79,7 +75,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";
@@ -87,75 +87,48 @@
     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";
-    private static final String DATA_PREFIX = "slice_data_";
+    public static final String EXTRA_UID = "uid";
+    public static final String EXTRA_PID = "pid";
+    public static final String EXTRA_RESULT = "result";
 
-    private static final long SLICE_BIND_ANR = 2000;
-
-    private static final boolean DEBUG = false;
     private final Handler mHandler = new Handler(Looper.getMainLooper());
-    private SliceProvider mSliceProvider;
-    private CompatPinnedList mPinnedList;
+    private final Context mContext;
 
     private String mCallback;
+    private final SliceProvider mProvider;
+    private CompatPinnedList mPinnedList;
+    private CompatPermissionManager mPermissionManager;
 
-    public SliceProviderCompat(SliceProvider provider) {
-        mSliceProvider = provider;
+    public SliceProviderCompat(SliceProvider provider, CompatPermissionManager permissionManager,
+            Context context) {
+        mProvider = provider;
+        mContext = context;
+        String prefsFile = DATA_PREFIX + getClass().getName();
+        SharedPreferences allFiles = mContext.getSharedPreferences(ALL_FILES, 0);
+        Set<String> files = allFiles.getStringSet(ALL_FILES, Collections.<String>emptySet());
+        if (!files.contains(prefsFile)) {
+            // Make sure this is editable.
+            files = new ArraySet<>(files);
+            files.add(prefsFile);
+            allFiles.edit()
+                    .putStringSet(ALL_FILES, files)
+                    .commit();
+        }
+        mPinnedList = new CompatPinnedList(mContext, prefsFile);
+        mPermissionManager = permissionManager;
     }
 
-    @Override
-    public boolean onCreate() {
-        mPinnedList = new CompatPinnedList(getContext(),
-                DATA_PREFIX + mSliceProvider.getClass().getName());
-        return mSliceProvider.onCreateSliceProvider();
+    private Context getContext() {
+        return mContext;
     }
 
-    @Override
-    public final int update(Uri uri, ContentValues values, String selection,
-            String[] selectionArgs) {
-        if (DEBUG) Log.d(TAG, "update " + uri);
-        return 0;
+    public String getCallingPackage() {
+        return mProvider.getCallingPackage();
     }
 
-    @Override
-    public final int delete(Uri uri, String selection, String[] selectionArgs) {
-        if (DEBUG) Log.d(TAG, "delete " + uri);
-        return 0;
-    }
-
-    @Override
-    public final Cursor query(Uri uri, String[] projection, String selection,
-            String[] selectionArgs, String sortOrder) {
-        if (DEBUG) Log.d(TAG, "query " + uri);
-        return null;
-    }
-
-    @Override
-    public final Cursor query(Uri uri, String[] projection, String selection, String[]
-            selectionArgs, String sortOrder, CancellationSignal cancellationSignal) {
-        if (DEBUG) Log.d(TAG, "query " + uri);
-        return null;
-    }
-
-    @Override
-    public final Cursor query(Uri uri, String[] projection, Bundle queryArgs,
-            CancellationSignal cancellationSignal) {
-        if (DEBUG) Log.d(TAG, "query " + uri);
-        return null;
-    }
-
-    @Override
-    public final Uri insert(Uri uri, ContentValues values) {
-        if (DEBUG) Log.d(TAG, "insert " + uri);
-        return null;
-    }
-
-    @Override
-    public final String getType(Uri uri) {
-        if (DEBUG) Log.d(TAG, "getFormat " + uri);
-        return SLICE_TYPE;
-    }
-
-    @Override
+    /**
+     * 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);
@@ -163,23 +136,23 @@
 
             Slice s = handleBindSlice(uri, specs, getCallingPackage());
             Bundle b = new Bundle();
-            b.putParcelable(EXTRA_SLICE, s.toBundle());
+            b.putParcelable(EXTRA_SLICE, s != null ? s.toBundle() : null);
             return b;
         } else if (method.equals(METHOD_MAP_INTENT)) {
             Intent intent = extras.getParcelable(EXTRA_INTENT);
-            Uri uri = mSliceProvider.onMapIntentToUri(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());
+                b.putParcelable(EXTRA_SLICE, s != null ? s.toBundle() : null);
             } else {
                 b.putParcelable(EXTRA_SLICE, null);
             }
             return b;
         } else if (method.equals(METHOD_MAP_ONLY_INTENT)) {
             Intent intent = extras.getParcelable(EXTRA_INTENT);
-            Uri uri = mSliceProvider.onMapIntentToUri(intent);
+            Uri uri = mProvider.onMapIntentToUri(intent);
             Bundle b = new Bundle();
             b.putParcelable(EXTRA_SLICE, uri);
             return b;
@@ -209,15 +182,37 @@
             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 super.call(method, arg, extras);
+        return null;
     }
 
     private Collection<Uri> handleGetDescendants(Uri uri) {
         mCallback = "onGetSliceDescendants";
         mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
         try {
-            return mSliceProvider.onGetSliceDescendants(uri);
+            return mProvider.onGetSliceDescendants(uri);
         } finally {
             mHandler.removeCallbacks(mAnr);
         }
@@ -227,7 +222,7 @@
         mCallback = "onSlicePinned";
         mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
         try {
-            mSliceProvider.onSlicePinned(sliceUri);
+            mProvider.onSlicePinned(sliceUri);
         } finally {
             mHandler.removeCallbacks(mAnr);
         }
@@ -237,7 +232,7 @@
         mCallback = "onSliceUnpinned";
         mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
         try {
-            mSliceProvider.onSliceUnpinned(sliceUri);
+            mProvider.onSliceUnpinned(sliceUri);
         } finally {
             mHandler.removeCallbacks(mAnr);
         }
@@ -249,74 +244,15 @@
         // 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);
-            }
+        if (mPermissionManager.checkSlicePermission(sliceUri, Binder.getCallingPid(),
+                Binder.getCallingUid()) != PERMISSION_GRANTED) {
+            return mProvider.createPermissionSlice(getContext(), sliceUri, pkg);
         }
         return onBindSliceStrict(sliceUri, specs);
     }
 
-    /**
-     * Generate a slice that contains a permission request.
-     */
-    public static Slice createPermissionSlice(Context context, Uri sliceUri,
-            String callingPackage) {
-        Slice.Builder parent = new Slice.Builder(sliceUri);
-
-        Slice.Builder action = new Slice.Builder(parent)
-                .addHints(HINT_TITLE, HINT_SHORTCUT)
-                .addAction(createPermissionIntent(context, sliceUri, callingPackage),
-                        new Slice.Builder(parent).build(), null);
-
-        parent.addSubSlice(new Slice.Builder(sliceUri.buildUpon().appendPath("permission").build())
-                .addText(getPermissionString(context, callingPackage), null)
-                .addSubSlice(action.build())
-                .build());
-
-        return parent.addHints(HINT_PERMISSION_REQUEST).build();
-    }
-
-    /**
-     * Create a PendingIntent pointing at the permission dialog.
-     */
-    public static PendingIntent createPermissionIntent(Context context, Uri sliceUri,
-            String callingPackage) {
-        Intent intent = new Intent();
-        intent.setComponent(new ComponentName(context.getPackageName(),
-                "androidx.slice.compat.SlicePermissionActivity"));
-        intent.putExtra(EXTRA_BIND_URI, sliceUri);
-        intent.putExtra(EXTRA_PKG, callingPackage);
-        intent.putExtra(EXTRA_PROVIDER_PKG, context.getPackageName());
-        // Unique pending intent.
-        intent.setData(sliceUri.buildUpon().appendQueryParameter("package", callingPackage)
-                .build());
-
-        return PendingIntent.getActivity(context, 0, intent, 0);
-    }
-
-    /**
-     * Get string describing permission request.
-     */
-    public static CharSequence getPermissionString(Context context, String callingPackage) {
-        PackageManager pm = context.getPackageManager();
-        try {
-            return context.getString(R.string.abc_slices_permission_request,
-                    pm.getApplicationInfo(callingPackage, 0).loadLabel(pm),
-                    context.getApplicationInfo().loadLabel(pm));
-        } catch (PackageManager.NameNotFoundException e) {
-            // This shouldn't be possible since the caller is verified.
-            throw new RuntimeException("Unknown calling app", e);
-        }
-    }
-
     private Slice onBindSliceStrict(Uri sliceUri, Set<SliceSpec> specs) {
-        ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
+        StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
         mCallback = "onBindSlice";
         mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
         try {
@@ -326,7 +262,7 @@
                     .build());
             SliceProvider.setSpecs(specs);
             try {
-                return mSliceProvider.onBindSlice(sliceUri);
+                return mProvider.onBindSlice(sliceUri);
             } finally {
                 SliceProvider.setSpecs(null);
                 mHandler.removeCallbacks(mAnr);
@@ -336,21 +272,28 @@
         }
     }
 
+    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}.
      */
     public static Slice bindSlice(Context context, Uri uri,
             Set<SliceSpec> supportedSpecs) {
-        ContentProviderClient provider = context.getContentResolver()
-                .acquireContentProviderClient(uri);
-        if (provider == null) {
+        ProviderHolder holder = acquireClient(context.getContentResolver(), uri);
+        if (holder.mProvider == null) {
             throw new IllegalArgumentException("Unknown URI " + uri);
         }
         try {
             Bundle extras = new Bundle();
             extras.putParcelable(EXTRA_BIND_URI, uri);
             addSpecs(extras, supportedSpecs);
-            final Bundle res = provider.call(METHOD_SLICE, null, extras);
+            final Bundle res = holder.mProvider.call(METHOD_SLICE, null, extras);
             if (res == null) {
                 return null;
             }
@@ -360,19 +303,16 @@
             }
             return new Slice((Bundle) bundle);
         } catch (RemoteException e) {
-            // Arbitrary and not worth documenting, as Activity
-            // Manager will kill this process shortly anyway.
+            Log.e(TAG, "Unable to bind slice", e);
             return null;
         } finally {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-                provider.close();
-            } else {
-                provider.release();
-            }
         }
     }
 
-    private static void addSpecs(Bundle extras, Set<SliceSpec> supportedSpecs) {
+    /**
+     * Compat way to push specs through the call.
+     */
+    public static void addSpecs(Bundle extras, Set<SliceSpec> supportedSpecs) {
         ArrayList<String> types = new ArrayList<>();
         ArrayList<Integer> revs = new ArrayList<>();
         for (SliceSpec spec : supportedSpecs) {
@@ -383,7 +323,10 @@
         extras.putIntegerArrayList(EXTRA_SUPPORTED_SPECS_REVS, revs);
     }
 
-    private static Set<SliceSpec> getSpecs(Bundle extras) {
+    /**
+     * Compat way to push specs through the call.
+     */
+    public static Set<SliceSpec> getSpecs(Bundle extras) {
         ArraySet<SliceSpec> specs = new ArraySet<>();
         ArrayList<String> types = extras.getStringArrayList(EXTRA_SUPPORTED_SPECS);
         ArrayList<Integer> revs = extras.getIntegerArrayList(EXTRA_SUPPORTED_SPECS_REVS);
@@ -398,6 +341,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
@@ -406,23 +353,37 @@
             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)
                 .authority(authority).build();
-        ContentProviderClient provider = resolver.acquireContentProviderClient(uri);
-        if (provider == null) {
+        ProviderHolder holder = acquireClient(resolver, uri);
+        if (holder.mProvider == null) {
             throw new IllegalArgumentException("Unknown URI " + uri);
         }
         try {
             Bundle extras = new Bundle();
             extras.putParcelable(EXTRA_INTENT, intent);
             addSpecs(extras, supportedSpecs);
-            final Bundle res = provider.call(METHOD_MAP_INTENT, null, extras);
+            final Bundle res = holder.mProvider.call(METHOD_MAP_INTENT, null, extras);
             if (res == null) {
                 return null;
             }
@@ -432,11 +393,8 @@
             }
             return new Slice((Bundle) bundle);
         } catch (RemoteException e) {
-            // Arbitrary and not worth documenting, as Activity
-            // Manager will kill this process shortly anyway.
+            Log.e(TAG, "Unable to bind slice", e);
             return null;
-        } finally {
-            provider.close();
         }
     }
 
@@ -445,9 +403,8 @@
      */
     public static void pinSlice(Context context, Uri uri,
             Set<SliceSpec> supportedSpecs) {
-        ContentProviderClient provider = context.getContentResolver()
-                .acquireContentProviderClient(uri);
-        if (provider == null) {
+        ProviderHolder holder = acquireClient(context.getContentResolver(), uri);
+        if (holder.mProvider == null) {
             throw new IllegalArgumentException("Unknown URI " + uri);
         }
         try {
@@ -455,12 +412,9 @@
             extras.putParcelable(EXTRA_BIND_URI, uri);
             extras.putString(EXTRA_PKG, context.getPackageName());
             addSpecs(extras, supportedSpecs);
-            provider.call(METHOD_PIN, null, extras);
+            holder.mProvider.call(METHOD_PIN, null, extras);
         } catch (RemoteException e) {
-            // Arbitrary and not worth documenting, as Activity
-            // Manager will kill this process shortly anyway.
-        } finally {
-            provider.close();
+            Log.e(TAG, "Unable to pin slice", e);
         }
     }
 
@@ -469,9 +423,8 @@
      */
     public static void unpinSlice(Context context, Uri uri,
             Set<SliceSpec> supportedSpecs) {
-        ContentProviderClient provider = context.getContentResolver()
-                .acquireContentProviderClient(uri);
-        if (provider == null) {
+        ProviderHolder holder = acquireClient(context.getContentResolver(), uri);
+        if (holder.mProvider == null) {
             throw new IllegalArgumentException("Unknown URI " + uri);
         }
         try {
@@ -479,12 +432,9 @@
             extras.putParcelable(EXTRA_BIND_URI, uri);
             extras.putString(EXTRA_PKG, context.getPackageName());
             addSpecs(extras, supportedSpecs);
-            provider.call(METHOD_UNPIN, null, extras);
+            holder.mProvider.call(METHOD_UNPIN, null, extras);
         } catch (RemoteException e) {
-            // Arbitrary and not worth documenting, as Activity
-            // Manager will kill this process shortly anyway.
-        } finally {
-            provider.close();
+            Log.e(TAG, "Unable to unpin slice", e);
         }
     }
 
@@ -492,26 +442,21 @@
      * Compat version of {@link android.app.slice.SliceManager#getPinnedSpecs(Uri)}.
      */
     public static Set<SliceSpec> getPinnedSpecs(Context context, Uri uri) {
-        ContentProviderClient provider = context.getContentResolver()
-                .acquireContentProviderClient(uri);
-        if (provider == null) {
+        ProviderHolder holder = acquireClient(context.getContentResolver(), uri);
+        if (holder.mProvider == null) {
             throw new IllegalArgumentException("Unknown URI " + uri);
         }
         try {
             Bundle extras = new Bundle();
             extras.putParcelable(EXTRA_BIND_URI, uri);
-            final Bundle res = provider.call(METHOD_GET_PINNED_SPECS, null, extras);
-            if (res == null) {
-                return null;
+            final Bundle res = holder.mProvider.call(METHOD_GET_PINNED_SPECS, null, extras);
+            if (res != null) {
+                return getSpecs(res);
             }
-            return getSpecs(res);
         } catch (RemoteException e) {
-            // Arbitrary and not worth documenting, as Activity
-            // Manager will kill this process shortly anyway.
-            return null;
-        } finally {
-            provider.close();
+            Log.e(TAG, "Unable to get pinned specs", e);
         }
+        return null;
     }
 
     /**
@@ -519,7 +464,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();
 
@@ -529,39 +475,41 @@
             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;
         }
         String authority = providers.get(0).providerInfo.authority;
         Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
                 .authority(authority).build();
-        try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
-            if (provider == null) {
+        try (ProviderHolder holder = acquireClient(resolver, uri)) {
+            if (holder.mProvider == null) {
                 throw new IllegalArgumentException("Unknown URI " + uri);
             }
             Bundle extras = new Bundle();
             extras.putParcelable(EXTRA_INTENT, intent);
-            final Bundle res = provider.call(METHOD_MAP_ONLY_INTENT, null, extras);
-            if (res == null) {
-                return null;
+            final Bundle res = holder.mProvider.call(METHOD_MAP_ONLY_INTENT, null, extras);
+            if (res != null) {
+                return res.getParcelable(EXTRA_SLICE);
             }
-            return res.getParcelable(EXTRA_SLICE);
         } catch (RemoteException e) {
-            // Arbitrary and not worth documenting, as Activity
-            // Manager will kill this process shortly anyway.
-            return null;
+            Log.e(TAG, "Unable to map slice", e);
         }
+        return null;
     }
 
     /**
@@ -569,22 +517,114 @@
      */
     public static @NonNull Collection<Uri> getSliceDescendants(Context context, @NonNull Uri uri) {
         ContentResolver resolver = context.getContentResolver();
-        try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
+        try (ProviderHolder holder = acquireClient(resolver, uri)) {
             Bundle extras = new Bundle();
             extras.putParcelable(EXTRA_BIND_URI, uri);
-            final Bundle res = provider.call(METHOD_GET_DESCENDANTS, null, extras);
-            return res.getParcelableArrayList(EXTRA_SLICE_DESCENDANTS);
+            final Bundle res = holder.mProvider.call(METHOD_GET_DESCENDANTS, null, extras);
+            if (res != null) {
+                return res.getParcelableArrayList(EXTRA_SLICE_DESCENDANTS);
+            }
         } catch (RemoteException e) {
             Log.e(TAG, "Unable to get slice descendants", e);
         }
         return Collections.emptyList();
     }
 
-    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 android.app.slice.SliceManager#checkSlicePermission}.
+     */
+    public static int checkSlicePermission(Context context, String packageName, Uri uri, int pid,
+            int uid) {
+        ContentResolver resolver = context.getContentResolver();
+        try (ProviderHolder holder = acquireClient(resolver, 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 = holder.mProvider.call(METHOD_CHECK_PERMISSION, null, extras);
+            if (res != null) {
+                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 (ProviderHolder holder = acquireClient(resolver, uri)) {
+            Bundle extras = new Bundle();
+            extras.putParcelable(EXTRA_BIND_URI, uri);
+            extras.putString(EXTRA_PROVIDER_PKG, packageName);
+            extras.putString(EXTRA_PKG, toPackage);
+
+            holder.mProvider.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 (ProviderHolder holder = acquireClient(resolver, uri)) {
+            Bundle extras = new Bundle();
+            extras.putParcelable(EXTRA_BIND_URI, uri);
+            extras.putString(EXTRA_PROVIDER_PKG, packageName);
+            extras.putString(EXTRA_PKG, toPackage);
+
+            holder.mProvider.call(METHOD_REVOKE_PERMISSION, null, extras);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Unable to get slice descendants", e);
+        }
+    }
+
+    /**
+     * Compat version of {@link android.app.slice.SliceManager#getPinnedSlices}.
+     */
+    public static List<Uri> getPinnedSlices(Context context) {
+        ArrayList<Uri> pinnedSlices = new ArrayList<>();
+        SharedPreferences prefs = context.getSharedPreferences(ALL_FILES, 0);
+        Set<String> prefSet = prefs.getStringSet(ALL_FILES, Collections.<String>emptySet());
+        for (String pref : prefSet) {
+            pinnedSlices.addAll(new CompatPinnedList(context, pref).getPinnedSlices());
+        }
+        return pinnedSlices;
+    }
+
+    private static ProviderHolder acquireClient(ContentResolver resolver, Uri uri) {
+        ContentProviderClient provider = resolver.acquireContentProviderClient(uri);
+        if (provider == null) {
+            throw new IllegalArgumentException("No provider found for " + uri);
+        }
+        return new ProviderHolder(provider);
+    }
+
+    private static class ProviderHolder implements AutoCloseable {
+        private final ContentProviderClient mProvider;
+
+        ProviderHolder(ContentProviderClient provider) {
+            this.mProvider = provider;
+        }
+
+        @Override
+        public void close() {
+            if (mProvider == null) return;
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                mProvider.close();
+            } else {
+                mProvider.release();
+            }
+        }
+    }
 }
diff --git a/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java b/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
index 5118cc7..78eea93 100644
--- a/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
+++ b/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
@@ -22,7 +22,9 @@
 import android.app.slice.Slice;
 import android.app.slice.SliceProvider;
 import android.app.slice.SliceSpec;
+import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ProviderInfo;
 import android.net.Uri;
 
 import androidx.annotation.NonNull;
@@ -46,13 +48,21 @@
 
         private androidx.slice.SliceProvider mSliceProvider;
 
-        public SliceProviderWrapper(androidx.slice.SliceProvider provider) {
+        public SliceProviderWrapper(androidx.slice.SliceProvider provider,
+                String[] autoGrantPermissions) {
+            super(autoGrantPermissions);
             mSliceProvider = provider;
         }
 
         @Override
+        public void attachInfo(Context context, ProviderInfo info) {
+            mSliceProvider.attachInfo(context, info);
+            super.attachInfo(context, info);
+        }
+
+        @Override
         public boolean onCreate() {
-            return mSliceProvider.onCreateSliceProvider();
+            return true;
         }
 
         @Override
diff --git a/slices/core/src/main/java/androidx/slice/core/SliceActionImpl.java b/slices/core/src/main/java/androidx/slice/core/SliceActionImpl.java
index b5ca913..4fb81a6 100644
--- a/slices/core/src/main/java/androidx/slice/core/SliceActionImpl.java
+++ b/slices/core/src/main/java/androidx/slice/core/SliceActionImpl.java
@@ -27,7 +27,6 @@
 import static android.app.slice.SliceItem.FORMAT_ACTION;
 import static android.app.slice.SliceItem.FORMAT_IMAGE;
 import static android.app.slice.SliceItem.FORMAT_INT;
-import static android.app.slice.SliceItem.FORMAT_SLICE;
 import static android.app.slice.SliceItem.FORMAT_TEXT;
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
@@ -64,6 +63,7 @@
     private boolean mIsChecked;
     private int mPriority = -1;
     private SliceItem mSliceItem;
+    private SliceItem mActionItem;
 
     /**
      * Construct a SliceAction representing a tappable icon.
@@ -147,40 +147,36 @@
     @RestrictTo(LIBRARY)
     public SliceActionImpl(SliceItem slice) {
         mSliceItem = slice;
-        if (slice.hasHint(HINT_SHORTCUT) && FORMAT_SLICE.equals(slice.getFormat())) {
-            SliceItem actionItem = SliceQuery.find(slice, FORMAT_ACTION);
-            if (actionItem == null) {
-                // Can't have action slice without action
-                return;
-            }
-            mAction = actionItem.getAction();
-            SliceItem iconItem = SliceQuery.find(actionItem.getSlice(), FORMAT_IMAGE);
-            if (iconItem != null) {
-                mIcon = iconItem.getIcon();
-                mImageMode = iconItem.hasHint(HINT_NO_TINT)
-                        ? iconItem.hasHint(HINT_LARGE) ? LARGE_IMAGE : SMALL_IMAGE
-                        : ICON_IMAGE;
-            }
-            SliceItem titleItem = SliceQuery.find(actionItem.getSlice(), FORMAT_TEXT, HINT_TITLE,
-                    null /* nonHints */);
-            if (titleItem != null) {
-                mTitle = titleItem.getText();
-            }
-            SliceItem cdItem = SliceQuery.findSubtype(actionItem.getSlice(), FORMAT_TEXT,
-                    SUBTYPE_CONTENT_DESCRIPTION);
-            if (cdItem != null) {
-                mContentDescription = cdItem.getText();
-            }
-            mIsToggle = SUBTYPE_TOGGLE.equals(actionItem.getSubType());
-            if (mIsToggle) {
-                mIsChecked = actionItem.hasHint(HINT_SELECTED);
-            }
-            SliceItem priority = SliceQuery.findSubtype(actionItem.getSlice(), FORMAT_INT,
-                    SUBTYPE_PRIORITY);
-            mPriority = priority != null ? priority.getInt() : -1;
-        } else if (FORMAT_ACTION.equals(slice.getFormat())) {
-            mAction = slice.getAction();
+        SliceItem actionItem = SliceQuery.find(slice, FORMAT_ACTION);
+        if (actionItem == null) {
+            // Can't have action slice without action
+            return;
         }
+        mActionItem = actionItem;
+        SliceItem iconItem = SliceQuery.find(actionItem.getSlice(), FORMAT_IMAGE);
+        if (iconItem != null) {
+            mIcon = iconItem.getIcon();
+            mImageMode = iconItem.hasHint(HINT_NO_TINT)
+                    ? iconItem.hasHint(HINT_LARGE) ? LARGE_IMAGE : SMALL_IMAGE
+                    : ICON_IMAGE;
+        }
+        SliceItem titleItem = SliceQuery.find(actionItem.getSlice(), FORMAT_TEXT, HINT_TITLE,
+                null /* nonHints */);
+        if (titleItem != null) {
+            mTitle = titleItem.getText();
+        }
+        SliceItem cdItem = SliceQuery.findSubtype(actionItem.getSlice(), FORMAT_TEXT,
+                SUBTYPE_CONTENT_DESCRIPTION);
+        if (cdItem != null) {
+            mContentDescription = cdItem.getText();
+        }
+        mIsToggle = SUBTYPE_TOGGLE.equals(actionItem.getSubType());
+        if (mIsToggle) {
+            mIsChecked = actionItem.hasHint(HINT_SELECTED);
+        }
+        SliceItem priority = SliceQuery.findSubtype(actionItem.getSlice(), FORMAT_INT,
+                SUBTYPE_PRIORITY);
+        mPriority = priority != null ? priority.getInt() : -1;
     }
 
     /**
@@ -218,7 +214,15 @@
     @NonNull
     @Override
     public PendingIntent getAction() {
-        return mAction;
+        return mAction != null ? mAction : mActionItem.getAction();
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public SliceItem getActionItem() {
+        return mActionItem;
     }
 
     /**
diff --git a/slices/core/src/main/java/androidx/slice/core/SliceHints.java b/slices/core/src/main/java/androidx/slice/core/SliceHints.java
index 588ff61..c29ad56 100644
--- a/slices/core/src/main/java/androidx/slice/core/SliceHints.java
+++ b/slices/core/src/main/java/androidx/slice/core/SliceHints.java
@@ -18,9 +18,13 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 import androidx.annotation.IntDef;
 import androidx.annotation.RestrictTo;
 
+import java.lang.annotation.Retention;
+
 /**
  * Temporary class to contain hint constants for slices to be used.
  * @hide
@@ -69,6 +73,7 @@
     @IntDef({
             LARGE_IMAGE, SMALL_IMAGE, ICON_IMAGE, UNKNOWN_IMAGE
     })
+    @Retention(SOURCE)
     public @interface ImageMode{}
 
     /**
diff --git a/slices/core/src/main/res/values-as/strings.xml b/slices/core/src/main/res/values-as/strings.xml
new file mode 100644
index 0000000..95722b9
--- /dev/null
+++ b/slices/core/src/main/res/values-as/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+~ Copyright 2018 The Android Open Source Project
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~      http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="abc_slices_permission_request" msgid="3604847235923472451">"<xliff:g id="APP_0">%1$s</xliff:g>এ <xliff:g id="APP_2">%2$s</xliff:g>ৰ অংশ দেখুওৱাব খুজিছে"</string>
+    <string name="abc_slice_permission_title" msgid="4175332421259324948">"<xliff:g id="APP_0">%1$s</xliff:g>ক <xliff:g id="APP_2">%2$s</xliff:g>ৰ অংশ দেখুওৱাবলৈ অনুমতি দিবনে?"</string>
+    <string name="abc_slice_permission_text_1" msgid="4525743640399572811">"- ই <xliff:g id="APP">%1$s</xliff:g>ৰ তথ্য পঢ়িব পাৰে"</string>
+    <string name="abc_slice_permission_text_2" msgid="7323565634860251794">"- ই <xliff:g id="APP">%1$s</xliff:g>ৰ ভিতৰত কাৰ্য কৰিব পাৰে"</string>
+    <string name="abc_slice_permission_checkbox" msgid="5696872682700058611">"<xliff:g id="APP">%1$s</xliff:g>ক যিকোনো এপৰ অংশ দেখুওৱাবলৈ অনুমতি দিয়ক"</string>
+    <string name="abc_slice_permission_allow" msgid="5024599872061409708">"অনুমতি দিয়ক"</string>
+    <string name="abc_slice_permission_deny" msgid="3819478292430407705">"অস্বীকাৰ কৰক"</string>
+</resources>
diff --git a/slices/core/src/main/res/values-be/strings.xml b/slices/core/src/main/res/values-be/strings.xml
index c9fc9a8..5fadc3f 100644
--- a/slices/core/src/main/res/values-be/strings.xml
+++ b/slices/core/src/main/res/values-be/strings.xml
@@ -17,11 +17,11 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="abc_slices_permission_request" msgid="3604847235923472451">"Праграма <xliff:g id="APP_0">%1$s</xliff:g> запытвае дазвол на паказ зрэзаў праграмы <xliff:g id="APP_2">%2$s</xliff:g>"</string>
-    <string name="abc_slice_permission_title" msgid="4175332421259324948">"Дазволіць праграме <xliff:g id="APP_0">%1$s</xliff:g> паказваць зрэзы праграмы <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
+    <string name="abc_slices_permission_request" msgid="3604847235923472451">"Праграма <xliff:g id="APP_0">%1$s</xliff:g> запытвае дазвол на паказ фрагментаў праграмы <xliff:g id="APP_2">%2$s</xliff:g>"</string>
+    <string name="abc_slice_permission_title" msgid="4175332421259324948">"Дазволіць праграме <xliff:g id="APP_0">%1$s</xliff:g> паказваць фрагменты праграмы <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="abc_slice_permission_text_1" msgid="4525743640399572811">"- Можа счытваць інфармацыю з праграмы <xliff:g id="APP">%1$s</xliff:g>"</string>
     <string name="abc_slice_permission_text_2" msgid="7323565634860251794">"- Можа выконваць дзеянні ў праграме <xliff:g id="APP">%1$s</xliff:g>"</string>
-    <string name="abc_slice_permission_checkbox" msgid="5696872682700058611">"Дазволіць праграме <xliff:g id="APP">%1$s</xliff:g> паказваць зрэзы іншых праграм"</string>
+    <string name="abc_slice_permission_checkbox" msgid="5696872682700058611">"Дазволіць праграме <xliff:g id="APP">%1$s</xliff:g> паказваць фрагменты іншых праграм"</string>
     <string name="abc_slice_permission_allow" msgid="5024599872061409708">"Дазволіць"</string>
     <string name="abc_slice_permission_deny" msgid="3819478292430407705">"Адмовіць"</string>
 </resources>
diff --git a/slices/core/src/main/res/values-ja/strings.xml b/slices/core/src/main/res/values-ja/strings.xml
index a04df0b..315326f 100644
--- a/slices/core/src/main/res/values-ja/strings.xml
+++ b/slices/core/src/main/res/values-ja/strings.xml
@@ -19,7 +19,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slices_permission_request" msgid="3604847235923472451">"<xliff:g id="APP_0">%1$s</xliff:g> が <xliff:g id="APP_2">%2$s</xliff:g> のスライスの表示をリクエストしています"</string>
     <string name="abc_slice_permission_title" msgid="4175332421259324948">"<xliff:g id="APP_2">%2$s</xliff:g> のスライスの表示を <xliff:g id="APP_0">%1$s</xliff:g> に許可しますか?"</string>
-    <string name="abc_slice_permission_text_1" msgid="4525743640399572811">"- <xliff:g id="APP">%1$s</xliff:g> からの情報を読み取ることがあります"</string>
+    <string name="abc_slice_permission_text_1" msgid="4525743640399572811">"- <xliff:g id="APP">%1$s</xliff:g> からの情報を読み取ることができます"</string>
     <string name="abc_slice_permission_text_2" msgid="7323565634860251794">"- <xliff:g id="APP">%1$s</xliff:g> 内部で操作することがあります"</string>
     <string name="abc_slice_permission_checkbox" msgid="5696872682700058611">"すべてのアプリのスライスを表示することを <xliff:g id="APP">%1$s</xliff:g> に許可する"</string>
     <string name="abc_slice_permission_allow" msgid="5024599872061409708">"許可"</string>
diff --git a/slices/core/src/main/res/values-or/strings.xml b/slices/core/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000..f6b9443
--- /dev/null
+++ b/slices/core/src/main/res/values-or/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+~ Copyright 2018 The Android Open Source Project
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~      http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="abc_slices_permission_request" msgid="3604847235923472451">"<xliff:g id="APP_0">%1$s</xliff:g>, <xliff:g id="APP_2">%2$s</xliff:g> ସ୍ଲାଇସ୍‌କୁ ଦେଖାଇବା ପାଇଁ ଚାହେଁ"</string>
+    <string name="abc_slice_permission_title" msgid="4175332421259324948">"<xliff:g id="APP_2">%2$s</xliff:g> ସ୍ଲାଇସ୍‌କୁ ଦେଖାଇବା ପାଇଁ <xliff:g id="APP_0">%1$s</xliff:g>କୁ ଅନୁମତି ଦେବେ?"</string>
+    <string name="abc_slice_permission_text_1" msgid="4525743640399572811">"- ଏହା <xliff:g id="APP">%1$s</xliff:g>ରୁ ସୂଚନାକୁ ପଢ଼ିପାରିବ"</string>
+    <string name="abc_slice_permission_text_2" msgid="7323565634860251794">"- ଏହା <xliff:g id="APP">%1$s</xliff:g> ଭିତରେ କାମ କରିପାରିବ"</string>
+    <string name="abc_slice_permission_checkbox" msgid="5696872682700058611">"ଯେକୌଣସି ଆପ୍‌ରେ ସ୍ଲାଇସ୍‌କୁ ଦେଖାଇବା ପାଇଁ <xliff:g id="APP">%1$s</xliff:g>କୁ ଅନୁମତି ଦିଅନ୍ତୁ"</string>
+    <string name="abc_slice_permission_allow" msgid="5024599872061409708">"ଅନୁମତି ଦିଅନ୍ତୁ"</string>
+    <string name="abc_slice_permission_deny" msgid="3819478292430407705">"ଅସ୍ଵୀକାର କରନ୍ତୁ"</string>
+</resources>
diff --git a/slices/view/api/current.txt b/slices/view/api/current.txt
index 04d4960..7be0368 100644
--- a/slices/view/api/current.txt
+++ b/slices/view/api/current.txt
@@ -5,6 +5,8 @@
     method public abstract androidx.slice.Slice bindSlice(android.content.Intent);
     method public abstract int checkSlicePermission(android.net.Uri, int, int);
     method public static androidx.slice.SliceManager getInstance(android.content.Context);
+    method public abstract java.util.List<android.net.Uri> getPinnedSlices();
+    method public abstract java.util.Set<androidx.slice.SliceSpec> getPinnedSpecs(android.net.Uri);
     method public abstract java.util.Collection<android.net.Uri> getSliceDescendants(android.net.Uri);
     method public abstract void grantSlicePermission(java.lang.String, android.net.Uri);
     method public abstract android.net.Uri mapIntentToUri(android.content.Intent);
@@ -24,6 +26,7 @@
     method public static androidx.slice.SliceMetadata from(android.content.Context, androidx.slice.Slice);
     method public long getExpiry();
     method public int getHeaderType();
+    method public android.app.PendingIntent getInputRangeAction();
     method public long getLastUpdatedTime();
     method public int getLoadingState();
     method public androidx.slice.core.SliceAction getPrimaryAction();
@@ -43,8 +46,8 @@
     method public static deprecated int getLoadingState(androidx.slice.Slice);
     method public static deprecated java.util.List<androidx.slice.SliceItem> getSliceActions(androidx.slice.Slice);
     method public static deprecated java.util.List<java.lang.String> getSliceKeywords(androidx.slice.Slice);
-    method public static androidx.slice.Slice parseSlice(java.io.InputStream, java.lang.String) throws java.io.IOException;
-    method public static void serializeSlice(androidx.slice.Slice, android.content.Context, java.io.OutputStream, java.lang.String, androidx.slice.SliceUtils.SerializeOptions) throws java.io.IOException;
+    method public static androidx.slice.Slice parseSlice(android.content.Context, java.io.InputStream, java.lang.String, androidx.slice.SliceUtils.SliceActionListener) throws java.io.IOException, androidx.slice.SliceUtils.SliceParseException;
+    method public static void serializeSlice(androidx.slice.Slice, android.content.Context, java.io.OutputStream, java.lang.String, androidx.slice.SliceUtils.SerializeOptions) throws java.io.IOException, java.lang.IllegalArgumentException;
     field public static final deprecated int LOADING_ALL = 0; // 0x0
     field public static final deprecated int LOADING_COMPLETE = 2; // 0x2
     field public static final deprecated int LOADING_PARTIAL = 1; // 0x1
@@ -54,11 +57,20 @@
     ctor public SliceUtils.SerializeOptions();
     method public androidx.slice.SliceUtils.SerializeOptions setActionMode(int);
     method public androidx.slice.SliceUtils.SerializeOptions setImageMode(int);
-    field public static final int MODE_DISABLE = 2; // 0x2
+    method public androidx.slice.SliceUtils.SerializeOptions setMaxImageHeight(int);
+    method public androidx.slice.SliceUtils.SerializeOptions setMaxImageWidth(int);
+    field public static final int MODE_CONVERT = 2; // 0x2
     field public static final int MODE_REMOVE = 1; // 0x1
     field public static final int MODE_THROW = 0; // 0x0
   }
 
+  public static abstract interface SliceUtils.SliceActionListener {
+    method public abstract void onSliceAction(android.net.Uri);
+  }
+
+  public static class SliceUtils.SliceParseException extends java.lang.Exception {
+  }
+
 }
 
 package androidx.slice.widget {
@@ -108,12 +120,13 @@
     method public java.util.List<androidx.slice.SliceItem> getSliceActions();
     method public void onChanged(androidx.slice.Slice);
     method public void onClick(android.view.View);
+    method public void setAccentColor(int);
     method public void setMode(int);
     method public void setOnSliceActionListener(androidx.slice.widget.SliceView.OnSliceActionListener);
     method public void setScrollable(boolean);
     method public void setSlice(androidx.slice.Slice);
     method public void setSliceActions(java.util.List<androidx.slice.SliceItem>);
-    method public void setTint(int);
+    method public deprecated void setTint(int);
     field public static final int MODE_LARGE = 2; // 0x2
     field public static final int MODE_SHORTCUT = 3; // 0x3
     field public static final int MODE_SMALL = 1; // 0x1
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 5564f72..e0e8f51 100644
--- a/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java
+++ b/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java
@@ -16,7 +16,10 @@
 
 package androidx.slice;
 
+import static androidx.slice.compat.SliceProviderCompat.PERMS_PREFIX;
+
 import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
@@ -36,8 +39,8 @@
 
 import androidx.annotation.NonNull;
 import androidx.core.os.BuildCompat;
+import androidx.slice.compat.CompatPermissionManager;
 import androidx.slice.render.SliceRenderActivity;
-import androidx.slice.widget.SliceLiveData;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -45,6 +48,7 @@
 
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
 import java.util.concurrent.Executor;
 
 @RunWith(AndroidJUnit4.class)
@@ -89,6 +93,28 @@
     }
 
     @Test
+    public void testPinList() {
+        Uri uri = new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(mContext.getPackageName())
+                .build();
+        Uri longerUri = uri.buildUpon().appendPath("something").build();
+        try {
+            mManager.pinSlice(uri);
+            mManager.pinSlice(longerUri);
+            verify(mSliceProvider, timeout(2000)).onSlicePinned(eq(longerUri));
+
+            List<Uri> uris = mManager.getPinnedSlices();
+            assertEquals(2, uris.size());
+            assertTrue(uris.contains(uri));
+            assertTrue(uris.contains(longerUri));
+        } finally {
+            mManager.unpinSlice(uri);
+            mManager.unpinSlice(longerUri);
+        }
+    }
+
+    @Test
     public void testCallback() {
         if (BuildCompat.isAtLeastP()) {
             return;
@@ -124,7 +150,8 @@
         mManager.pinSlice(uri);
         verify(mSliceProvider).onSlicePinned(eq(uri));
 
-        assertEquals(SliceLiveData.SUPPORTED_SPECS, mManager.getPinnedSpecs(uri));
+        // Disabled while we update APIs.
+        //assertEquals(SliceLiveData.SUPPORTED_SPECS, mManager.getPinnedSpecs(uri));
     }
 
     @Test
@@ -145,8 +172,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
@@ -212,6 +239,12 @@
             }
         }
 
+        protected CompatPermissionManager onCreatePermissionManager(
+                String[] autoGrantPermissions) {
+            return new CompatPermissionManager(getContext(), PERMS_PREFIX + getClass().getName(),
+                    -1 /* Different uid to run permissions */, autoGrantPermissions);
+        }
+
         @Override
         public Collection<Uri> onGetSliceDescendants(Uri uri) {
             if (sSliceProviderReceiver != null) {
diff --git a/slices/view/src/androidTest/java/androidx/slice/SliceMetadataTest.java b/slices/view/src/androidTest/java/androidx/slice/SliceMetadataTest.java
index 2662368..78c8cf6 100644
--- a/slices/view/src/androidTest/java/androidx/slice/SliceMetadataTest.java
+++ b/slices/view/src/androidTest/java/androidx/slice/SliceMetadataTest.java
@@ -16,7 +16,6 @@
 
 package androidx.slice;
 
-import static android.app.slice.Slice.HINT_PARTIAL;
 import static android.app.slice.Slice.HINT_TITLE;
 
 import static androidx.slice.SliceMetadata.LOADED_ALL;
@@ -24,6 +23,7 @@
 import static androidx.slice.SliceMetadata.LOADED_PARTIAL;
 import static androidx.slice.builders.ListBuilder.ICON_IMAGE;
 import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
+import static androidx.slice.core.SliceHints.INFINITY;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertTrue;
@@ -36,7 +36,6 @@
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
-import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SmallTest;
@@ -47,7 +46,6 @@
 import androidx.slice.builders.GridRowBuilder;
 import androidx.slice.builders.ListBuilder;
 import androidx.slice.builders.SliceAction;
-import androidx.slice.compat.SliceProviderCompat;
 import androidx.slice.core.SliceActionImpl;
 import androidx.slice.core.SliceHints;
 import androidx.slice.render.SliceRenderActivity;
@@ -93,7 +91,7 @@
         PendingIntent pi = getIntent("");
         Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
         new Canvas(b).drawColor(0xffff0000);
-        IconCompat icon = IconCompat.createFromIcon(Icon.createWithBitmap(b));
+        IconCompat icon = IconCompat.createWithBitmap(b);
 
         SliceAction action1 = new SliceAction(pi, icon, "action1");
         SliceAction action2 = new SliceAction(pi, icon, "action2");
@@ -124,7 +122,7 @@
         PendingIntent pi = getIntent("");
         Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
         new Canvas(b).drawColor(0xffff0000);
-        IconCompat icon = IconCompat.createFromIcon(Icon.createWithBitmap(b));
+        IconCompat icon = IconCompat.createWithBitmap(b);
 
         SliceAction primaryAction = new SliceAction(pi, icon, "action");
 
@@ -147,7 +145,7 @@
         PendingIntent pi = getIntent("");
         Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
         new Canvas(b).drawColor(0xffff0000);
-        IconCompat icon = IconCompat.createFromIcon(Icon.createWithBitmap(b));
+        IconCompat icon = IconCompat.createWithBitmap(b);
 
         SliceAction primaryAction = new SliceAction(pi, icon, "action");
         SliceAction endAction = new SliceAction(pi, "toogle action", false);
@@ -170,7 +168,7 @@
         PendingIntent pi = getIntent("");
         Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
         new Canvas(b).drawColor(0xffff0000);
-        IconCompat icon = IconCompat.createFromIcon(Icon.createWithBitmap(b));
+        IconCompat icon = IconCompat.createWithBitmap(b);
 
         SliceAction primaryAction = new SliceAction(pi, icon, "action");
         SliceAction sliceAction = new SliceAction(pi, "another action", true);
@@ -193,7 +191,7 @@
         PendingIntent pi = getIntent("");
         Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
         new Canvas(b).drawColor(0xffff0000);
-        IconCompat icon = IconCompat.createFromIcon(Icon.createWithBitmap(b));
+        IconCompat icon = IconCompat.createWithBitmap(b);
 
         SliceAction endAction1 = new SliceAction(pi, icon, "action");
         SliceAction endAction2 = new SliceAction(pi, "toogle action", false);
@@ -264,7 +262,7 @@
                 .setTitle("another title")
                 .setValue(5)
                 .setMax(10)
-                .setAction(pi));
+                .setInputAction(pi));
 
         Slice sliderSlice = lb.build();
         SliceMetadata sliderInfo = SliceMetadata.from(mContext, sliderSlice);
@@ -306,22 +304,22 @@
         Uri uri = Uri.parse("content://pkg/slice");
         Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
         new Canvas(b).drawColor(0xffff0000);
-        Icon icon = Icon.createWithBitmap(b);
+        IconCompat icon = IconCompat.createWithBitmap(b);
 
         ListBuilder lb = new ListBuilder(mContext, uri, ListBuilder.INFINITY);
         GridRowBuilder grb = new GridRowBuilder(lb);
         grb.addCell(new GridRowBuilder.CellBuilder(grb)
                 .addText("some text")
                 .addText("more text")
-                .addImage(IconCompat.createFromIcon(icon), ICON_IMAGE));
+                .addImage(icon, ICON_IMAGE));
         grb.addCell(new GridRowBuilder.CellBuilder(grb)
                 .addText("some text")
                 .addText("more text")
-                .addImage(IconCompat.createFromIcon(icon), ICON_IMAGE));
+                .addImage(icon, ICON_IMAGE));
         grb.addCell(new GridRowBuilder.CellBuilder(grb)
                 .addText("some text")
                 .addText("more text")
-                .addImage(IconCompat.createFromIcon(icon), ICON_IMAGE));
+                .addImage(icon, ICON_IMAGE));
         lb.addGridRow(grb);
 
         Slice gridSlice = lb.build();
@@ -408,7 +406,7 @@
         PendingIntent pi = getIntent("");
         Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
         new Canvas(b).drawColor(0xffff0000);
-        IconCompat icon = IconCompat.createFromIcon(Icon.createWithBitmap(b));
+        IconCompat icon = IconCompat.createWithBitmap(b);
 
         SliceAction toggleAction = new SliceAction(pi, icon, "toggle", false /* isChecked */);
         SliceAction toggleAction2 = new SliceAction(pi, icon, "toggle2", true /* isChecked */);
@@ -469,6 +467,31 @@
     }
 
     @Test
+    public void testGetInputRangeAction() {
+        Uri uri = Uri.parse("content://pkg/slice");
+        PendingIntent expectedIntent = getIntent("rangeintent");
+
+        Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
+        new Canvas(b).drawColor(0xffff0000);
+        IconCompat icon = IconCompat.createWithBitmap(b);
+        SliceAction primaryAction = new SliceAction(getIntent(""), icon, "action");
+
+        ListBuilder lb = new ListBuilder(mContext, uri, ListBuilder.INFINITY);
+        lb.addInputRange(new ListBuilder.InputRangeBuilder(lb)
+                .setTitle("another title")
+                .setValue(7)
+                .setMin(5)
+                .setMax(10)
+                .setPrimaryAction(primaryAction)
+                .setInputAction(expectedIntent));
+        Slice sliderSlice = lb.build();
+
+        SliceMetadata sliderInfo = SliceMetadata.from(mContext, sliderSlice);
+        assertEquals(expectedIntent, sliderInfo.getInputRangeAction());
+        assertEquivalent(primaryAction, sliderInfo.getPrimaryAction());
+    }
+
+    @Test
     public void testGetRangeForProgress() {
         Uri uri = Uri.parse("content://pkg/slice");
 
@@ -528,17 +551,23 @@
     @Test
     public void testGetLoadingState() {
         Uri uri = Uri.parse("content://pkg/slice");
-        Slice s1 = new Slice.Builder(uri).build();
+        Slice s1 = new ListBuilder(mContext, uri, INFINITY).build();
         SliceMetadata SliceMetadata1 = SliceMetadata.from(mContext, s1);
         int actualState1 = SliceMetadata1.getLoadingState();
         assertEquals(LOADED_NONE, actualState1);
 
-        Slice s2 = new Slice.Builder(uri).addText(null, null, HINT_PARTIAL).build();
+        ListBuilder lb = new ListBuilder(mContext, uri, INFINITY);
+        Slice s2 = lb.addRow(new ListBuilder.RowBuilder(lb)
+                .setTitle(null, true /* isLoading */))
+                .build();
         SliceMetadata SliceMetadata2 = SliceMetadata.from(mContext, s2);
         int actualState2 = SliceMetadata2.getLoadingState();
         assertEquals(LOADED_PARTIAL, actualState2);
 
-        Slice s3 = new Slice.Builder(uri).addText("Text", null).build();
+        ListBuilder lb2 = new ListBuilder(mContext, uri, INFINITY);
+        Slice s3 = lb2.addRow(new ListBuilder.RowBuilder(lb2)
+                .setTitle("Title", false /* isLoading */))
+                .build();
         SliceMetadata SliceMetadata3 = SliceMetadata.from(mContext, s3);
         int actualState3 = SliceMetadata3.getLoadingState();
         assertEquals(LOADED_ALL, actualState3);
@@ -551,9 +580,9 @@
         long ttl = TimeUnit.DAYS.toMillis(1);
         Slice ttlSlice = new Slice.Builder(uri)
                 .addText("Some text", null)
-                .addTimestamp(timestamp, null)
-                .addTimestamp(timestamp, null, SliceHints.HINT_LAST_UPDATED)
-                .addTimestamp(ttl, null, SliceHints.HINT_TTL)
+                .addLong(timestamp, null)
+                .addLong(timestamp, null, SliceHints.HINT_LAST_UPDATED)
+                .addLong(ttl, null, SliceHints.HINT_TTL)
                 .build();
 
         SliceMetadata si1 = SliceMetadata.from(mContext, ttlSlice);
@@ -562,7 +591,7 @@
 
         Slice noTtlSlice = new Slice.Builder(uri)
                 .addText("Some text", null)
-                .addTimestamp(timestamp, null).build();
+                .addLong(timestamp, null).build();
         SliceMetadata si2 = SliceMetadata.from(mContext, noTtlSlice);
         long retrievedTtl2 = si2.getExpiry();
         assertEquals(0, retrievedTtl2);
@@ -575,9 +604,9 @@
         long ttl = TimeUnit.DAYS.toMillis(1);
         Slice ttlSlice = new Slice.Builder(uri)
                 .addText("Some text", null)
-                .addTimestamp(timestamp - 20, null)
-                .addTimestamp(timestamp, null, SliceHints.HINT_LAST_UPDATED)
-                .addTimestamp(ttl, null, SliceHints.HINT_TTL)
+                .addLong(timestamp - 20, null)
+                .addLong(timestamp, null, SliceHints.HINT_LAST_UPDATED)
+                .addLong(ttl, null, SliceHints.HINT_TTL)
                 .build();
 
         SliceMetadata si1 = SliceMetadata.from(mContext, ttlSlice);
@@ -586,7 +615,7 @@
 
         Slice noTtlSlice = new Slice.Builder(uri)
                 .addText("Some text", null)
-                .addTimestamp(timestamp, null).build();
+                .addLong(timestamp, null).build();
 
         SliceMetadata si2 = SliceMetadata.from(mContext, noTtlSlice);
         long retrievedLastUpdated2 = si2.getLastUpdatedTime();
@@ -597,7 +626,7 @@
     public void testIsPermissionSlice() {
         Uri uri = Uri.parse("content://pkg/slice");
         Slice permissionSlice =
-                SliceProviderCompat.createPermissionSlice(mContext, uri, mContext.getPackageName());
+                SliceProvider.createPermissionSlice(mContext, uri, mContext.getPackageName());
 
         SliceMetadata metadata = SliceMetadata.from(mContext, permissionSlice);
         assertEquals(true, metadata.isPermissionSlice());
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/androidTest/java/androidx/slice/SliceXmlTest.java b/slices/view/src/androidTest/java/androidx/slice/SliceXmlTest.java
index 298d498..d1a647c 100644
--- a/slices/view/src/androidTest/java/androidx/slice/SliceXmlTest.java
+++ b/slices/view/src/androidTest/java/androidx/slice/SliceXmlTest.java
@@ -24,6 +24,11 @@
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertTrue;
 
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.app.PendingIntent;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
@@ -33,6 +38,8 @@
 import android.support.test.runner.AndroidJUnit4;
 
 import androidx.core.graphics.drawable.IconCompat;
+import androidx.slice.core.SliceQuery;
+import androidx.slice.view.R;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -51,7 +58,7 @@
     public void testThrowForAction() throws IOException {
         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
         Slice s = new Slice.Builder(Uri.parse("content://pkg/slice"))
-                .addAction(null, null, null)
+                .addAction((PendingIntent) null, null, null)
                 .build();
         SliceUtils.serializeSlice(s, mContext, outputStream, "UTF-8", new SliceUtils
                 .SerializeOptions());
@@ -81,7 +88,7 @@
     public void testNoThrowForAction() throws IOException {
         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
         Slice s = new Slice.Builder(Uri.parse("content://pkg/slice"))
-                .addAction(null, null, null)
+                .addAction((PendingIntent) null, null, null)
                 .build();
         SliceUtils.serializeSlice(s, mContext, outputStream, "UTF-8", new SliceUtils
                 .SerializeOptions().setActionMode(SliceUtils.SerializeOptions.MODE_REMOVE));
@@ -108,7 +115,7 @@
     }
 
     @Test
-    public void testSerialization() throws IOException {
+    public void testSerialization() throws Exception {
         Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
         new Canvas(b).drawColor(0xffff0000);
         // Create a slice containing all the types in a hierarchy.
@@ -118,24 +125,32 @@
                         .build())
                 .addIcon(IconCompat.createWithBitmap(b), null)
                 .addText("Some text", null)
-                .addAction(null, new Slice.Builder(Uri.parse("content://pkg/slice/sub"))
+                .addAction((PendingIntent) null,
+                        new Slice.Builder(Uri.parse("content://pkg/slice/action"))
                         .addText("Action text", null)
                         .build(), null)
                 .addInt(0xff00ff00, "subtype")
+                .addIcon(IconCompat.createWithResource(mContext, R.drawable.abc_slice_see_more_bg),
+                        null)
                 .addHints("Hint 1", "Hint 2")
                 .build();
         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
 
         SliceUtils.serializeSlice(before, mContext, outputStream, "UTF-8",
                 new SliceUtils.SerializeOptions()
-                        .setImageMode(SliceUtils.SerializeOptions.MODE_DISABLE)
-                        .setActionMode(SliceUtils.SerializeOptions.MODE_DISABLE));
+                        .setImageMode(SliceUtils.SerializeOptions.MODE_CONVERT)
+                        .setActionMode(SliceUtils.SerializeOptions.MODE_CONVERT));
 
         byte[] bytes = outputStream.toByteArray();
         ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
-        Slice after = SliceUtils.parseSlice(inputStream, "UTF-8");
+        SliceUtils.SliceActionListener listener = mock(SliceUtils.SliceActionListener.class);
+        Slice after = SliceUtils.parseSlice(mContext, inputStream, "UTF-8", listener);
 
         assertEquivalent(before, after);
+
+        SliceItem action = SliceQuery.find(after, FORMAT_ACTION);
+        action.fireAction(null, null);
+        verify(listener).onSliceAction(eq(Uri.parse("content://pkg/slice/action")));
     }
 
     private void assertEquivalent(Slice desired, Slice actual) {
diff --git a/slices/view/src/androidTest/java/androidx/slice/render/RenderTest.java b/slices/view/src/androidTest/java/androidx/slice/render/RenderTest.java
index deb1f90..4c3b6fc 100644
--- a/slices/view/src/androidTest/java/androidx/slice/render/RenderTest.java
+++ b/slices/view/src/androidTest/java/androidx/slice/render/RenderTest.java
@@ -16,12 +16,15 @@
 
 package androidx.slice.render;
 
+import static android.os.Build.VERSION.SDK_INT;
+
 import static androidx.slice.render.SliceRenderer.SCREENSHOT_DIR;
 
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.os.Build;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
@@ -29,6 +32,11 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -39,7 +47,7 @@
     private final Context mContext = InstrumentationRegistry.getContext();
 
     @Test
-    public void testRender() throws InterruptedException {
+    public void testRender() throws Exception {
         final CountDownLatch latch = new CountDownLatch(1);
         BroadcastReceiver receiver = new BroadcastReceiver() {
             @Override
@@ -53,8 +61,53 @@
                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
 
         latch.await(30000, TimeUnit.MILLISECONDS);
-        String path = mContext.getDataDir().toString() + "/" + SCREENSHOT_DIR;
-        InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
-                "mv " + path + " " + "/sdcard/");
+        String path = new File(mContext.getFilesDir(), SCREENSHOT_DIR).getAbsolutePath();
+        if (SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+                    "mv " + path + " " + "/sdcard/");
+        } else {
+            deleteDir(new File("/sdcard/" + SCREENSHOT_DIR));
+            copyDirectory(new File(path), new File("/sdcard/" + SCREENSHOT_DIR));
+        }
+    }
+
+
+    public static void copyDirectory(File sourceLocation, File targetLocation) throws Exception {
+        if (sourceLocation.isDirectory()) {
+            if (!targetLocation.exists()) {
+                targetLocation.mkdirs();
+            }
+
+            String[] children = sourceLocation.list();
+            for (int i = 0; i < children.length; i++) {
+                copyDirectory(new File(sourceLocation, children[i]), new File(
+                        targetLocation, children[i]));
+            }
+        } else {
+            copyFile(sourceLocation, targetLocation);
+        }
+    }
+
+    public static void copyFile(File sourceLocation, File targetLocation) throws Exception {
+        InputStream in = new FileInputStream(sourceLocation);
+        OutputStream out = new FileOutputStream(targetLocation);
+
+        byte[] buf = new byte[1024];
+        int len;
+        while ((len = in.read(buf)) > 0) {
+            out.write(buf, 0, len);
+        }
+        in.close();
+        out.close();
+    }
+
+    static void deleteDir(File file) {
+        File[] contents = file.listFiles();
+        if (contents != null) {
+            for (File f : contents) {
+                deleteDir(f);
+            }
+        }
+        file.delete();
     }
 }
diff --git a/slices/view/src/androidTest/java/androidx/slice/render/SliceCreator.java b/slices/view/src/androidTest/java/androidx/slice/render/SliceCreator.java
index f181617..5f08f3f 100644
--- a/slices/view/src/androidTest/java/androidx/slice/render/SliceCreator.java
+++ b/slices/view/src/androidTest/java/androidx/slice/render/SliceCreator.java
@@ -36,11 +36,11 @@
 
 import androidx.core.graphics.drawable.IconCompat;
 import androidx.slice.Slice;
+import androidx.slice.SliceProvider;
 import androidx.slice.builders.GridRowBuilder;
 import androidx.slice.builders.ListBuilder;
 import androidx.slice.builders.MessagingSliceBuilder;
 import androidx.slice.builders.SliceAction;
-import androidx.slice.compat.SliceProviderCompat;
 import androidx.slice.view.test.R;
 
 import java.util.Arrays;
@@ -57,9 +57,27 @@
             "com.example.androidx.slice.action.TOAST";
     public static final String EXTRA_TOAST_MESSAGE = "com.example.androidx.extra.TOAST_MESSAGE";
 
-    public static final String[] URI_PATHS = {"message", "wifi", "wifi2", "note", "ride",
-            "ride-ttl", "toggle", "toggle2", "contact", "gallery", "subscription", "subscription2",
-            "weather", "reservation", "inputrange", "range", "permission"};
+    public static final String[] URI_PATHS = {
+            "message",
+            "wifi",
+            "wifi2",
+            "note",
+            "ride",
+            "ride-ttl",
+            "toggle",
+            "toggle2",
+            "contact",
+            "gallery",
+            "subscription",
+            "subscription2",
+            "weather",
+            "reservation",
+            "inputrange",
+            "inputrange2",
+            "range",
+            "permission",
+            "empty",
+    };
 
     private final Context mContext;
 
@@ -115,10 +133,14 @@
                 return createReservationSlice(sliceUri);
             case "/inputrange":
                 return createStarRatingInputRange(sliceUri);
+            case "/inputrange2":
+                return createBasicInputRange(sliceUri);
             case "/range":
                 return createDownloadProgressRange(sliceUri);
             case "/permission":
                 return createPermissionSlice(sliceUri);
+            case "/empty":
+                return new ListBuilder(getContext(), sliceUri, INFINITY).build();
         }
         throw new IllegalArgumentException("Unknown uri " + sliceUri);
     }
@@ -236,7 +258,7 @@
                 "See contact info"), IconCompat.createWithResource(getContext(),
                 R.drawable.mady), SMALL_IMAGE, "Mady");
         GridRowBuilder gb = new GridRowBuilder(b);
-        return b.setColor(0xff3949ab)
+        return b.setAccentColor(0xff3949ab)
                 .addRow(rb
                         .setTitle("Mady Pitza")
                         .setSubtitle("Frequently contacted contact")
@@ -292,7 +314,7 @@
     private Slice createNoteSlice(Uri sliceUri) {
         // TODO: Remote input.
         ListBuilder lb = new ListBuilder(getContext(), sliceUri, INFINITY);
-        return lb.setColor(0xfff4b400)
+        return lb.setAccentColor(0xfff4b400)
                 .addRow(new ListBuilder.RowBuilder(lb)
                         .setTitle("Create new note")
                         .setSubtitle("with this note taking app")
@@ -322,11 +344,11 @@
                 ? -TimeUnit.MINUTES.toMillis(2) // negative for testing
                 : INFINITY;
         ListBuilder lb = new ListBuilder(getContext(), sliceUri, ttl);
-        return lb.setColor(0xff0F9D58)
+        return lb.setAccentColor(0xff0F9D58)
                 .setHeader(new ListBuilder.HeaderBuilder(lb)
                         .setTitle("Get ride")
                         .setSubtitle(headerSubtitle)
-                        .setSummarySubtitle("Ride to work in 12 min | Ride home in 1 hour 45 min")
+                        .setSummary("Ride to work in 12 min | Ride home in 1 hour 45 min")
                         .setPrimaryAction(primaryAction))
                 .addRow(new ListBuilder.RowBuilder(lb)
                         .setTitle("Work")
@@ -345,7 +367,7 @@
 
     private Slice createCustomToggleSlice(Uri sliceUri) {
         ListBuilder b = new ListBuilder(getContext(), sliceUri, -TimeUnit.HOURS.toMillis(1));
-        return b.setColor(0xffff4081)
+        return b.setAccentColor(0xffff4081)
                 .addRow(new ListBuilder.RowBuilder(b)
                         .setTitle("Custom toggle")
                         .addEndItem(
@@ -358,7 +380,7 @@
 
     private Slice createTwoCustomToggleSlices(Uri sliceUri) {
         ListBuilder lb = new ListBuilder(getContext(), sliceUri, INFINITY);
-        return lb.setColor(0xffff4081)
+        return lb.setAccentColor(0xffff4081)
                 .addRow(new ListBuilder.RowBuilder(lb)
                         .setTitle("2 toggles")
                         .setSubtitle("each supports two states")
@@ -398,7 +420,7 @@
         ListBuilder lb = new ListBuilder(getContext(), sliceUri, INFINITY);
         SliceAction primaryAction = new SliceAction(getIntent(Settings.ACTION_WIFI_SETTINGS),
                 IconCompat.createWithResource(getContext(), R.drawable.ic_wifi), "Wi-fi Settings");
-        lb.setColor(0xff4285f4);
+        lb.setAccentColor(0xff4285f4);
         lb.addRow(new ListBuilder.RowBuilder(lb)
                 .setTitle("Wi-fi")
                 .setTitleItem(IconCompat.createWithResource(getContext(), R.drawable.ic_wifi),
@@ -459,7 +481,7 @@
                 .addCell(new GridRowBuilder.CellBuilder(gb2)
                         .addTitleText("Check Out")
                         .addText("11:00 AM, Feb 19"));
-        return lb.setColor(0xffFF5252)
+        return lb.setAccentColor(0xffFF5252)
                 .setHeader(new ListBuilder.HeaderBuilder(lb)
                         .setTitle("Upcoming trip to Seattle")
                         .setSubtitle("Feb 1 - 19 | 2 guests"))
@@ -475,13 +497,30 @@
                 .build();
     }
 
+    private Slice createBasicInputRange(Uri sliceUri) {
+        IconCompat icon = IconCompat.createWithResource(getContext(), R.drawable.ic_star_on);
+        SliceAction primaryAction =
+                new SliceAction(getBroadcastIntent(ACTION_TOAST, "open star rating"),
+                        icon, "Rate");
+        ListBuilder lb = new ListBuilder(getContext(), sliceUri, INFINITY);
+        return lb.setAccentColor(0xff4285f4)
+                .addInputRange(new ListBuilder.InputRangeBuilder(lb)
+                        .setTitle("Alarm volume")
+                        .setSubtitle("Adjust your volume")
+                        .setInputAction(getBroadcastIntent(ACTION_TOAST, "volume changed"))
+                        .setValue(80)
+                        .setPrimaryAction(primaryAction)
+                        .setContentDescription("Slider for alarm volume"))
+                .build();
+    }
+
     private Slice createStarRatingInputRange(Uri sliceUri) {
         IconCompat icon = IconCompat.createWithResource(getContext(), R.drawable.ic_star_on);
         SliceAction primaryAction =
                 new SliceAction(getBroadcastIntent(ACTION_TOAST, "open star rating"), icon,
                         "Rate");
         ListBuilder lb = new ListBuilder(getContext(), sliceUri, INFINITY);
-        return lb.setColor(0xffff4081)
+        return lb.setAccentColor(0xffff4081)
                 .addInputRange(new ListBuilder.InputRangeBuilder(lb)
                         .setTitle("Star rating")
                         .setSubtitle("Pick a rating from 0 to 5")
@@ -502,7 +541,7 @@
                         getBroadcastIntent(ACTION_TOAST, "open download"), icon,
                         "Download");
         ListBuilder lb = new ListBuilder(getContext(), sliceUri, INFINITY);
-        return lb.setColor(0xffff4081)
+        return lb.setAccentColor(0xffff4081)
                 .addRange(new ListBuilder.RangeBuilder(lb)
                         .setTitle("Download progress")
                         .setSubtitle("Download is happening")
@@ -513,7 +552,7 @@
     }
 
     private Slice createPermissionSlice(Uri uri) {
-        return SliceProviderCompat.createPermissionSlice(getContext(), uri,
+        return SliceProvider.createPermissionSlice(getContext(), uri,
                 getContext().getPackageName());
     }
 
diff --git a/slices/view/src/androidTest/java/androidx/slice/render/SliceRenderer.java b/slices/view/src/androidTest/java/androidx/slice/render/SliceRenderer.java
index 5625c34..ec5e55a 100644
--- a/slices/view/src/androidTest/java/androidx/slice/render/SliceRenderer.java
+++ b/slices/view/src/androidTest/java/androidx/slice/render/SliceRenderer.java
@@ -22,7 +22,7 @@
 import android.app.ProgressDialog;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
-import android.os.AsyncTask;
+import android.net.Uri;
 import android.os.Handler;
 import android.util.Log;
 import android.util.TypedValue;
@@ -30,24 +30,33 @@
 import android.view.View;
 import android.view.ViewGroup;
 
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.util.concurrent.CountDownLatch;
-
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.slice.Slice;
 import androidx.slice.SliceProvider;
+import androidx.slice.SliceUtils;
 import androidx.slice.view.test.R;
 import androidx.slice.widget.SliceLiveData;
 import androidx.slice.widget.SliceView;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
 public class SliceRenderer {
 
     private static final String TAG = "SliceRenderer";
     public static final String SCREENSHOT_DIR = "slice-screenshots";
+
+    private static final int MAX_CONCURRENT = 5;
+
     private static File sScreenshotDirectory;
 
+    private final Object mRenderLock = new Object();
+
     private final Activity mContext;
     private final View mLayout;
     private final SliceView mSV1;
@@ -107,7 +116,7 @@
 
     private File getScreenshotDirectory() {
         if (sScreenshotDirectory == null) {
-            File storage = mContext.getDataDir();
+            File storage = mContext.getFilesDir();
             sScreenshotDirectory = new File(storage, SCREENSHOT_DIR);
             if (!sScreenshotDirectory.exists()) {
                 if (!sScreenshotDirectory.mkdirs()) {
@@ -121,20 +130,46 @@
 
 
     private void doRender() {
-        File output = getScreenshotDirectory();
+        final File output = getScreenshotDirectory();
         if (!output.exists()) {
             output.mkdir();
         }
-        mDoneLatch = new CountDownLatch(SliceCreator.URI_PATHS.length);
-        for (String slice : SliceCreator.URI_PATHS) {
-            doRender(slice, new File(output, String.format("%s.png", slice)),
-                    true /* scrollable */);
+        mDoneLatch = new CountDownLatch(SliceCreator.URI_PATHS.length * 2 + 2);
+
+        ExecutorService executor = Executors.newFixedThreadPool(5);
+        for (final String slice : SliceCreator.URI_PATHS) {
+            final Slice s = mSliceCreator.onBindSlice(SliceCreator.getUri(slice, mContext));
+
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    doRender(slice, s, new File(output, String.format("%s.png", slice)),
+                            true /* scrollable */);
+                }
+            });
+            final Slice serialized = serAndUnSer(s);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    doRender(slice + "-ser", serialized, new File(output, String.format(
+                            "%s-serialized.png", slice)), true /* scrollable */);
+                }
+            });
             if (slice.equals("wifi") || slice.equals("wifi2")) {
                 // Test scrolling
-                doRender(slice, new File(output, String.format("%s-no-scroll.png", slice)),
-                        false /* scrollable */);
+                executor.execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        doRender(slice + "-ns", s, new File(output, String.format(
+                                "%s-no-scroll.png", slice)), false /* scrollable */);
+                    }
+                });
             }
         }
+        try {
+            mDoneLatch.await();
+        } catch (InterruptedException e) {
+        }
         Log.d(TAG, "Wrote render to " + output.getAbsolutePath());
         mContext.runOnUiThread(new Runnable() {
             @Override
@@ -142,71 +177,83 @@
                 ((ViewGroup) mParent.getParent()).removeView(mParent);
             }
         });
+    }
+
+    private Slice serAndUnSer(Slice s) {
         try {
-            mDoneLatch.await();
-        } catch (InterruptedException e) {
+            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+            SliceUtils.serializeSlice(s, mContext, outputStream, "UTF-8",
+                    new SliceUtils.SerializeOptions()
+                            .setImageMode(SliceUtils.SerializeOptions.MODE_CONVERT)
+                            .setActionMode(SliceUtils.SerializeOptions.MODE_CONVERT));
+
+            byte[] bytes = outputStream.toByteArray();
+            Log.d(TAG, "Serialized: " + new String(bytes));
+            ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
+            return SliceUtils.parseSlice(mContext, inputStream, "UTF-8",
+                    new SliceUtils.SliceActionListener() {
+                        @Override
+                        public void onSliceAction(Uri actionUri) { }
+                    });
+        } catch (Exception e) {
+            throw new RuntimeException(e);
         }
     }
 
-    private void doRender(final String slice, final File file, final boolean scrollable) {
+    private void doRender(final String slice, final Slice s, final File file,
+            final boolean scrollable) {
         Log.d(TAG, "Rendering " + slice + " to " + file.getAbsolutePath());
 
-        final Slice s = mSliceCreator.onBindSlice(SliceCreator.getUri(slice, mContext));
-
-        final CountDownLatch l = new CountDownLatch(1);
-        mContext.runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                mSV1.setSlice(s);
-                mSV2.setSlice(s);
-                mSV3.setSlice(s);
-                mSV3.setScrollable(scrollable);
-                mSV1.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+        try {
+            final CountDownLatch l = new CountDownLatch(1);
+            final Bitmap[] b = new Bitmap[1];
+            synchronized (mRenderLock) {
+                mContext.runOnUiThread(new Runnable() {
                     @Override
-                    public void onLayoutChange(View v, int left, int top, int right, int bottom,
-                            int oldLeft, int oldTop, int oldRight, int oldBottom) {
-                        mSV1.removeOnLayoutChangeListener(this);
-                        mSV1.postDelayed(new Runnable() {
+                    public void run() {
+                        mSV1.setSlice(s);
+                        mSV2.setSlice(s);
+                        mSV3.setSlice(s);
+                        mSV3.setScrollable(scrollable);
+                        mSV1.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
                             @Override
-                            public void run() {
-                                Log.d(TAG, "Drawing " + slice);
-                                Bitmap b = Bitmap.createBitmap(mLayout.getMeasuredWidth(),
-                                        mLayout.getMeasuredHeight(),
-                                        Bitmap.Config.ARGB_8888);
+                            public void onLayoutChange(View v, int left, int top, int right,
+                                    int bottom,
+                                    int oldLeft, int oldTop, int oldRight, int oldBottom) {
+                                mSV1.removeOnLayoutChangeListener(this);
+                                mSV1.postDelayed(new Runnable() {
+                                    @Override
+                                    public void run() {
+                                        Log.d(TAG, "Drawing " + slice);
+                                        b[0] = Bitmap.createBitmap(mLayout.getMeasuredWidth(),
+                                                mLayout.getMeasuredHeight(),
+                                                Bitmap.Config.ARGB_8888);
 
-                                mLayout.draw(new Canvas(b));
-                                try {
-                                    doCompress(slice, b, new FileOutputStream(file));
-                                } catch (FileNotFoundException e) {
-                                    throw new RuntimeException(e);
-                                }
-                                l.countDown();
+                                        mLayout.draw(new Canvas(b[0]));
+                                        l.countDown();
+                                    }
+                                }, 10);
                             }
-                        }, 10);
+                        });
                     }
                 });
+                l.await();
             }
-        });
-        try {
-            l.await();
-        } catch (InterruptedException e) {
+            doCompress(slice, b[0], new FileOutputStream(file));
+        } catch (Exception e) {
+            throw new RuntimeException(e);
         }
     }
 
     private void doCompress(final String slice, final Bitmap b, final FileOutputStream s) {
-        AsyncTask.execute(new Runnable() {
-            @Override
-            public void run() {
-                Log.d(TAG, "Compressing " + slice);
-                if (!b.compress(Bitmap.CompressFormat.PNG, 100, s)) {
-                    throw new RuntimeException("Unable to compress");
-                }
+        Log.d(TAG, "Compressing " + slice);
+        if (!b.compress(Bitmap.CompressFormat.PNG, 100, s)) {
+            throw new RuntimeException("Unable to compress");
+        }
 
-                b.recycle();
-                Log.d(TAG, "Done " + slice);
-                mDoneLatch.countDown();
-            }
-        });
+        b.recycle();
+        mDoneLatch.countDown();
+        Log.d(TAG, "Done " + slice);
     }
 
     public void renderAll(final Runnable runnable) {
diff --git a/slices/view/src/main/java/androidx/slice/SliceManager.java b/slices/view/src/main/java/androidx/slice/SliceManager.java
index 63c56e8..83febcc 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManager.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManager.java
@@ -28,6 +28,7 @@
 import androidx.core.os.BuildCompat;
 
 import java.util.Collection;
+import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Executor;
 
@@ -125,10 +126,8 @@
      * <p>
      * This is the set of specs supported for a specific pinned slice. It will take
      * into account all clients and returns only specs supported by all.
-     * @hide
      * @see SliceSpec
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public abstract @NonNull Set<SliceSpec> getPinnedSpecs(@NonNull Uri uri);
 
     /**
@@ -141,9 +140,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 +152,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
@@ -223,6 +232,12 @@
     public abstract @NonNull Collection<Uri> getSliceDescendants(@NonNull Uri uri);
 
     /**
+     * Get the list of currently pinned slices for this app.
+     * @see SliceProvider#onSlicePinned
+     */
+    public abstract @NonNull List<Uri> getPinnedSlices();
+
+    /**
      * Class that listens to changes in {@link Slice}s.
      */
     public interface SliceCallback {
diff --git a/slices/view/src/main/java/androidx/slice/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..0018c13 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
@@ -29,6 +29,7 @@
 import androidx.slice.widget.SliceLiveData;
 
 import java.util.Collection;
+import java.util.List;
 import java.util.Set;
 
 
@@ -76,7 +77,30 @@
     }
 
     @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);
     }
+
+    @Override
+    public List<Uri> getPinnedSlices() {
+        return SliceProviderCompat.getPinnedSlices(mContext);
+    }
 }
diff --git a/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java b/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
index ac38e36..2dc7281 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
@@ -19,7 +19,6 @@
 import static androidx.slice.SliceConvert.unwrap;
 import static androidx.slice.widget.SliceLiveData.SUPPORTED_SPECS;
 
-import android.app.slice.SliceManager;
 import android.app.slice.SliceSpec;
 import android.content.Context;
 import android.content.Intent;
@@ -29,9 +28,11 @@
 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;
+import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
@@ -44,12 +45,6 @@
 
     private final android.app.slice.SliceManager mManager;
     private final List<SliceSpec> mSpecs;
-    private final SliceManager.SliceCallback mCallback = new SliceManager.SliceCallback() {
-        @Override
-        public void onSliceUpdated(@NonNull android.app.slice.Slice s) {
-
-        }
-    };
 
     SliceManagerWrapper(Context context) {
         this(context, context.getSystemService(android.app.slice.SliceManager.class));
@@ -63,31 +58,31 @@
 
     @Override
     public void pinSlice(@NonNull Uri uri) {
-        mManager.registerSliceCallback(uri, mSpecs, mCallback);
+        mManager.pinSlice(uri, mSpecs);
     }
 
     @Override
     public void unpinSlice(@NonNull Uri uri) {
-        mManager.unregisterSliceCallback(uri, mCallback);
+        mManager.unpinSlice(uri);
     }
 
     @Override
     public @NonNull Set<androidx.slice.SliceSpec> getPinnedSpecs(@NonNull Uri uri) {
-        return SliceConvert.wrap(mManager.getPinnedSpecs(uri));
+        // Disabled while we update APIs.
+        //return SliceConvert.wrap(mManager.getPinnedSpecs(uri));
+        return Collections.EMPTY_SET;
     }
 
     @Nullable
     @Override
     public androidx.slice.Slice bindSlice(@NonNull Uri uri) {
-        return SliceConvert.wrap(android.app.slice.Slice.bindSlice(
-                mContext.getContentResolver(), uri, mSpecs));
+        return SliceConvert.wrap(mManager.bindSlice(uri, mSpecs));
     }
 
     @Nullable
     @Override
     public androidx.slice.Slice bindSlice(@NonNull Intent intent) {
-        return SliceConvert.wrap(android.app.slice.Slice.bindSlice(
-                mContext, intent, mSpecs));
+        return SliceConvert.wrap(mManager.bindSlice(intent, mSpecs));
     }
 
     @Override
@@ -95,11 +90,30 @@
         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) {
-        // TODO: Switch over to mapIntentToUri once it lands in prebuilt.
-        Slice slice = bindSlice(intent);
-        return slice != null ? slice.getUri() : null;
+        return mManager.mapIntentToUri(intent);
+    }
+
+    @Override
+    public List<Uri> getPinnedSlices() {
+        return mManager.getPinnedSlices();
     }
 }
diff --git a/slices/view/src/main/java/androidx/slice/SliceMetadata.java b/slices/view/src/main/java/androidx/slice/SliceMetadata.java
index 8a4ee03..a2c996c 100644
--- a/slices/view/src/main/java/androidx/slice/SliceMetadata.java
+++ b/slices/view/src/main/java/androidx/slice/SliceMetadata.java
@@ -35,6 +35,7 @@
 import static androidx.slice.widget.EventInfo.ROW_TYPE_PROGRESS;
 import static androidx.slice.widget.EventInfo.ROW_TYPE_SLIDER;
 
+import android.app.PendingIntent;
 import android.content.Context;
 import android.text.TextUtils;
 
@@ -128,7 +129,7 @@
         }
         mSliceActions = getSliceActions(mSlice);
 
-        mListContent = new ListContent(context, slice);
+        mListContent = new ListContent(context, slice, null, 0, 0);
         mHeaderItem = mListContent.getHeaderItem();
         mTemplateType = mListContent.getHeaderTemplateType();
 
@@ -197,6 +198,23 @@
     }
 
     /**
+     * Gets the input range action associated for this slice, if it exists.
+     *
+     * @return the {@link android.app.PendingIntent} for the input range.
+     */
+    @Nullable
+    public PendingIntent getInputRangeAction() {
+        if (mTemplateType == ROW_TYPE_SLIDER) {
+            RowContent rc = new RowContent(mContext, mHeaderItem, true /* isHeader */);
+            SliceItem range = rc.getRange();
+            if (range != null) {
+                return range.getAction();
+            }
+        }
+        return null;
+    }
+
+    /**
      * Gets the range information associated with a progress bar or input range associated with this
      * slice, if it exists.
      *
@@ -270,7 +288,7 @@
     public int getLoadingState() {
         // Check loading state
         boolean hasHintPartial = SliceQuery.find(mSlice, null, HINT_PARTIAL, null) != null;
-        if (mSlice.getItems().size() == 0) {
+        if (!mListContent.isValid()) {
             // Empty slice
             return LOADED_NONE;
         } else if (hasHintPartial) {
diff --git a/slices/view/src/main/java/androidx/slice/SliceUtils.java b/slices/view/src/main/java/androidx/slice/SliceUtils.java
index 93b149c..442b324 100644
--- a/slices/view/src/main/java/androidx/slice/SliceUtils.java
+++ b/slices/view/src/main/java/androidx/slice/SliceUtils.java
@@ -30,7 +30,10 @@
 import static androidx.slice.SliceMetadata.LOADED_PARTIAL;
 import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 import android.content.Context;
+import android.net.Uri;
 import android.text.TextUtils;
 
 import androidx.annotation.IntDef;
@@ -42,6 +45,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.lang.annotation.Retention;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -65,10 +69,11 @@
      * @param output The output of the serialization.
      * @param encoding The encoding to use for serialization.
      * @param options Options defining how to handle non-serializable items.
+     * @throws IllegalArgumentException if the slice cannot be serialized using the given options.
      */
     public static void serializeSlice(@NonNull Slice s, @NonNull Context context,
             @NonNull OutputStream output, @NonNull String encoding,
-            @NonNull SerializeOptions options) throws IOException {
+            @NonNull SerializeOptions options) throws IOException, IllegalArgumentException {
         SliceXml.serializeSlice(s, context, output, encoding, options);
     }
 
@@ -76,13 +81,18 @@
      * Parse a slice that has been previously serialized.
      * <p>
      * Parses a slice that was serialized with {@link #serializeSlice}.
+     * <p>
+     * Note: Slices returned by this cannot be passed to {@link SliceConvert#unwrap(Slice)}.
      *
      * @param input The input stream to read from.
      * @param encoding The encoding to read as.
+     * @param listener Listener used to handle actions when reconstructing the slice.
+     * @throws SliceParseException if the InputStream cannot be parsed.
      */
-    public static @NonNull Slice parseSlice(@NonNull InputStream input, @NonNull String encoding)
-            throws IOException {
-        return SliceXml.parseSlice(input, encoding);
+    public static @NonNull Slice parseSlice(@NonNull Context context, @NonNull InputStream input,
+            @NonNull String encoding, @NonNull SliceActionListener listener)
+            throws IOException, SliceParseException {
+        return SliceXml.parseSlice(context, input, encoding, listener);
     }
 
     /**
@@ -101,17 +111,22 @@
         /**
          * Constant indicating that the SliceItem should be serialized as much as possible.
          * <p>
-         * For images this means it will be replaced with an empty image. For actions, the
-         * action will be removed but the content of the action will be serialized.
+         * For images this means they will be attempted to be serialized. For actions, the
+         * action will be removed but the content of the action will be serialized. The action
+         * may be triggered later on a de-serialized slice by binding the slice again and activating
+         * a pending-intent at the same location as the serialized action.
          */
-        public static final int MODE_DISABLE = 2;
+        public static final int MODE_CONVERT = 2;
 
-        @IntDef({MODE_THROW, MODE_REMOVE, MODE_DISABLE})
+        @IntDef({MODE_THROW, MODE_REMOVE, MODE_CONVERT})
+        @Retention(SOURCE)
         @interface FormatMode {
         }
 
         private int mActionMode = MODE_THROW;
         private int mImageMode = MODE_THROW;
+        private int mMaxWidth = 1000;
+        private int mMaxHeight = 1000;
 
         /**
          * @hide
@@ -149,6 +164,22 @@
         }
 
         /**
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        public int getMaxWidth() {
+            return mMaxWidth;
+        }
+
+        /**
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        public int getMaxHeight() {
+            return mMaxHeight;
+        }
+
+        /**
          * Sets how {@link android.app.slice.SliceItem#FORMAT_ACTION} items should be handled.
          *
          * The default mode is {@link #MODE_THROW}.
@@ -169,6 +200,30 @@
             mImageMode = mode;
             return this;
         }
+
+        /**
+         * Set the maximum width of an image to use when serializing.
+         * <p>
+         * Will only be used if the {@link #setImageMode(int)} is set to {@link #MODE_CONVERT}.
+         * Any images larger than the maximum size will be scaled down to fit within that size.
+         * The default value is 1000.
+         */
+        public SerializeOptions setMaxImageWidth(int width) {
+            mMaxWidth = width;
+            return this;
+        }
+
+        /**
+         * Set the maximum height of an image to use when serializing.
+         * <p>
+         * Will only be used if the {@link #setImageMode(int)} is set to {@link #MODE_CONVERT}.
+         * Any images larger than the maximum size will be scaled down to fit within that size.
+         * The default value is 1000.
+         */
+        public SerializeOptions setMaxImageHeight(int height) {
+            mMaxHeight = height;
+            return this;
+        }
     }
 
     /**
@@ -255,4 +310,39 @@
         }
         return null;
     }
+
+    /**
+     * A listener used to receive events on slices parsed with
+     * {@link #parseSlice(Context, InputStream, String, SliceActionListener)}.
+     */
+    public interface SliceActionListener {
+        /**
+         * Called when an action is triggered on a slice parsed with
+         * {@link #parseSlice(Context, InputStream, String, SliceActionListener)}.
+         * @param actionUri The uri of the action selected.
+         */
+        void onSliceAction(Uri actionUri);
+    }
+
+    /**
+     * Exception thrown during
+     * {@link #parseSlice(Context, InputStream, String, SliceActionListener)}.
+     */
+    public static class SliceParseException extends Exception {
+        /**
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        public SliceParseException(String s, Throwable e) {
+            super(s, e);
+        }
+
+        /**
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        public SliceParseException(String s) {
+            super(s);
+        }
+    }
 }
diff --git a/slices/view/src/main/java/androidx/slice/SliceXml.java b/slices/view/src/main/java/androidx/slice/SliceXml.java
index 65f1e7d..50d1e17 100644
--- a/slices/view/src/main/java/androidx/slice/SliceXml.java
+++ b/slices/view/src/main/java/androidx/slice/SliceXml.java
@@ -20,22 +20,32 @@
 import static org.xmlpull.v1.XmlPullParser.TEXT;
 
 import android.annotation.SuppressLint;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
 import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
 import android.net.Uri;
+import android.os.Build;
 import android.text.Html;
 import android.text.Spanned;
 import android.text.TextUtils;
+import android.util.Base64;
 
 import androidx.annotation.RestrictTo;
 import androidx.core.graphics.drawable.IconCompat;
+import androidx.core.util.Consumer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 import org.xmlpull.v1.XmlPullParserFactory;
 import org.xmlpull.v1.XmlSerializer;
 
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -50,14 +60,24 @@
     private static final String NAMESPACE = null;
 
     private static final String TAG_SLICE = "slice";
+    private static final String TAG_ACTION = "action";
     private static final String TAG_ITEM = "item";
 
     private static final String ATTR_URI = "uri";
     private static final String ATTR_FORMAT = "format";
     private static final String ATTR_SUBTYPE = "subtype";
     private static final String ATTR_HINTS = "hints";
+    private static final String ATTR_ICON_TYPE = "iconType";
+    private static final String ATTR_ICON_PACKAGE = "pkg";
+    private static final String ATTR_ICON_RES_TYPE = "resType";
 
-    public static Slice parseSlice(InputStream input, String encoding) throws IOException {
+    private static final String ICON_TYPE_RES = "res";
+    private static final String ICON_TYPE_URI = "uri";
+    private static final String ICON_TYPE_DEFAULT = "def";
+
+    public static Slice parseSlice(Context context, InputStream input,
+            String encoding, SliceUtils.SliceActionListener listener)
+            throws IOException, SliceUtils.SliceParseException {
         try {
             XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
             parser.setInput(input, encoding);
@@ -70,7 +90,7 @@
                 if (type != START_TAG) {
                     continue;
                 }
-                s = parseSlice(parser);
+                s = parseSlice(context, parser, listener);
             }
             return s;
         } catch (XmlPullParserException e) {
@@ -79,9 +99,10 @@
     }
 
     @SuppressLint("WrongConstant")
-    private static Slice parseSlice(XmlPullParser parser)
-            throws IOException, XmlPullParserException {
-        if (!TAG_SLICE.equals(parser.getName())) {
+    private static Slice parseSlice(Context context, XmlPullParser parser,
+            SliceUtils.SliceActionListener listener)
+            throws IOException, XmlPullParserException, SliceUtils.SliceParseException {
+        if (!TAG_SLICE.equals(parser.getName()) && !TAG_ACTION.equals(parser.getName())) {
             throw new IOException("Unexpected tag " + parser.getName());
         }
         int outerDepth = parser.getDepth();
@@ -94,20 +115,24 @@
         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
             if (type == START_TAG && TAG_ITEM.equals(parser.getName())) {
-                parseItem(b, parser);
+                parseItem(context, b, parser, listener);
             }
         }
         return b.build();
     }
 
     @SuppressLint("WrongConstant")
-    private static void parseItem(Slice.Builder b, XmlPullParser parser)
-            throws IOException, XmlPullParserException {
+    private static void parseItem(Context context, Slice.Builder b,
+            XmlPullParser parser, final SliceUtils.SliceActionListener listener)
+            throws IOException, XmlPullParserException, SliceUtils.SliceParseException {
         int type;
         int outerDepth = parser.getDepth();
         String format = parser.getAttributeValue(NAMESPACE, ATTR_FORMAT);
         String subtype = parser.getAttributeValue(NAMESPACE, ATTR_SUBTYPE);
         String hintStr = parser.getAttributeValue(NAMESPACE, ATTR_HINTS);
+        String iconType = parser.getAttributeValue(NAMESPACE, ATTR_ICON_TYPE);
+        String pkg = parser.getAttributeValue(NAMESPACE, ATTR_ICON_PACKAGE);
+        String resType = parser.getAttributeValue(NAMESPACE, ATTR_ICON_RES_TYPE);
         String[] hints = hints(hintStr);
         String v;
         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
@@ -118,16 +143,37 @@
                         // Nothing for now.
                         break;
                     case android.app.slice.SliceItem.FORMAT_IMAGE:
-                        v = parser.getText();
-                        if (!TextUtils.isEmpty(v)) {
-                            if (android.os.Build.VERSION.SDK_INT
-                                    >= android.os.Build.VERSION_CODES.M) {
-                                String[] split = v.split(",");
-                                int w = Integer.parseInt(split[0]);
-                                int h = Integer.parseInt(split[1]);
-                                Bitmap image = Bitmap.createBitmap(w, h, Bitmap.Config.ALPHA_8);
+                        switch (iconType) {
+                            case ICON_TYPE_RES:
+                                String resName = parser.getText();
+                                try {
+                                    Resources r = context.getPackageManager()
+                                                .getResourcesForApplication(pkg);
+                                    int id = r.getIdentifier(resName, resType, pkg);
+                                    if (id != 0) {
+                                        b.addIcon(IconCompat.createWithResource(
+                                                context.createPackageContext(pkg, 0), id), subtype,
+                                                hints);
+                                    } else {
+                                        throw new SliceUtils.SliceParseException(
+                                                "Cannot find resource " + pkg + ":" + resType
+                                                        + "/" + resName);
+                                    }
+                                } catch (PackageManager.NameNotFoundException e) {
+                                    throw new SliceUtils.SliceParseException(
+                                            "Invalid icon package " + pkg, e);
+                                }
+                                break;
+                            case ICON_TYPE_URI:
+                                v = parser.getText();
+                                b.addIcon(IconCompat.createWithContentUri(v), subtype, hints);
+                                break;
+                            default:
+                                v = parser.getText();
+                                byte[] data = Base64.decode(v, Base64.NO_WRAP);
+                                Bitmap image = BitmapFactory.decodeByteArray(data, 0, data.length);
                                 b.addIcon(IconCompat.createWithBitmap(image), subtype, hints);
-                            }
+                                break;
                         }
                         break;
                     case android.app.slice.SliceItem.FORMAT_INT:
@@ -136,17 +182,28 @@
                         break;
                     case android.app.slice.SliceItem.FORMAT_TEXT:
                         v = parser.getText();
+                        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
+                            // 19-21 don't allow special characters in XML, so we base64 encode it.
+                            v = new String(Base64.decode(v, Base64.NO_WRAP));
+                        }
                         b.addText(Html.fromHtml(v), subtype, hints);
                         break;
-                    case android.app.slice.SliceItem.FORMAT_TIMESTAMP:
+                    case android.app.slice.SliceItem.FORMAT_LONG:
                         v = parser.getText();
-                        b.addTimestamp(Long.parseLong(v), subtype, hints);
+                        b.addLong(Long.parseLong(v), subtype, hints);
                         break;
                     default:
                         throw new IllegalArgumentException("Unrecognized format " + format);
                 }
             } else if (type == START_TAG && TAG_SLICE.equals(parser.getName())) {
-                b.addSubSlice(parseSlice(parser), subtype);
+                b.addSubSlice(parseSlice(context, parser, listener), subtype);
+            } else if (type == START_TAG && TAG_ACTION.equals(parser.getName())) {
+                b.addAction(new Consumer<Uri>() {
+                    @Override
+                    public void accept(Uri uri) {
+                        listener.onSliceAction(uri);
+                    }
+                }, parseSlice(context, parser, listener), subtype);
             }
         }
     }
@@ -162,7 +219,7 @@
             serializer.setOutput(output, encoding);
             serializer.startDocument(encoding, null);
 
-            serialize(s, context, options, serializer);
+            serialize(s, context, options, serializer, false, null);
 
             serializer.endDocument();
             serializer.flush();
@@ -172,9 +229,12 @@
     }
 
     private static void serialize(Slice s, Context context, SliceUtils.SerializeOptions options,
-            XmlSerializer serializer) throws IOException {
-        serializer.startTag(NAMESPACE, TAG_SLICE);
+            XmlSerializer serializer, boolean isAction, String subType) throws IOException {
+        serializer.startTag(NAMESPACE, isAction ? TAG_ACTION : TAG_SLICE);
         serializer.attribute(NAMESPACE, ATTR_URI, s.getUri().toString());
+        if (subType != null) {
+            serializer.attribute(NAMESPACE, ATTR_SUBTYPE, subType);
+        }
         if (!s.getHints().isEmpty()) {
             serializer.attribute(NAMESPACE, ATTR_HINTS, hintStr(s.getHints()));
         }
@@ -182,7 +242,7 @@
             serialize(item, context, options, serializer);
         }
 
-        serializer.endTag(NAMESPACE, TAG_SLICE);
+        serializer.endTag(NAMESPACE, isAction ? TAG_ACTION : TAG_SLICE);
     }
 
     private static void serialize(SliceItem item, Context context,
@@ -201,37 +261,65 @@
 
         switch (format) {
             case android.app.slice.SliceItem.FORMAT_ACTION:
-                if (options.getActionMode() == SliceUtils.SerializeOptions.MODE_DISABLE) {
-                    serialize(item.getSlice(), context, options, serializer);
+                if (options.getActionMode() == SliceUtils.SerializeOptions.MODE_CONVERT) {
+                    serialize(item.getSlice(), context, options, serializer, true,
+                            item.getSubType());
+                } else if (options.getActionMode() == SliceUtils.SerializeOptions.MODE_THROW) {
+                    throw new IllegalArgumentException("Slice contains an action " + item);
                 }
                 break;
             case android.app.slice.SliceItem.FORMAT_REMOTE_INPUT:
                 // Nothing for now.
                 break;
             case android.app.slice.SliceItem.FORMAT_IMAGE:
-                if (options.getImageMode() == SliceUtils.SerializeOptions.MODE_DISABLE) {
-                    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
-                        Drawable d = item.getIcon().loadDrawable(context);
-                        serializer.text(String.format("%d,%d",
-                                d.getIntrinsicWidth(), d.getIntrinsicHeight()));
+                if (options.getImageMode() == SliceUtils.SerializeOptions.MODE_CONVERT) {
+                    IconCompat icon = item.getIcon();
+
+                    switch (icon.getType()) {
+                        case Icon.TYPE_RESOURCE:
+                            serializeResIcon(serializer, icon, context);
+                            break;
+                        case Icon.TYPE_URI:
+                            Uri uri = icon.getUri();
+                            if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
+                                serializeFileIcon(serializer, icon, context);
+                            } else {
+                                serializeIcon(serializer, icon, context, options);
+                            }
+                            break;
+                        default:
+                            serializeIcon(serializer, icon, context, options);
+                            break;
                     }
+                } else if (options.getImageMode() == SliceUtils.SerializeOptions.MODE_THROW) {
+                    throw new IllegalArgumentException("Slice contains an image " + item);
                 }
                 break;
             case android.app.slice.SliceItem.FORMAT_INT:
                 serializer.text(String.valueOf(item.getInt()));
                 break;
             case android.app.slice.SliceItem.FORMAT_SLICE:
-                serialize(item.getSlice(), context, options, serializer);
+                serialize(item.getSlice(), context, options, serializer, false, item.getSubType());
                 break;
             case android.app.slice.SliceItem.FORMAT_TEXT:
                 if (item.getText() instanceof Spanned) {
-                    serializer.text(Html.toHtml((Spanned) item.getText()));
+                    String text = Html.toHtml((Spanned) item.getText());
+                    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
+                        // 19-21 don't allow special characters in XML, so we base64 encode it.
+                        text = Base64.encodeToString(text.getBytes(), Base64.NO_WRAP);
+                    }
+                    serializer.text(text);
                 } else {
-                    serializer.text(String.valueOf(item.getText()));
+                    String text = String.valueOf(item.getText());
+                    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
+                        // 19-21 don't allow special characters in XML, so we base64 encode it.
+                        text = Base64.encodeToString(text.getBytes(), Base64.NO_WRAP);
+                    }
+                    serializer.text(text);
                 }
                 break;
-            case android.app.slice.SliceItem.FORMAT_TIMESTAMP:
-                serializer.text(String.valueOf(item.getTimestamp()));
+            case android.app.slice.SliceItem.FORMAT_LONG:
+                serializer.text(String.valueOf(item.getLong()));
                 break;
             default:
                 throw new IllegalArgumentException("Unrecognized format " + format);
@@ -239,6 +327,53 @@
         serializer.endTag(NAMESPACE, TAG_ITEM);
     }
 
+    private static void serializeResIcon(XmlSerializer serializer, IconCompat icon, Context context)
+            throws IOException {
+        try {
+            Resources res = context.getPackageManager().getResourcesForApplication(
+                    icon.getResPackage());
+            int id = icon.getResId();
+            serializer.attribute(NAMESPACE, ATTR_ICON_TYPE, ICON_TYPE_RES);
+            serializer.attribute(NAMESPACE, ATTR_ICON_PACKAGE, res.getResourcePackageName(id));
+            serializer.attribute(NAMESPACE, ATTR_ICON_RES_TYPE, res.getResourceTypeName(id));
+            serializer.text(res.getResourceEntryName(id));
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new IllegalArgumentException("Slice contains invalid icon", e);
+        }
+    }
+
+    private static void serializeFileIcon(XmlSerializer serializer, IconCompat icon,
+            Context context) throws IOException {
+        serializer.attribute(NAMESPACE, ATTR_ICON_TYPE, ICON_TYPE_URI);
+        serializer.text(icon.getUri().toString());
+    }
+
+    private static void serializeIcon(XmlSerializer serializer, IconCompat icon,
+            Context context, SliceUtils.SerializeOptions options) throws IOException {
+        Drawable d = icon.loadDrawable(context);
+        int width = d.getIntrinsicWidth();
+        int height = d.getIntrinsicHeight();
+        if (width > options.getMaxWidth()) {
+            height = (int) (options.getMaxWidth() * height / (double) width);
+            width = options.getMaxWidth();
+        }
+        if (height > options.getMaxHeight()) {
+            width = (int) (options.getMaxHeight() * width / (double) height);
+            height = options.getMaxHeight();
+        }
+        Bitmap b = Bitmap.createBitmap(width, height,
+                Bitmap.Config.ARGB_8888);
+        Canvas c = new Canvas(b);
+        d.setBounds(0, 0, c.getWidth(), c.getHeight());
+        d.draw(c);
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        b.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
+        b.recycle();
+
+        serializer.attribute(NAMESPACE, ATTR_ICON_TYPE, ICON_TYPE_DEFAULT);
+        serializer.text(new String(Base64.encode(outputStream.toByteArray(), Base64.NO_WRAP)));
+    }
+
     private static String hintStr(List<String> hints) {
         return TextUtils.join(",", hints);
     }
diff --git a/slices/view/src/main/java/androidx/slice/widget/ActionRow.java b/slices/view/src/main/java/androidx/slice/widget/ActionRow.java
index bd6ad13..55bbca1 100644
--- a/slices/view/src/main/java/androidx/slice/widget/ActionRow.java
+++ b/slices/view/src/main/java/androidx/slice/widget/ActionRow.java
@@ -22,7 +22,6 @@
 
 import static androidx.slice.core.SliceHints.ICON_IMAGE;
 
-import android.app.PendingIntent;
 import android.app.PendingIntent.CanceledException;
 import android.app.RemoteInput;
 import android.app.slice.Slice;
@@ -132,7 +131,7 @@
             } else if (action.hasHint(Slice.HINT_SHORTCUT)) {
                 final SliceActionImpl ac = new SliceActionImpl(action);
                 IconCompat iconItem = ac.getIcon();
-                if (iconItem != null && ac.getAction() != null) {
+                if (iconItem != null && ac.getActionItem() != null) {
                     boolean tint = ac.getImageMode() == ICON_IMAGE;
                     addAction(iconItem, tint).setOnClickListener(
                             new OnClickListener() {
@@ -140,7 +139,7 @@
                                 public void onClick(View v) {
                                     try {
                                         // TODO - should log events here
-                                        ac.getAction().send();
+                                        ac.getActionItem().fireAction(null, null);
                                     } catch (CanceledException e) {
                                         e.printStackTrace();
                                     }
@@ -165,7 +164,7 @@
                     new OnClickListener() {
                         @Override
                         public void onClick(View v) {
-                            handleRemoteInputClick(v, action.getAction(),
+                            handleRemoteInputClick(v, action,
                                     input.getRemoteInput());
                         }
                     });
@@ -182,7 +181,7 @@
     }
 
     @RequiresApi(21)
-    private boolean handleRemoteInputClick(View view, PendingIntent pendingIntent,
+    private boolean handleRemoteInputClick(View view, SliceItem action,
             RemoteInput input) {
         if (input == null) {
             return false;
@@ -223,7 +222,7 @@
                 Math.max((w - cx) + cy, (w - cx) + (h - cy)));
 
         riv.setRevealParameters(cx, cy, r);
-        riv.setPendingIntent(pendingIntent);
+        riv.setAction(action);
         riv.setRemoteInput(new RemoteInput[] {
                 input
         }, input);
diff --git a/slices/view/src/main/java/androidx/slice/widget/GridContent.java b/slices/view/src/main/java/androidx/slice/widget/GridContent.java
index 69d5c44..5ca395c 100644
--- a/slices/view/src/main/java/androidx/slice/widget/GridContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/GridContent.java
@@ -17,7 +17,6 @@
 package androidx.slice.widget;
 
 import static android.app.slice.Slice.HINT_ACTIONS;
-import static android.app.slice.Slice.HINT_LIST_ITEM;
 import static android.app.slice.Slice.HINT_SEE_MORE;
 import static android.app.slice.Slice.HINT_SHORTCUT;
 import static android.app.slice.Slice.HINT_TITLE;
@@ -102,7 +101,11 @@
                 new String[] {HINT_ACTIONS} /* nonHints */);
         mAllImages = true;
         if (FORMAT_SLICE.equals(gridItem.getFormat())) {
-            List<SliceItem> items = gridItem.getSlice().getItems().get(0).getSlice().getItems();
+            List<SliceItem> items = gridItem.getSlice().getItems();
+            if (items.size() == 1 && FORMAT_SLICE.equals(items.get(0).getFormat())) {
+                // TODO: this can be removed at release
+                items = items.get(0).getSlice().getItems();
+            }
             items = filterAndProcessItems(items);
             // Check if it it's only one item that is a slice
             if (items.size() == 1 && items.get(0).getFormat().equals(FORMAT_SLICE)) {
@@ -197,11 +200,14 @@
         List<SliceItem> filteredItems = new ArrayList<>();
         for (int i = 0; i < items.size(); i++) {
             SliceItem item = items.get(i);
-            boolean isNonCellContent = item.hasAnyHints(HINT_SHORTCUT, HINT_SEE_MORE,
-                    HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED);
+            // TODO: This see more can be removed at release
+            boolean containsSeeMore = SliceQuery.find(item, null, HINT_SEE_MORE, null) != null;
+            boolean isNonCellContent = containsSeeMore
+                    || item.hasAnyHints(HINT_SHORTCUT, HINT_SEE_MORE, HINT_KEYWORDS, HINT_TTL,
+                            HINT_LAST_UPDATED);
             if (SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) {
                 mContentDescr = item;
-            } else if (item.hasHint(HINT_LIST_ITEM) && !isNonCellContent) {
+            } else if (!isNonCellContent) {
                 filteredItems.add(item);
             }
         }
@@ -224,6 +230,8 @@
 
     /**
      * @return the height to display a grid row at when it is used as a small template.
+     * Does not include padding that might be added by slice view attributes,
+     * see {@link ListContent#getListHeight(Context, List)}.
      */
     public int getSmallHeight() {
         return getHeight(true /* isSmall */);
@@ -231,6 +239,8 @@
 
     /**
      * @return the height the content in this template requires to be displayed.
+     * Does not include padding that might be added by slice view attributes,
+     * see {@link ListContent#getListHeight(Context, List)}.
      */
     public int getActualHeight() {
         return getHeight(false /* isSmall */);
diff --git a/slices/view/src/main/java/androidx/slice/widget/GridRowView.java b/slices/view/src/main/java/androidx/slice/widget/GridRowView.java
index 1bb90e7..9d06d85 100644
--- a/slices/view/src/main/java/androidx/slice/widget/GridRowView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/GridRowView.java
@@ -48,7 +48,6 @@
 
 import androidx.annotation.ColorInt;
 import androidx.annotation.RestrictTo;
-import androidx.slice.Slice;
 import androidx.slice.SliceItem;
 import androidx.slice.core.SliceQuery;
 import androidx.slice.view.R;
@@ -79,9 +78,12 @@
     private static final int MAX_CELL_IMAGES = 1;
 
     private int mRowIndex;
+    private int mRowCount;
+
     private int mSmallImageSize;
     private int mIconSize;
     private int mGutter;
+    private int mTextPadding;
 
     private GridContent mGridContent;
     private LinearLayout mViewContainer;
@@ -100,17 +102,43 @@
         mIconSize = res.getDimensionPixelSize(R.dimen.abc_slice_icon_size);
         mSmallImageSize = res.getDimensionPixelSize(R.dimen.abc_slice_small_image_size);
         mGutter = res.getDimensionPixelSize(R.dimen.abc_slice_grid_gutter);
+        mTextPadding = res.getDimensionPixelSize(R.dimen.abc_slice_grid_text_padding);
     }
 
     @Override
     public int getSmallHeight() {
         // GridRow is small if its the first element in a list without a header presented in small
-        return mGridContent != null ? mGridContent.getSmallHeight() : 0;
+        if (mGridContent == null) {
+            return 0;
+        }
+        return mGridContent.getSmallHeight() + getExtraTopPadding() + getExtraBottomPadding();
     }
 
     @Override
     public int getActualHeight() {
-        return mGridContent != null ? mGridContent.getActualHeight() : 0;
+        if (mGridContent == null) {
+            return 0;
+        }
+        return mGridContent.getActualHeight() + getExtraTopPadding() + getExtraBottomPadding();
+    }
+
+    private int getExtraTopPadding() {
+        if (mGridContent != null && mGridContent.isAllImages()) {
+            // Might need to add padding if in first or last position
+            if (mRowIndex == 0) {
+                return mGridTopPadding;
+            }
+        }
+        return 0;
+    }
+
+    private int getExtraBottomPadding() {
+        if (mGridContent != null && mGridContent.isAllImages()) {
+            if (mRowIndex == mRowCount - 1 || getMode() == MODE_SMALL) {
+                return mGridBottomPadding;
+            }
+        }
+        return 0;
     }
 
     @Override
@@ -132,22 +160,19 @@
         }
     }
 
-    @Override
-    public void setSlice(Slice slice) {
-        // Nothing to do
-    }
-
     /**
      * This is called when GridView is being used as a component in a larger template.
      */
     @Override
-    public void setSliceItem(SliceItem slice, boolean isHeader, int index,
-            SliceView.OnSliceActionListener observer) {
+    public void setSliceItem(SliceItem slice, boolean isHeader, int rowIndex,
+            int rowCount, SliceView.OnSliceActionListener observer) {
         resetView();
         setSliceActionListener(observer);
-        mRowIndex = index;
+        mRowIndex = rowIndex;
+        mRowCount = rowCount;
         mGridContent = new GridContent(getContext(), slice);
         populateViews(mGridContent);
+        mViewContainer.setPadding(0, getExtraTopPadding(), 0, getExtraBottomPadding());
     }
 
     private void populateViews(GridContent gc) {
@@ -204,6 +229,11 @@
             seeMoreView = (LinearLayout) inflater.inflate(
                     R.layout.abc_slice_grid_see_more, mViewContainer, false);
             extraText = seeMoreView.findViewById(R.id.text_see_more_count);
+
+            // Update text appearance
+            TextView moreText = seeMoreView.findViewById(R.id.text_see_more);
+            moreText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mGridTitleSize);
+            moreText.setTextColor(mTitleColor);
         }
         mViewContainer.addView(seeMoreView, new LinearLayout.LayoutParams(0, MATCH_PARENT, 1));
         extraText.setText(getResources().getString(R.string.abc_slice_more_content, numExtra));
@@ -249,25 +279,29 @@
             Iterator<SliceItem> iterator = textItems.iterator();
             while (textItems.size() > 1) {
                 SliceItem item = iterator.next();
-                if (!item.hasHint(HINT_TITLE)) {
+                if (!item.hasAnyHints(HINT_TITLE, HINT_LARGE)) {
                     iterator.remove();
                 }
             }
         }
+        SliceItem prevItem = null;
         for (int i = 0; i < cellItems.size(); i++) {
             SliceItem item = cellItems.get(i);
             final String itemFormat = item.getFormat();
+            int padding = determinePadding(prevItem);
             if (textCount < maxCellText && (FORMAT_TEXT.equals(itemFormat)
                     || FORMAT_TIMESTAMP.equals(itemFormat))) {
                 if (textItems != null && !textItems.contains(item)) {
                     continue;
                 }
-                if (addItem(item, mTintColor, cellContainer, singleItem)) {
+                if (addItem(item, mTintColor, cellContainer, padding)) {
+                    prevItem = item;
                     textCount++;
                     added = true;
                 }
             } else if (imageCount < MAX_CELL_IMAGES && FORMAT_IMAGE.equals(item.getFormat())) {
-                if (addItem(item, mTintColor, cellContainer, singleItem)) {
+                if (addItem(item, mTintColor, cellContainer, 0)) {
+                    prevItem = item;
                     imageCount++;
                     added = true;
                 }
@@ -300,22 +334,28 @@
 
     /**
      * Adds simple items to a container. Simple items include icons, text, and timestamps.
+     *
+     * @param item item to add to the container.
+     * @param container the container to add to.
+     * @param padding the padding to apply to the item.
+     *
      * @return Whether an item was added.
      */
-    private boolean addItem(SliceItem item, int color, ViewGroup container, boolean singleItem) {
+    private boolean addItem(SliceItem item, int color, ViewGroup container, int padding) {
         final String format = item.getFormat();
         View addedView = null;
         if (FORMAT_TEXT.equals(format) || FORMAT_TIMESTAMP.equals(format)) {
             boolean title = SliceQuery.hasAnyHints(item, HINT_LARGE, HINT_TITLE);
             TextView tv = (TextView) LayoutInflater.from(getContext()).inflate(title
                     ? TITLE_TEXT_LAYOUT : TEXT_LAYOUT, null);
-            tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, title ? mTitleSize : mSubtitleSize);
+            tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, title ? mGridTitleSize : mGridSubtitleSize);
             tv.setTextColor(title ? mTitleColor : mSubtitleColor);
             CharSequence text = FORMAT_TIMESTAMP.equals(format)
                     ? SliceViewUtil.getRelativeTimeString(item.getTimestamp())
                     : item.getText();
             tv.setText(text);
             container.addView(tv);
+            tv.setPadding(0, padding, 0, 0);
             addedView = tv;
         } else if (FORMAT_IMAGE.equals(format)) {
             ImageView iv = new ImageView(getContext());
@@ -339,6 +379,19 @@
         return addedView != null;
     }
 
+    private int determinePadding(SliceItem prevItem) {
+        if (prevItem == null) {
+            // No need for top padding
+            return 0;
+        } else if (FORMAT_IMAGE.equals(prevItem.getFormat())) {
+            return mTextPadding;
+        } else if (FORMAT_TEXT.equals(prevItem.getFormat())
+                || FORMAT_LONG.equals(prevItem.getFormat())) {
+            return mVerticalGridTextPadding;
+        }
+        return 0;
+    }
+
     private void makeClickable(View layout, boolean isClickable) {
         layout.setOnClickListener(isClickable ? this : null);
         layout.setBackground(isClickable
@@ -354,7 +407,7 @@
         final EventInfo info = tagItem.second;
         if (actionItem != null && FORMAT_ACTION.equals(actionItem.getFormat())) {
             try {
-                actionItem.getAction().send();
+                actionItem.fireAction(null, null);
                 if (mObserver != null) {
                     mObserver.onSliceAction(info, actionItem);
                 }
diff --git a/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java b/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java
index 9270353c..81059d4 100644
--- a/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java
+++ b/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java
@@ -17,6 +17,7 @@
 package androidx.slice.widget;
 
 import static android.app.slice.Slice.HINT_HORIZONTAL;
+import static android.app.slice.Slice.HINT_SUMMARY;
 import static android.app.slice.Slice.SUBTYPE_MESSAGE;
 import static android.app.slice.Slice.SUBTYPE_SOURCE;
 import static android.app.slice.SliceItem.FORMAT_INT;
@@ -103,14 +104,14 @@
     /**
      * Set the {@link SliceItem}'s to be displayed in the adapter and the accent color.
      */
-    public void setSliceItems(List<SliceItem> slices, int color) {
+    public void setSliceItems(List<SliceItem> slices, int color, int mode) {
         if (slices == null) {
             mSlices.clear();
         } else {
             mIdGen.resetUsage();
             mSlices = new ArrayList<>(slices.size());
             for (SliceItem s : slices) {
-                mSlices.add(new SliceWrapper(s, mIdGen));
+                mSlices.add(new SliceWrapper(s, mIdGen, mode));
             }
         }
         mColor = color;
@@ -199,10 +200,10 @@
         private final int mType;
         private final long mId;
 
-        public SliceWrapper(SliceItem item, IdGenerator idGen) {
+        public SliceWrapper(SliceItem item, IdGenerator idGen, int mode) {
             mItem = item;
             mType = getFormat(item);
-            mId = idGen.getId(item);
+            mId = idGen.getId(item, mode);
         }
 
         public static int getFormat(SliceItem item) {
@@ -250,12 +251,12 @@
             mSliceChildView.setMode(mode);
             mSliceChildView.setTint(mColor);
             mSliceChildView.setStyle(mAttrs, mDefStyleAttr, mDefStyleRes);
-            mSliceChildView.setSliceItem(item, isHeader, position, mSliceObserver);
-            if (isHeader && mSliceChildView instanceof RowView) {
+            mSliceChildView.setSliceItem(item, isHeader, position, getItemCount(), mSliceObserver);
+            mSliceChildView.setSliceActions(isHeader ? mSliceActions : null);
+            mSliceChildView.setLastUpdated(isHeader ? mLastUpdated : -1);
+            mSliceChildView.setShowLastUpdated(isHeader && mShowLastUpdated);
+            if (mSliceChildView instanceof RowView) {
                 ((RowView) mSliceChildView).setSingleItem(getItemCount() == 1);
-                mSliceChildView.setSliceActions(mSliceActions);
-                mSliceChildView.setLastUpdated(mLastUpdated);
-                mSliceChildView.setShowLastUpdated(mShowLastUpdated);
             }
             int[] info = new int[2];
             info[0] = ListContent.getRowType(mContext, item, isHeader, mSliceActions);
@@ -285,8 +286,12 @@
         private final ArrayMap<String, Long> mCurrentIds = new ArrayMap<>();
         private final ArrayMap<String, Integer> mUsedIds = new ArrayMap<>();
 
-        public long getId(SliceItem item) {
+        public long getId(SliceItem item, int mode) {
             String str = genString(item);
+            SliceItem summary = SliceQuery.find(item, null, HINT_SUMMARY, null);
+            if (summary != null) {
+                str += mode; // mode matters
+            }
             if (!mCurrentIds.containsKey(str)) {
                 mCurrentIds.put(str, mNextLong++);
             }
diff --git a/slices/view/src/main/java/androidx/slice/widget/LargeTemplateView.java b/slices/view/src/main/java/androidx/slice/widget/LargeTemplateView.java
index 6632097..0cba347 100644
--- a/slices/view/src/main/java/androidx/slice/widget/LargeTemplateView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/LargeTemplateView.java
@@ -16,7 +16,7 @@
 
 package androidx.slice.widget;
 
-import static android.app.slice.Slice.HINT_HORIZONTAL;
+import static androidx.slice.widget.SliceView.MODE_SMALL;
 
 import android.content.Context;
 import android.os.Build;
@@ -28,7 +28,6 @@
 import androidx.annotation.RestrictTo;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
-import androidx.slice.Slice;
 import androidx.slice.SliceItem;
 
 import java.util.ArrayList;
@@ -45,7 +44,6 @@
     private final View mForeground;
     private final LargeSliceAdapter mAdapter;
     private final RecyclerView mRecyclerView;
-    private Slice mSlice;
     private boolean mIsScrollable;
     private ListContent mListContent;
     private List<SliceItem> mDisplayedItems = new ArrayList<>();
@@ -131,13 +129,8 @@
             return 0;
         }
         SliceItem headerItem = mListContent.getHeaderItem();
-        if (headerItem.hasHint(HINT_HORIZONTAL)) {
-            GridContent gc = new GridContent(getContext(), headerItem);
-            return gc.getSmallHeight();
-        } else {
-            RowContent rc = new RowContent(getContext(), headerItem, mListContent.hasHeader());
-            return rc.getSmallHeight();
-        }
+        return mListContent.getHeight(getContext(), headerItem, true /* isHeader */,
+                0 /* rowIndex */, 1 /* rowCount */, MODE_SMALL);
     }
 
     @Override
@@ -160,8 +153,8 @@
     }
 
     @Override
-    public void setSlice(Slice slice) {
-        mSlice = slice;
+    public void setSliceContent(ListContent sliceContent) {
+        mListContent = sliceContent;
         populate();
     }
 
@@ -184,11 +177,10 @@
     }
 
     private void populate() {
-        if (mSlice == null) {
+        if (mListContent == null) {
             resetView();
             return;
         }
-        mListContent = new ListContent(getContext(), mSlice);
         updateDisplayedItems(getMeasuredHeight());
     }
 
@@ -216,21 +208,21 @@
         } else {
             mDisplayedItems = mListContent.getRowItems();
         }
-        mDisplayedItemsHeight = ListContent.getListHeight(getContext(), mDisplayedItems);
-        if (getMode() == SliceView.MODE_LARGE) {
-            mAdapter.setSliceItems(mDisplayedItems, mTintColor);
-        } else if (getMode() == SliceView.MODE_SMALL) {
+        mDisplayedItemsHeight = mListContent.getListHeight(getContext(), mDisplayedItems);
+        int mode = getMode();
+        if (mode == SliceView.MODE_LARGE) {
+            mAdapter.setSliceItems(mDisplayedItems, mTintColor, mode);
+        } else if (mode == MODE_SMALL) {
             mAdapter.setSliceItems(
-                    Collections.singletonList(mDisplayedItems.get(0)), mTintColor);
+                    Collections.singletonList(mDisplayedItems.get(0)), mTintColor, mode);
         }
     }
 
     @Override
     public void resetView() {
-        mSlice = null;
         mDisplayedItemsHeight = 0;
         mDisplayedItems.clear();
-        mAdapter.setSliceItems(null, -1);
+        mAdapter.setSliceItems(null, -1, getMode());
         mListContent = null;
     }
 }
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 1dca6cb..8f3cdf2 100644
--- a/slices/view/src/main/java/androidx/slice/widget/ListContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/ListContent.java
@@ -30,8 +30,12 @@
 import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
 import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
 import static androidx.slice.core.SliceHints.HINT_TTL;
+import static androidx.slice.widget.SliceView.MODE_LARGE;
+import static androidx.slice.widget.SliceView.MODE_SMALL;
 
 import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -42,6 +46,7 @@
 import androidx.slice.core.SliceAction;
 import androidx.slice.core.SliceActionImpl;
 import androidx.slice.core.SliceQuery;
+import androidx.slice.view.R;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -53,6 +58,7 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public class ListContent {
 
+    private Slice mSlice;
     private SliceItem mHeaderItem;
     private SliceItem mColorItem;
     private SliceItem mSeeMoreItem;
@@ -60,8 +66,54 @@
     private List<SliceItem> mSliceActions;
     private Context mContext;
 
-    public ListContent(Context context, Slice slice) {
+    private int mHeaderTitleSize;
+    private int mHeaderSubtitleSize;
+    private int mVerticalHeaderTextPadding;
+    private int mTitleSize;
+    private int mSubtitleSize;
+    private int mVerticalTextPadding;
+    private int mGridTitleSize;
+    private int mGridSubtitleSize;
+    private int mVerticalGridTextPadding;
+    private int mGridTopPadding;
+    private int mGridBottomPadding;
+
+    public ListContent(Context context, Slice slice, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        mSlice = slice;
         mContext = context;
+
+        // TODO: duplicated code from SliceChildView; could do something better
+        // Some of this information will impact the size calculations for slice content.
+        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SliceView,
+                defStyleAttr, defStyleRes);
+        try {
+            mHeaderTitleSize = (int) a.getDimension(
+                    R.styleable.SliceView_headerTitleSize, 0);
+            mHeaderSubtitleSize = (int) a.getDimension(
+                    R.styleable.SliceView_headerSubtitleSize, 0);
+            mVerticalHeaderTextPadding = (int) a.getDimension(
+                    R.styleable.SliceView_headerTextVerticalPadding, 0);
+
+            mTitleSize = (int) a.getDimension(R.styleable.SliceView_titleSize, 0);
+            mSubtitleSize = (int) a.getDimension(
+                    R.styleable.SliceView_subtitleSize, 0);
+            mVerticalTextPadding = (int) a.getDimension(
+                    R.styleable.SliceView_textVerticalPadding, 0);
+
+            mGridTitleSize = (int) a.getDimension(R.styleable.SliceView_gridTitleSize, 0);
+            mGridSubtitleSize = (int) a.getDimension(
+                    R.styleable.SliceView_gridSubtitleSize, 0);
+            int defaultVerticalGridPadding = context.getResources().getDimensionPixelSize(
+                    R.dimen.abc_slice_grid_text_inner_padding);
+            mVerticalGridTextPadding = (int) a.getDimension(
+                    R.styleable.SliceView_gridTextVerticalPadding, defaultVerticalGridPadding);
+            mGridTopPadding = (int) a.getDimension(R.styleable.SliceView_gridTopPadding, 0);
+            mGridBottomPadding = (int) a.getDimension(R.styleable.SliceView_gridTopPadding, 0);
+        } finally {
+            a.recycle();
+        }
+
         populate(slice);
     }
 
@@ -107,7 +159,7 @@
      *
      * @return the total height of all the rows contained in the provided list.
      */
-    public static int getListHeight(Context context, List<SliceItem> listItems) {
+    public int getListHeight(Context context, List<SliceItem> listItems) {
         if (listItems == null) {
             return 0;
         }
@@ -119,10 +171,12 @@
             hasRealHeader = !maybeHeader.hasAnyHints(HINT_LIST_ITEM, HINT_HORIZONTAL);
         }
         if (listItems.size() == 1 && !maybeHeader.hasHint(HINT_HORIZONTAL)) {
-            return getHeight(context, maybeHeader, true);
+            return getHeight(context, maybeHeader, true /* isHeader */, 0, 1, MODE_LARGE);
         }
+        int rowCount = listItems.size();
         for (int i = 0; i < listItems.size(); i++) {
-            height += getHeight(context, listItems.get(i), i == 0 && hasRealHeader /* isHeader */);
+            height += getHeight(context, listItems.get(i), i == 0 && hasRealHeader /* isHeader */,
+                    i, rowCount, MODE_LARGE);
         }
         return height;
     }
@@ -149,8 +203,10 @@
             RowContent rc = new RowContent(mContext, mSeeMoreItem, false /* isHeader */);
             visibleHeight += rc.getActualHeight();
         }
-        for (int i = 0; i < mRowItems.size(); i++) {
-            int itemHeight = getHeight(mContext, mRowItems.get(i), i == 0 /* isHeader */);
+        int rowCount = mRowItems.size();
+        for (int i = 0; i < rowCount; i++) {
+            int itemHeight = getHeight(mContext, mRowItems.get(i), i == 0 /* isHeader */,
+                    i, rowCount, MODE_LARGE);
             if ((height == -1 && i > idealItemCount)
                     || (height > 0 && visibleHeight + itemHeight > height)) {
                 break;
@@ -170,13 +226,20 @@
         return visibleItems;
     }
 
-    private static int getHeight(Context context, SliceItem item, boolean isHeader) {
+    /**
+     * Determines the height of the provided {@link SliceItem}.
+     */
+    public int getHeight(Context context, SliceItem item, boolean isHeader, int index,
+            int count, int mode) {
         if (item.hasHint(HINT_HORIZONTAL)) {
             GridContent gc = new GridContent(context, item);
-            return gc.getActualHeight();
+            int topPadding = gc.isAllImages() && index == 0 ? mGridTopPadding : 0;
+            int bottomPadding = gc.isAllImages() && index == count - 1 ? mGridBottomPadding : 0;
+            int height = mode == MODE_SMALL ? gc.getSmallHeight() : gc.getActualHeight();
+            return height + topPadding + bottomPadding;
         } else {
             RowContent rc = new RowContent(context, item, isHeader);
-            return rc.getActualHeight();
+            return mode == MODE_SMALL ? rc.getSmallHeight() : rc.getActualHeight();
         }
     }
 
@@ -188,6 +251,11 @@
     }
 
     @Nullable
+    public Slice getSlice() {
+        return mSlice;
+    }
+
+    @Nullable
     public SliceItem getColorItem() {
         return mColorItem;
     }
@@ -291,7 +359,7 @@
     private static SliceItem findHeaderItem(@NonNull Slice slice) {
         // See if header is specified
         String[] nonHints = new String[] {HINT_LIST_ITEM, HINT_SHORTCUT, HINT_ACTIONS,
-                HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED};
+                HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED, HINT_HORIZONTAL};
         SliceItem header = SliceQuery.find(slice, FORMAT_SLICE, null, nonHints);
         if (header != null && isValidHeader(header)) {
             return header;
@@ -302,7 +370,7 @@
     @Nullable
     private static SliceItem getSeeMoreItem(@NonNull Slice slice) {
         SliceItem item = SliceQuery.find(slice, null, HINT_SEE_MORE, null);
-        if (item != null && item.hasHint(HINT_SEE_MORE)) {
+        if (item != null) {
             if (FORMAT_SLICE.equals(item.getFormat())) {
                 List<SliceItem> items = item.getSlice().getItems();
                 if (items.size() == 1 && FORMAT_ACTION.equals(items.get(0).getFormat())) {
diff --git a/slices/view/src/main/java/androidx/slice/widget/MessageView.java b/slices/view/src/main/java/androidx/slice/widget/MessageView.java
index 6035d0a..c597ed4 100644
--- a/slices/view/src/main/java/androidx/slice/widget/MessageView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/MessageView.java
@@ -30,7 +30,6 @@
 import android.widget.TextView;
 
 import androidx.annotation.RestrictTo;
-import androidx.slice.Slice;
 import androidx.slice.SliceItem;
 import androidx.slice.core.SliceQuery;
 
@@ -57,11 +56,6 @@
     }
 
     @Override
-    public void setSlice(Slice slice) {
-        // Do nothing it's always a list item
-    }
-
-    @Override
     public void resetView() {
         // TODO
     }
@@ -75,7 +69,7 @@
 
     @Override
     public void setSliceItem(SliceItem slice, boolean isHeader, int index,
-            SliceView.OnSliceActionListener observer) {
+            int rowCount, SliceView.OnSliceActionListener observer) {
         setSliceActionListener(observer);
         mRowIndex = index;
         SliceItem source = SliceQuery.findSubtype(slice, FORMAT_IMAGE, SUBTYPE_SOURCE);
diff --git a/slices/view/src/main/java/androidx/slice/widget/RemoteInputView.java b/slices/view/src/main/java/androidx/slice/widget/RemoteInputView.java
index 1792f29..d4ff299 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RemoteInputView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RemoteInputView.java
@@ -50,6 +50,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.core.content.ContextCompat;
+import androidx.slice.SliceItem;
 import androidx.slice.view.R;
 
 /**
@@ -72,7 +73,7 @@
     private RemoteEditText mEditText;
     private ImageButton mSendButton;
     private ProgressBar mProgressBar;
-    private PendingIntent mPendingIntent;
+    private SliceItem mAction;
     private RemoteInput[] mRemoteInputs;
     private RemoteInput mRemoteInput;
 
@@ -139,11 +140,11 @@
         // but that's an edge case, and also because we can't always know which package will receive
         // an intent, so we just reset for the creator.
         //getContext().getSystemService(ShortcutManager.class).onApplicationActive(
-        //        mPendingIntent.getCreatorPackage(),
+        //        mAction.getCreatorPackage(),
         //        getContext().getUserId());
 
         try {
-            mPendingIntent.send(getContext(), 0, fillInIntent);
+            mAction.fireAction(getContext(), fillInIntent);
             reset();
         } catch (PendingIntent.CanceledException e) {
             Log.i(TAG, "Unable to send remote input result", e);
@@ -185,8 +186,8 @@
     /**
      * Set the pending intent for remote input.
      */
-    public void setPendingIntent(PendingIntent pendingIntent) {
-        mPendingIntent = pendingIntent;
+    public void setAction(SliceItem action) {
+        mAction = action;
     }
 
     /**
diff --git a/slices/view/src/main/java/androidx/slice/widget/RowContent.java b/slices/view/src/main/java/androidx/slice/widget/RowContent.java
index c600af5..c865b02 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RowContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RowContent.java
@@ -16,7 +16,7 @@
 
 package androidx.slice.widget;
 
-import static android.app.slice.Slice.HINT_ACTIONS;
+import static android.app.slice.Slice.HINT_HORIZONTAL;
 import static android.app.slice.Slice.HINT_PARTIAL;
 import static android.app.slice.Slice.HINT_SEE_MORE;
 import static android.app.slice.Slice.HINT_SHORTCUT;
@@ -95,15 +95,7 @@
             Log.w(TAG, "Provided SliceItem is invalid for RowContent");
             return false;
         }
-        // Find primary action first (otherwise filtered out of valid row items)
-        String[] hints = new String[] {HINT_SHORTCUT, HINT_TITLE};
-        mPrimaryAction = SliceQuery.find(rowSlice, FORMAT_SLICE, hints,
-                new String[] { HINT_ACTIONS, HINT_KEYWORDS} /* nonHints */);
-
-        if (mPrimaryAction == null && FORMAT_ACTION.equals(rowSlice.getFormat())
-                && rowSlice.getSlice().getItems().size() == 1) {
-            mPrimaryAction = rowSlice;
-        }
+        determineStartAndPrimaryAction(rowSlice);
 
         mContentDescr = SliceQuery.findSubtype(rowSlice, FORMAT_TEXT, SUBTYPE_CONTENT_DESCRIPTION);
 
@@ -112,7 +104,7 @@
         // If we've only got one item that's a slice / action use those items instead
         if (rowItems.size() == 1 && (FORMAT_ACTION.equals(rowItems.get(0).getFormat())
                 || FORMAT_SLICE.equals(rowItems.get(0).getFormat()))
-                && !rowItems.get(0).hasHint(HINT_SHORTCUT)) {
+                && !rowItems.get(0).hasAnyHints(HINT_SHORTCUT, HINT_TITLE)) {
             if (isValidRow(rowItems.get(0))) {
                 rowSlice = rowItems.get(0);
                 rowItems = filterInvalidItems(rowSlice);
@@ -122,14 +114,12 @@
             mRange = rowSlice;
         }
         if (rowItems.size() > 0) {
-            // Start item
-            SliceItem firstItem = rowItems.get(0);
-            if (FORMAT_SLICE.equals(firstItem.getFormat())) {
-                SliceItem unwrappedItem = firstItem.getSlice().getItems().get(0);
-                if (isStartType(unwrappedItem)) {
-                    mStartItem = unwrappedItem;
-                    rowItems.remove(0);
-                }
+            // Remove the things we already know about
+            if (mStartItem != null) {
+                rowItems.remove(mStartItem);
+            }
+            if (mPrimaryAction != null) {
+                rowItems.remove(mPrimaryAction);
             }
 
             // Text + end items
@@ -155,23 +145,18 @@
             if (hasText(mSubtitleItem)) {
                 mLineCount++;
             }
-            // Special rules for end items: only one timestamp, can't be mixture of icons / actions
+            // Special rules for end items: only one timestamp
             boolean hasTimestamp = mStartItem != null
                     && FORMAT_TIMESTAMP.equals(mStartItem.getFormat());
-            String desiredFormat = null;
             for (int i = 0; i < endItems.size(); i++) {
                 final SliceItem item = endItems.get(i);
-                boolean isAction = FORMAT_SLICE.equals(item.getFormat())
-                        && item.hasHint(HINT_SHORTCUT);
+                boolean isAction = SliceQuery.find(item, FORMAT_ACTION) != null;
                 if (FORMAT_TIMESTAMP.equals(item.getFormat())) {
                     if (!hasTimestamp) {
                         hasTimestamp = true;
                         mEndItems.add(item);
                     }
-                } else if (desiredFormat == null) {
-                    desiredFormat = item.getFormat();
-                    processContent(item, isAction);
-                } else if (desiredFormat.equals(item.getFormat())) {
+                } else {
                     processContent(item, isAction);
                 }
             }
@@ -191,6 +176,37 @@
     }
 
     /**
+     * Sets the {@link #getPrimaryAction()} and {@link #getStartItem()} for this row.
+     */
+    private void determineStartAndPrimaryAction(@NonNull SliceItem rowSlice) {
+        List<SliceItem> possibleStartItems = SliceQuery.findAll(rowSlice, null, HINT_TITLE, null);
+        if (possibleStartItems.size() > 0) {
+            // The start item will be at position 0 if it exists
+            String format = possibleStartItems.get(0).getFormat();
+            if ((FORMAT_ACTION.equals(format)
+                    && SliceQuery.find(possibleStartItems.get(0), FORMAT_IMAGE) != null)
+                    || FORMAT_SLICE.equals(format)
+                    || FORMAT_LONG.equals(format)
+                    || FORMAT_IMAGE.equals(format)) {
+                mStartItem = possibleStartItems.get(0);
+            }
+        }
+
+        String[] hints = new String[] {HINT_SHORTCUT, HINT_TITLE};
+        List<SliceItem> possiblePrimaries = SliceQuery.findAll(rowSlice, FORMAT_SLICE, hints, null);
+        if (possiblePrimaries.isEmpty() && FORMAT_ACTION.equals(rowSlice.getFormat())
+                && rowSlice.getSlice().getItems().size() == 1) {
+            mPrimaryAction = rowSlice;
+        } else if (mStartItem != null && possiblePrimaries.size() > 1
+                && possiblePrimaries.get(0) == mStartItem) {
+            // Next item is the primary action
+            mPrimaryAction = possiblePrimaries.get(1);
+        } else if (possiblePrimaries.size() > 0) {
+            mPrimaryAction = possiblePrimaries.get(0);
+        }
+    }
+
+    /**
      * @return the {@link SliceItem} used to populate this row.
      */
     @NonNull
@@ -207,6 +223,22 @@
     }
 
     /**
+     * @return the {@link SliceItem} for the icon to use for the input range thumb drawable.
+     */
+    @Nullable
+    public SliceItem getInputRangeThumb() {
+        if (mRange != null) {
+            List<SliceItem> items = mRange.getSlice().getItems();
+            for (int i = 0; i < items.size(); i++) {
+                if (FORMAT_IMAGE.equals(items.get(i).getFormat())) {
+                    return items.get(i);
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
      * @return the {@link SliceItem} used for the main intent for this row; can be null.
      */
     @Nullable
@@ -326,6 +358,7 @@
      */
     public boolean isValid() {
         return mStartItem != null
+                || mPrimaryAction != null
                 || mTitleItem != null
                 || mSubtitleItem != null
                 || mEndItems.size() > 0
@@ -343,17 +376,17 @@
         // Must be slice or action
         if (FORMAT_SLICE.equals(rowSlice.getFormat())
                 || FORMAT_ACTION.equals(rowSlice.getFormat())) {
-            // Must have at least one legitimate child
             List<SliceItem> rowItems = rowSlice.getSlice().getItems();
+            // Special case: default see more just has an action but no other items
+            if (rowSlice.hasHint(HINT_SEE_MORE) && rowItems.isEmpty()) {
+                return true;
+            }
+            // Must have at least one legitimate child
             for (int i = 0; i < rowItems.size(); i++) {
                 if (isValidRowContent(rowSlice, rowItems.get(i))) {
                     return true;
                 }
             }
-            // Special case: default see more just has an action but no other items
-            if (rowSlice.hasHint(HINT_SEE_MORE) && rowItems.isEmpty()) {
-                return true;
-            }
         }
         return false;
     }
@@ -373,27 +406,20 @@
     }
 
     /**
-     * @return whether this item is valid content to display in a row.
+     * @return whether this item is valid content to visibly appear in a row.
      */
     private static boolean isValidRowContent(SliceItem slice, SliceItem item) {
-        if (item.hasAnyHints(HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED)) {
+        if (item.hasAnyHints(HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED, HINT_HORIZONTAL)
+                || SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) {
             return false;
         }
-        if (FORMAT_SLICE.equals(item.getFormat()) && !item.hasHint(HINT_SHORTCUT)) {
-            // Unpack contents of slice
-            item = item.getSlice().getItems().get(0);
-        }
         final String itemFormat = item.getFormat();
-        return (FORMAT_TEXT.equals(itemFormat)
-                && !SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType()))
-                || FORMAT_IMAGE.equals(itemFormat)
+        return FORMAT_IMAGE.equals(itemFormat)
+                || FORMAT_TEXT.equals(itemFormat)
                 || FORMAT_TIMESTAMP.equals(itemFormat)
-                || FORMAT_REMOTE_INPUT.equals(itemFormat)
-                || (FORMAT_SLICE.equals(itemFormat) && item.hasHint(HINT_TITLE)
-                && !item.hasHint(HINT_SHORTCUT))
-                || (FORMAT_SLICE.equals(itemFormat) && item.hasHint(HINT_SHORTCUT)
-                && !item.hasHint(HINT_TITLE))
                 || FORMAT_ACTION.equals(itemFormat)
+                || FORMAT_REMOTE_INPUT.equals(itemFormat)
+                || FORMAT_SLICE.equals(itemFormat)
                 || (FORMAT_INT.equals(itemFormat) && SUBTYPE_RANGE.equals(slice.getSubType()));
     }
 
diff --git a/slices/view/src/main/java/androidx/slice/widget/RowView.java b/slices/view/src/main/java/androidx/slice/widget/RowView.java
index 4f25fed..341f21c 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RowView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RowView.java
@@ -21,7 +21,6 @@
 import static android.app.slice.Slice.HINT_PARTIAL;
 import static android.app.slice.Slice.HINT_SHORTCUT;
 import static android.app.slice.Slice.SUBTYPE_MAX;
-import static android.app.slice.Slice.SUBTYPE_TOGGLE;
 import static android.app.slice.Slice.SUBTYPE_VALUE;
 import static android.app.slice.SliceItem.FORMAT_ACTION;
 import static android.app.slice.SliceItem.FORMAT_IMAGE;
@@ -38,7 +37,6 @@
 import static androidx.slice.widget.EventInfo.ROW_TYPE_TOGGLE;
 import static androidx.slice.widget.SliceView.MODE_SMALL;
 
-import android.app.PendingIntent;
 import android.app.PendingIntent.CanceledException;
 import android.content.Context;
 import android.content.Intent;
@@ -47,6 +45,7 @@
 import android.text.SpannableString;
 import android.text.TextUtils;
 import android.text.style.StyleSpan;
+import android.util.ArrayMap;
 import android.util.Log;
 import android.util.TypedValue;
 import android.view.LayoutInflater;
@@ -63,13 +62,11 @@
 import androidx.annotation.RestrictTo;
 import androidx.core.graphics.drawable.DrawableCompat;
 import androidx.core.graphics.drawable.IconCompat;
-import androidx.slice.Slice;
 import androidx.slice.SliceItem;
 import androidx.slice.core.SliceActionImpl;
 import androidx.slice.core.SliceQuery;
 import androidx.slice.view.R;
 
-import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -93,7 +90,7 @@
     private TextView mSecondaryText;
     private TextView mLastUpdatedText;
     private View mDivider;
-    private ArrayList<SliceActionView> mToggles = new ArrayList<>();
+    private ArrayMap<SliceActionImpl, SliceActionView> mToggles = new ArrayMap<>();
     private LinearLayout mEndContainer;
     private ProgressBar mRangeBar;
     private View mSeeMoreView;
@@ -218,17 +215,12 @@
         }
     }
 
-    @Override
-    public void setSlice(Slice slice) {
-        // Nothing to do
-    }
-
     /**
      * This is called when RowView is being used as a component in a large template.
      */
     @Override
     public void setSliceItem(SliceItem slice, boolean isHeader, int index,
-            SliceView.OnSliceActionListener observer) {
+            int rowCount, SliceView.OnSliceActionListener observer) {
         setSliceActionListener(observer);
         mRowIndex = index;
         mIsHeader = ListContent.isValidHeader(slice);
@@ -247,9 +239,9 @@
         if (contentDescr != null) {
             mContent.setContentDescription(contentDescr);
         }
-        boolean showStart = false;
         final SliceItem startItem = mRowContent.getStartItem();
-        if (startItem != null) {
+        boolean showStart = startItem != null && mRowIndex > 0;
+        if (showStart) {
             showStart = addItem(startItem, mTintColor, true /* isStart */);
         }
         mStartContainer.setVisibility(showStart ? View.VISIBLE : View.GONE);
@@ -270,11 +262,12 @@
         addSubtitle(subtitleItem);
 
         SliceItem primaryAction = mRowContent.getPrimaryAction();
-        if (primaryAction != null) {
+        if (primaryAction != null && primaryAction != startItem) {
             mRowAction = new SliceActionImpl(primaryAction);
             if (mRowAction.isToggle()) {
                 // If primary action is a toggle, add it and we're done
                 addAction(mRowAction, mTintColor, mEndContainer, false /* isStart */);
+                // TODO: if start item is tappable, touch feedback should exclude it
                 setViewClickable(mRootView, true);
                 return;
             }
@@ -291,42 +284,21 @@
 
         // If we're here we can can show end items; check for top level actions first
         List<SliceItem> endItems = mRowContent.getEndItems();
-        String desiredFormat = FORMAT_ACTION;
         if (mHeaderActions != null && mHeaderActions.size() > 0) {
             // Use these if we have them instead
             endItems = mHeaderActions;
-        } else if (!endItems.isEmpty()) {
-            // Prefer to show actions as end items if possible; fall back to the first format type.
-            SliceItem firstEndItem = endItems.get(0);
-            desiredFormat = mRowContent.endItemsContainAction()
-                    ? FORMAT_ACTION : firstEndItem.getSlice().getItems().get(0).getFormat();
         }
-        boolean hasRowAction = mRowAction != null;
-        if (endItems.isEmpty()) {
-            if (hasRowAction) setViewClickable(mRootView, true);
-            return;
-        }
-
         // If we're here we might be able to show end items
         int itemCount = 0;
         boolean firstItemIsADefaultToggle = false;
+        boolean hasEndItemAction = false;
         for (int i = 0; i < endItems.size(); i++) {
             final SliceItem endItem = endItems.get(i);
-            final String endFormat = endItem.hasHint(HINT_SHORTCUT)
-                    ? FORMAT_ACTION
-                    : endItem.getSlice().getItems().get(0).getFormat();
-            // Only show one type of format at the end of the slice, use whatever is first
-            if (itemCount < MAX_END_ITEMS
-                    && (desiredFormat.equals(endFormat)
-                    || FORMAT_TIMESTAMP.equals(endFormat))) {
-
-                final EventInfo info = new EventInfo(getMode(),
-                        EventInfo.ACTION_TYPE_BUTTON,
-                        EventInfo.ROW_TYPE_LIST, mRowIndex);
-                info.setPosition(EventInfo.POSITION_END, i,
-                        Math.min(endItems.size(), MAX_END_ITEMS));
-
+            if (itemCount < MAX_END_ITEMS) {
                 if (addItem(endItem, mTintColor, false /* isStart */)) {
+                    if (SliceQuery.find(endItem, FORMAT_ACTION) != null) {
+                        hasEndItemAction = true;
+                    }
                     itemCount++;
                     if (itemCount == 1) {
                         firstItemIsADefaultToggle = !mToggles.isEmpty()
@@ -336,21 +308,22 @@
             }
         }
 
-        boolean hasEndItemAction = FORMAT_ACTION.contentEquals(desiredFormat);
         // If there is a row action and the first end item is a default toggle, show the divider.
-        mDivider.setVisibility(hasRowAction && firstItemIsADefaultToggle
+        mDivider.setVisibility(mRowAction != null && firstItemIsADefaultToggle
                 ? View.VISIBLE : View.GONE);
-        if (hasRowAction) {
-            if (itemCount > 0 && hasEndItemAction) {
-                setViewClickable(mContent, true);
+        boolean hasStartAction = startItem != null
+                && SliceQuery.find(startItem, FORMAT_ACTION) != null;
+
+        if (mRowAction != null) {
+            // If there are outside actions make only the content bit clickable
+            // TODO: if start item is an image touch feedback should include it
+            setViewClickable((hasEndItemAction || hasStartAction) ? mContent : mRootView, true);
+        } else if (hasEndItemAction != hasStartAction && (itemCount == 1 || hasStartAction)) {
+            // Single action; make whole row clickable for it
+            if (!mToggles.isEmpty()) {
+                mRowAction = mToggles.keySet().iterator().next();
             } else {
-                setViewClickable(mRootView, true);
-            }
-        } else if (mRowContent.endItemsContainAction() && itemCount == 1) {
-            // If the only end item is an action, make the whole row clickable.
-            SliceItem unwrappedActionItem = endItems.get(0).getSlice().getItems().get(0);
-            if (!SUBTYPE_TOGGLE.equals(unwrappedActionItem.getSubType())) {
-                mRowAction = new SliceActionImpl(endItems.get(0));
+                mRowAction = new SliceActionImpl(hasEndItemAction ? endItems.get(0) : startItem);
             }
             setViewClickable(mRootView, true);
         }
@@ -358,7 +331,7 @@
 
     private void addSubtitle(final SliceItem subtitleItem) {
         CharSequence subtitleTimeString = null;
-        if (mShowLastUpdated) {
+        if (mShowLastUpdated && mLastUpdated != -1) {
             subtitleTimeString = getResources().getString(R.string.abc_slice_updated,
                     SliceViewUtil.getRelativeTimeString(mLastUpdated));
         }
@@ -371,6 +344,8 @@
                     ? mHeaderSubtitleSize
                     : mSubtitleSize);
             mSecondaryText.setTextColor(mSubtitleColor);
+            int verticalPadding = mIsHeader ? mVerticalHeaderTextPadding : mVerticalTextPadding;
+            mSecondaryText.setPadding(0, verticalPadding, 0, 0);
         }
         if (subtitleTimeString != null) {
             if (!TextUtils.isEmpty(subtitle)) {
@@ -415,7 +390,7 @@
         addView(progressBar);
         mRangeBar = progressBar;
         if (isSeekBar) {
-            SliceItem thumb = SliceQuery.find(range, FORMAT_IMAGE);
+            SliceItem thumb = mRowContent.getInputRangeThumb();
             SeekBar seekBar = (SeekBar) mRangeBar;
             if (thumb != null) {
                 seekBar.setThumb(thumb.getIcon().loadDrawable(getContext()));
@@ -431,10 +406,9 @@
                 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                     progress += finalMinValue;
                     try {
-                        PendingIntent pi = range.getAction();
-                        Intent i = new Intent().putExtra(EXTRA_RANGE_VALUE, progress);
                         // TODO: sending this PendingIntent should be rate limited.
-                        pi.send(getContext(), 0, i, null, null);
+                        range.fireAction(getContext(),
+                                new Intent().putExtra(EXTRA_RANGE_VALUE, progress));
                     } catch (CanceledException e) { }
                 }
 
@@ -463,9 +437,8 @@
             info.setPosition(EventInfo.POSITION_START, 0, 1);
         }
         sav.setAction(actionContent, info, mObserver, color);
-
         if (isToggle) {
-            mToggles.add(sav);
+            mToggles.put(actionContent, sav);
         }
     }
 
@@ -478,7 +451,8 @@
         int imageMode = 0;
         SliceItem timeStamp = null;
         ViewGroup container = isStart ? mStartContainer : mEndContainer;
-        if (FORMAT_SLICE.equals(sliceItem.getFormat())) {
+        if (FORMAT_SLICE.equals(sliceItem.getFormat())
+                || FORMAT_ACTION.equals(sliceItem.getFormat())) {
             if (sliceItem.hasHint(HINT_SHORTCUT)) {
                 addAction(new SliceActionImpl(sliceItem), color, container, isStart);
                 return true;
@@ -532,7 +506,7 @@
                                 EventInfo.ROW_TYPE_LIST, mRowIndex);
                         mObserver.onSliceAction(info, mRowContent.getSlice());
                     }
-                    mRowContent.getSlice().getAction().send();
+                    mRowContent.getSlice().fireAction(null, null);
                 } catch (CanceledException e) {
                     Log.e(TAG, "PendingIntent for slice cannot be sent", e);
                 }
@@ -547,21 +521,25 @@
 
     @Override
     public void onClick(View view) {
-        if (mRowAction != null && mRowAction.getAction() != null && !mRowAction.isToggle()) {
-            // Check for a row action
-            try {
-                mRowAction.getAction().send();
-                if (mObserver != null) {
-                    EventInfo info = new EventInfo(getMode(), EventInfo.ACTION_TYPE_CONTENT,
-                            EventInfo.ROW_TYPE_LIST, mRowIndex);
-                    mObserver.onSliceAction(info, mRowAction.getSliceItem());
+        if (mRowAction != null && mRowAction.getActionItem() != null) {
+            // Check if it's a row click for a toggle, in this case need to update the UI
+            if (mRowAction.isToggle() && !(view instanceof SliceActionView)) {
+                SliceActionView sav = mToggles.get(mRowAction);
+                if (sav != null) {
+                    sav.toggle();
                 }
-            } catch (CanceledException e) {
-                Log.e(TAG, "PendingIntent for slice cannot be sent", e);
+            } else {
+                try {
+                    mRowAction.getActionItem().fireAction(null, null);
+                    if (mObserver != null) {
+                        EventInfo info = new EventInfo(getMode(), EventInfo.ACTION_TYPE_CONTENT,
+                                EventInfo.ROW_TYPE_LIST, mRowIndex);
+                        mObserver.onSliceAction(info, mRowAction.getSliceItem());
+                    }
+                } catch (CanceledException e) {
+                    Log.e(TAG, "PendingIntent for slice cannot be sent", e);
+                }
             }
-        } else if (mToggles.size() == 1) {
-            // If there is only one toggle and no row action, just toggle it.
-            mToggles.get(0).toggle();
         }
     }
 
@@ -575,16 +553,18 @@
 
     @Override
     public void resetView() {
-        mRootView.setVisibility(View.VISIBLE);
+        mRootView.setVisibility(VISIBLE);
         setViewClickable(mRootView, false);
         setViewClickable(mContent, false);
         mStartContainer.removeAllViews();
         mEndContainer.removeAllViews();
         mPrimaryText.setText(null);
         mSecondaryText.setText(null);
+        mLastUpdatedText.setText(null);
+        mLastUpdatedText.setVisibility(GONE);
         mToggles.clear();
         mRowAction = null;
-        mDivider.setVisibility(View.GONE);
+        mDivider.setVisibility(GONE);
         if (mRangeBar != null) {
             removeView(mRangeBar);
         }
diff --git a/slices/view/src/main/java/androidx/slice/widget/ShortcutView.java b/slices/view/src/main/java/androidx/slice/widget/ShortcutView.java
index 6f02437..2f97c64 100644
--- a/slices/view/src/main/java/androidx/slice/widget/ShortcutView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/ShortcutView.java
@@ -26,7 +26,6 @@
 import static android.app.slice.SliceItem.FORMAT_SLICE;
 import static android.app.slice.SliceItem.FORMAT_TEXT;
 
-import android.annotation.SuppressLint;
 import android.app.PendingIntent;
 import android.app.PendingIntent.CanceledException;
 import android.content.Context;
@@ -43,6 +42,7 @@
 import android.widget.ImageView;
 
 import androidx.annotation.RestrictTo;
+import androidx.core.graphics.drawable.DrawableCompat;
 import androidx.slice.Slice;
 import androidx.slice.SliceItem;
 import androidx.slice.core.SliceQuery;
@@ -56,7 +56,7 @@
 
     private static final String TAG = "ShortcutView";
 
-    private Slice mSlice;
+    private ListContent mListContent;
     private Uri mUri;
     private SliceItem mActionItem;
     private SliceItem mLabel;
@@ -72,21 +72,23 @@
         mLargeIconSize = res.getDimensionPixelSize(R.dimen.abc_slice_shortcut_size);
     }
 
-    @SuppressLint("NewApi") // mIcon can only be non-null on API 23+
     @Override
-    public void setSlice(Slice slice) {
+    public void setSliceContent(ListContent sliceContent) {
         resetView();
-        mSlice = slice;
-        determineShortcutItems(getContext(), slice);
-        SliceItem colorItem = SliceQuery.findSubtype(slice, FORMAT_INT, SUBTYPE_COLOR);
+        mListContent = sliceContent;
+        if (mListContent == null) {
+            return;
+        }
+        determineShortcutItems(getContext());
+        SliceItem colorItem = mListContent.getColorItem();
         if (colorItem == null) {
-            colorItem = SliceQuery.findSubtype(slice, FORMAT_INT, SUBTYPE_COLOR);
+            colorItem = SliceQuery.findSubtype(sliceContent.getSlice(), FORMAT_INT, SUBTYPE_COLOR);
         }
         final int color = colorItem != null
                 ? colorItem.getInt()
                 : SliceViewUtil.getColorAccent(getContext());
-        ShapeDrawable circle = new ShapeDrawable(new OvalShape());
-        circle.setTint(color);
+        Drawable circle = DrawableCompat.wrap(new ShapeDrawable(new OvalShape()));
+        DrawableCompat.setTint(circle, color);
         ImageView iv = new ImageView(getContext());
         if (mIcon != null && !mIcon.hasHint(HINT_NO_TINT)) {
             // Only set the background if we're tintable
@@ -98,7 +100,7 @@
             final int iconSize = isImage ? mLargeIconSize : mSmallIconSize;
             SliceViewUtil.createCircledIcon(getContext(), iconSize, mIcon.getIcon(),
                     isImage, this /* parent */);
-            mUri = slice.getUri();
+            mUri = sliceContent.getSlice().getUri();
             setClickable(true);
         } else {
             setClickable(false);
@@ -112,10 +114,13 @@
 
     @Override
     public boolean performClick() {
+        if (mListContent == null) {
+            return false;
+        }
         if (!callOnClick()) {
             try {
                 if (mActionItem != null) {
-                    mActionItem.getAction().send();
+                    mActionItem.fireAction(null, null);
                 } else {
                     Intent intent = new Intent(Intent.ACTION_VIEW).setData(mUri);
                     intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -127,8 +132,8 @@
                             EventInfo.ROW_TYPE_SHORTCUT, 0 /* rowIndex */);
                     SliceItem interactedItem = mActionItem != null
                             ? mActionItem
-                            : new SliceItem(mSlice, FORMAT_SLICE, null /* subtype */,
-                                    mSlice.getHints());
+                            : new SliceItem(mListContent.getSlice(), FORMAT_SLICE,
+                                    null /* subtype */, mListContent.getSlice().getHints());
                     mObserver.onSliceAction(ei, interactedItem);
                 }
             } catch (CanceledException e) {
@@ -141,9 +146,12 @@
     /**
      * Looks at the slice and determines which items are best to use to compose the shortcut.
      */
-    private void determineShortcutItems(Context context, Slice slice) {
-        ListContent lc = new ListContent(context, slice);
-        SliceItem primaryAction = lc.getPrimaryAction();
+    private void determineShortcutItems(Context context) {
+        if (mListContent == null) {
+            return;
+        }
+        SliceItem primaryAction = mListContent.getPrimaryAction();
+        Slice slice = mListContent.getSlice();
 
         if (primaryAction != null) {
             // Preferred case: slice has a primary action
@@ -204,7 +212,7 @@
 
     @Override
     public void resetView() {
-        mSlice = null;
+        mListContent = null;
         mUri = null;
         mActionItem = null;
         mLabel = null;
diff --git a/slices/view/src/main/java/androidx/slice/widget/SliceActionView.java b/slices/view/src/main/java/androidx/slice/widget/SliceActionView.java
index 54da214..ffe4587 100644
--- a/slices/view/src/main/java/androidx/slice/widget/SliceActionView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/SliceActionView.java
@@ -168,14 +168,14 @@
                 // Update the intent extra state
                 boolean isChecked = ((Checkable) mActionView).isChecked();
                 Intent i = new Intent().putExtra(EXTRA_TOGGLE_STATE, isChecked);
-                pi.send(getContext(), 0, i, null, null);
+                mSliceAction.getActionItem().fireAction(getContext(), i);
 
                 // Update event info state
                 if (mEventInfo != null) {
                     mEventInfo.state = isChecked ? EventInfo.STATE_ON : EventInfo.STATE_OFF;
                 }
             } else {
-                pi.send();
+                mSliceAction.getActionItem().fireAction(null, null); 
             }
             if (mObserver != null && mEventInfo != null) {
                 mObserver.onSliceAction(mEventInfo, mSliceAction.getSliceItem());
diff --git a/slices/view/src/main/java/androidx/slice/widget/SliceChildView.java b/slices/view/src/main/java/androidx/slice/widget/SliceChildView.java
index 2400e73..fcd71bb 100644
--- a/slices/view/src/main/java/androidx/slice/widget/SliceChildView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/SliceChildView.java
@@ -19,13 +19,11 @@
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.util.AttributeSet;
-import android.view.View;
 import android.widget.FrameLayout;
 
 import androidx.annotation.ColorInt;
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
-import androidx.slice.Slice;
 import androidx.slice.SliceItem;
 import androidx.slice.view.R;
 
@@ -45,10 +43,15 @@
     protected int mSubtitleColor;
     protected int mHeaderTitleSize;
     protected int mHeaderSubtitleSize;
+    protected int mVerticalHeaderTextPadding;
     protected int mTitleSize;
     protected int mSubtitleSize;
+    protected int mVerticalTextPadding;
     protected int mGridTitleSize;
     protected int mGridSubtitleSize;
+    protected int mVerticalGridTextPadding;
+    protected int mGridTopPadding;
+    protected int mGridBottomPadding;
     protected boolean mShowLastUpdated;
     protected long mLastUpdated = -1;
 
@@ -60,6 +63,46 @@
         this(context);
     }
 
+    /**
+     * Called when the view should be reset.
+     */
+    public abstract void resetView();
+
+    /**
+     * Sets the content to display in this slice.
+     */
+    public void setSliceContent(ListContent content) {
+        // Do nothing
+    }
+
+    /**
+     * Called when the slice being displayed in this view is an element of a larger list.
+     */
+    public void setSliceItem(SliceItem slice, boolean isHeader, int rowIndex,
+            int rowCount, SliceView.OnSliceActionListener observer) {
+        // Do nothing
+    }
+
+    /**
+     * Sets the slice actions for this view.
+     */
+    public void setSliceActions(List<SliceItem> actions) {
+        // Do nothing
+    }
+
+    /**
+     * @return the height of the view when displayed in {@link SliceView#MODE_SMALL}.
+     */
+    public int getSmallHeight() {
+        return 0;
+    }
+
+    /**
+     * @return the height of the view when displayed in {@link SliceView#MODE_LARGE}.
+     */
+    public int getActualHeight() {
+        return 0;
+    }
 
     /**
      * Set the mode of the slice being presented.
@@ -77,37 +120,6 @@
     }
 
     /**
-     * @return the height of this view when displayed in {@link SliceView#MODE_SMALL}.
-     */
-    public int getSmallHeight() {
-        return 0;
-    }
-
-    /**
-     * @return the height of this view if it displayed all of its contents.
-     */
-    public int getActualHeight() {
-        return 0;
-    }
-
-    /**
-     * @param slice the slice to show in this view.
-     */
-    public abstract void setSlice(Slice slice);
-
-    /**
-     * Called when the view should be reset.
-     */
-    public abstract void resetView();
-
-    /**
-     * @return the view.
-     */
-    public View getView() {
-        return this;
-    }
-
-    /**
      * Sets a custom color to use for tinting elements like icons for this view.
      */
     public void setTint(@ColorInt int tintColor) {
@@ -146,33 +158,31 @@
             mTintColor = themeColor != -1 ? themeColor : mTintColor;
             mTitleColor = a.getColor(R.styleable.SliceView_titleColor, 0);
             mSubtitleColor = a.getColor(R.styleable.SliceView_subtitleColor, 0);
+
             mHeaderTitleSize = (int) a.getDimension(
                     R.styleable.SliceView_headerTitleSize, 0);
             mHeaderSubtitleSize = (int) a.getDimension(
                     R.styleable.SliceView_headerSubtitleSize, 0);
+            mVerticalHeaderTextPadding = (int) a.getDimension(
+                    R.styleable.SliceView_headerTextVerticalPadding, 0);
+
             mTitleSize = (int) a.getDimension(R.styleable.SliceView_titleSize, 0);
             mSubtitleSize = (int) a.getDimension(
                     R.styleable.SliceView_subtitleSize, 0);
+            mVerticalTextPadding = (int) a.getDimension(
+                    R.styleable.SliceView_textVerticalPadding, 0);
+
             mGridTitleSize = (int) a.getDimension(R.styleable.SliceView_gridTitleSize, 0);
             mGridSubtitleSize = (int) a.getDimension(
                     R.styleable.SliceView_gridSubtitleSize, 0);
+            int defaultVerticalGridPadding = getContext().getResources().getDimensionPixelSize(
+                    R.dimen.abc_slice_grid_text_inner_padding);
+            mVerticalGridTextPadding = (int) a.getDimension(
+                    R.styleable.SliceView_gridTextVerticalPadding, defaultVerticalGridPadding);
+            mGridTopPadding = (int) a.getDimension(R.styleable.SliceView_gridTopPadding, 0);
+            mGridBottomPadding = (int) a.getDimension(R.styleable.SliceView_gridTopPadding, 0);
         } finally {
             a.recycle();
         }
     }
-
-    /**
-     * Called when the slice being displayed in this view is an element of a larger list.
-     */
-    public void setSliceItem(SliceItem slice, boolean isHeader, int rowIndex,
-            SliceView.OnSliceActionListener observer) {
-        // Do nothing
-    }
-
-    /**
-     * Sets the slice actions for this view.
-     */
-    public void setSliceActions(List<SliceItem> actions) {
-        // Do nothing
-    }
 }
diff --git a/slices/view/src/main/java/androidx/slice/widget/SliceLiveData.java b/slices/view/src/main/java/androidx/slice/widget/SliceLiveData.java
index 2ffc02c..8639893 100644
--- a/slices/view/src/main/java/androidx/slice/widget/SliceLiveData.java
+++ b/slices/view/src/main/java/androidx/slice/widget/SliceLiveData.java
@@ -63,8 +63,7 @@
 
     /**
      * Produces an {@link LiveData} that tracks a Slice for a given Uri. To use
-     * this method your app must have the permission to the slice Uri or hold
-     * {@link android.Manifest.permission#BIND_SLICE}).
+     * this method your app must have the permission to the slice Uri.
      */
     public static LiveData<Slice> fromUri(Context context, Uri uri) {
         return new SliceLiveDataImpl(context.getApplicationContext(), uri);
@@ -72,8 +71,7 @@
 
     /**
      * Produces an {@link LiveData} that tracks a Slice for a given Intent. To use
-     * this method your app must have the permission to the slice Uri or hold
-     * {@link android.Manifest.permission#BIND_SLICE}).
+     * this method your app must have the permission to the slice Uri.
      */
     public static LiveData<Slice> fromIntent(@NonNull Context context, @NonNull Intent intent) {
         return new SliceLiveDataImpl(context.getApplicationContext(), intent);
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 855bded..7ba9eeb 100644
--- a/slices/view/src/main/java/androidx/slice/widget/SliceView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/SliceView.java
@@ -32,6 +32,7 @@
 import android.view.ViewConfiguration;
 import android.view.ViewGroup;
 
+import androidx.annotation.ColorInt;
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -312,7 +313,7 @@
     private int getHeightForMode() {
         int mode = getMode();
         if (mode == MODE_SHORTCUT) {
-            return mShortcutSize;
+            return mListContent != null && mListContent.isValid() ? mShortcutSize : 0;
         }
         return mode == MODE_LARGE
                 ? mCurrentView.getActualHeight()
@@ -375,7 +376,7 @@
 
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        View v = mCurrentView.getView();
+        View v = mCurrentView;
         final int left = getPaddingLeft();
         final int top = getPaddingTop();
         v.layout(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight());
@@ -484,15 +485,23 @@
     }
 
     /**
+     * @deprecated TO BE REMOVED; use {@link #setAccentColor(int)} instead.
+     */
+    @Deprecated
+    public void setTint(int tintColor) {
+        setAccentColor(tintColor);
+    }
+
+    /**
      * Contents of a slice such as icons, text, and controls (e.g. toggle) can be tinted. Normally
      * a color for tinting will be provided by the slice. Using this method will override
-     * this color information and instead tint elements with the provided color.
+     * the slice-provided color information and instead tint elements with the color set here.
      *
-     * @param tintColor the color to use for tinting contents of this view.
+     * @param accentColor the color to use for tinting contents of this view.
      */
-    public void setTint(int tintColor) {
-        mThemeTintColor = tintColor;
-        mCurrentView.setTint(tintColor);
+    public void setAccentColor(@ColorInt int accentColor) {
+        mThemeTintColor = accentColor;
+        mCurrentView.setTint(accentColor);
     }
 
     /**
@@ -540,11 +549,14 @@
     private void reinflate() {
         if (mCurrentSlice == null) {
             mCurrentView.resetView();
+            updateActions();
             return;
         }
-        mListContent = new ListContent(getContext(), mCurrentSlice);
+        mListContent = new ListContent(getContext(), mCurrentSlice, mAttrs, mDefStyleAttr,
+                mDefStyleRes);
         if (!mListContent.isValid()) {
             mCurrentView.resetView();
+            updateActions();
             return;
         }
 
@@ -579,7 +591,7 @@
         mCurrentView.setShowLastUpdated(mShowLastUpdated && expired);
 
         // Set the slice
-        mCurrentView.setSlice(mCurrentSlice);
+        mCurrentView.setSliceContent(mListContent);
         updateActions();
     }
 
diff --git a/slices/view/src/main/res-public/values/public_attrs.xml b/slices/view/src/main/res-public/values/public_attrs.xml
index ad909ea..a535a6d 100644
--- a/slices/view/src/main/res-public/values/public_attrs.xml
+++ b/slices/view/src/main/res-public/values/public_attrs.xml
@@ -22,9 +22,14 @@
     <public type="attr" name="tintColor" />
     <public type="attr" name="headerTitleSize" />
     <public type="attr" name="headerSubtitleSize" />
+    <public type="attr" name="headerTextVerticalPadding" />
     <public type="attr" name="titleSize" />
     <public type="attr" name="subtitleSize" />
+    <public type="attr" name="textVerticalPadding" />
     <public type="attr" name="gridTitleSize" />
     <public type="attr" name="gridSubtitleSize" />
+    <public type="attr" name="gridTextVerticalPadding" />
+    <public type="attr" name="gridTopPadding" />
+    <public type="attr" name="gridBottomPadding" />
     <public type="attr" name="sliceViewStyle" />
-</resources>
\ No newline at end of file
+</resources>
diff --git a/slices/view/src/main/res/layout/abc_slice_grid_see_more.xml b/slices/view/src/main/res/layout/abc_slice_grid_see_more.xml
index 17c1c0f..0a2e746 100644
--- a/slices/view/src/main/res/layout/abc_slice_grid_see_more.xml
+++ b/slices/view/src/main/res/layout/abc_slice_grid_see_more.xml
@@ -33,5 +33,6 @@
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:gravity="center_horizontal"
+              android:paddingTop="@dimen/abc_slice_grid_text_padding"
               android:text="@string/abc_slice_more"/>
 </LinearLayout>
\ No newline at end of file
diff --git a/slices/view/src/main/res/layout/abc_slice_secondary_text.xml b/slices/view/src/main/res/layout/abc_slice_secondary_text.xml
index 0870465..c6ff594 100644
--- a/slices/view/src/main/res/layout/abc_slice_secondary_text.xml
+++ b/slices/view/src/main/res/layout/abc_slice_secondary_text.xml
@@ -15,14 +15,11 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<!-- LinearLayout -->
 <TextView
         xmlns:android="http://schemas.android.com/apk/res/android"
-        android:textAppearance="?android:attr/textAppearanceSmall"
-        android:textColor="?android:attr/textColorSecondary"
         android:gravity="center"
         android:layout_height="wrap_content"
-        android:padding="4dp"
         android:layout_width="match_parent"
         android:maxLines="1"
+        android:singleLine="true"
         android:ellipsize="end"/>
diff --git a/slices/view/src/main/res/layout/abc_slice_title.xml b/slices/view/src/main/res/layout/abc_slice_title.xml
index 70a1400..eac5e8c 100644
--- a/slices/view/src/main/res/layout/abc_slice_title.xml
+++ b/slices/view/src/main/res/layout/abc_slice_title.xml
@@ -18,11 +18,9 @@
 
 <TextView
         xmlns:android="http://schemas.android.com/apk/res/android"
-        android:textAppearance="?android:attr/textAppearanceMedium"
-        android:textColor="?android:attr/textColorPrimary"
         android:gravity="center"
         android:layout_height="wrap_content"
-        android:padding="4dp"
         android:layout_width="match_parent"
         android:maxLines="1"
+        android:singleLine="true"
         android:ellipsize="end"/>
diff --git a/slices/view/src/main/res/values-af/strings.xml b/slices/view/src/main/res/values-af/strings.xml
index ea5ab25..cfe8138 100644
--- a/slices/view/src/main/res/values-af/strings.xml
+++ b/slices/view/src/main/res/values-af/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Meer"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Wys meer"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Opgedateer om <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> min. gelede</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> min. gelede</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> jaar gelede</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> jaar gelede</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> dae gelede</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> dag gelede</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Kon nie koppel nie"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-am/strings.xml b/slices/view/src/main/res/values-am/strings.xml
index ea5ab25..da542e1 100644
--- a/slices/view/src/main/res/values-am/strings.xml
+++ b/slices/view/src/main/res/values-am/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"ተጨማሪ"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"ተጨማሪ አሳይ"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"የተዘመነው <xliff:g id="TIME">%1$s</xliff:g> ላይ"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> ደቂቃ በፊት</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ደቂቃ በፊት</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> ዓመት በፊት</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ዓመት በፊት</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> ቀናት በፊት</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ቀናት በፊት</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"መገናኘት አልተቻለም"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-ar/strings.xml b/slices/view/src/main/res/values-ar/strings.xml
index 21aa8a4..1e9ca94 100644
--- a/slices/view/src/main/res/values-ar/strings.xml
+++ b/slices/view/src/main/res/values-ar/strings.xml
@@ -18,4 +18,32 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"بالإضافة إلى <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"المزيد"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"عرض المزيد"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"وقت التحديث الأخير: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="zero">قبل <xliff:g id="ID_2">%d</xliff:g> دقيقة</item>
+      <item quantity="two">قبل دقيقتين (<xliff:g id="ID_2">%d</xliff:g>)</item>
+      <item quantity="few">قبل <xliff:g id="ID_2">%d</xliff:g> دقائق</item>
+      <item quantity="many">قبل <xliff:g id="ID_2">%d</xliff:g> دقيقة</item>
+      <item quantity="other">قبل <xliff:g id="ID_2">%d</xliff:g> دقيقة</item>
+      <item quantity="one">قبل <xliff:g id="ID_1">%d</xliff:g> دقيقة</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="zero">قبل <xliff:g id="ID_2">%d</xliff:g> سنة</item>
+      <item quantity="two">قبل سنتين (<xliff:g id="ID_2">%d</xliff:g>)</item>
+      <item quantity="few">قبل <xliff:g id="ID_2">%d</xliff:g> سنوات</item>
+      <item quantity="many">قبل <xliff:g id="ID_2">%d</xliff:g> سنة</item>
+      <item quantity="other">قبل <xliff:g id="ID_2">%d</xliff:g> سنة</item>
+      <item quantity="one">قبل <xliff:g id="ID_1">%d</xliff:g> سنة</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="zero">قبل <xliff:g id="ID_2">%d</xliff:g> يوم</item>
+      <item quantity="two">قبل يومين (<xliff:g id="ID_2">%d</xliff:g>)</item>
+      <item quantity="few">قبل <xliff:g id="ID_2">%d</xliff:g> أيام</item>
+      <item quantity="many">قبل <xliff:g id="ID_2">%d</xliff:g> يومًا</item>
+      <item quantity="other">قبل <xliff:g id="ID_2">%d</xliff:g> يوم</item>
+      <item quantity="one">قبل <xliff:g id="ID_1">%d</xliff:g> يوم</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"تعذّر الاتصال."</string>
 </resources>
diff --git a/slices/view/src/main/res/values-as/strings.xml b/slices/view/src/main/res/values-as/strings.xml
new file mode 100644
index 0000000..77f9af6
--- /dev/null
+++ b/slices/view/src/main/res/values-as/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ Copyright 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"অধিক"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"অধিক দেখুৱাওক"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> আপডেট কৰা হৈছিল"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"সংযোগ কৰিব পৰা নগ\'ল"</string>
+</resources>
diff --git a/slices/view/src/main/res/values-az/strings.xml b/slices/view/src/main/res/values-az/strings.xml
index ea5ab25..837f08c 100644
--- a/slices/view/src/main/res/values-az/strings.xml
+++ b/slices/view/src/main/res/values-az/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Digər"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Digərinə baxın"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> tarixində yenilənib"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> dəq əvvəl</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> dəq əvvəl</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> il əvvəl</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> il əvvəl</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> gün əvvəl</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> gün əvvəl</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Qoşulmaq mümkün olmadı"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-b+sr+Latn/strings.xml b/slices/view/src/main/res/values-b+sr+Latn/strings.xml
index ffd9b9b..8deb17c 100644
--- a/slices/view/src/main/res/values-b+sr+Latn/strings.xml
+++ b/slices/view/src/main/res/values-b+sr+Latn/strings.xml
@@ -18,4 +18,23 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"i još <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Još"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Prikaži više"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Ažurirano <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="one">pre <xliff:g id="ID_2">%d</xliff:g> min</item>
+      <item quantity="few">pre <xliff:g id="ID_2">%d</xliff:g> min</item>
+      <item quantity="other">pre <xliff:g id="ID_2">%d</xliff:g> min</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="one">pre <xliff:g id="ID_2">%d</xliff:g> god</item>
+      <item quantity="few">pre <xliff:g id="ID_2">%d</xliff:g> god</item>
+      <item quantity="other">pre <xliff:g id="ID_2">%d</xliff:g> god</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="one">pre <xliff:g id="ID_2">%d</xliff:g> dan</item>
+      <item quantity="few">pre <xliff:g id="ID_2">%d</xliff:g> dana</item>
+      <item quantity="other">pre <xliff:g id="ID_2">%d</xliff:g> dana</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Povezivanje nije uspelo"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-be/strings.xml b/slices/view/src/main/res/values-be/strings.xml
index df8e965..f1ae045 100644
--- a/slices/view/src/main/res/values-be/strings.xml
+++ b/slices/view/src/main/res/values-be/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"Яшчэ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Яшчэ"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Яшчэ"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Абноўлена <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Не атрымалася падключыцца"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-bg/strings.xml b/slices/view/src/main/res/values-bg/strings.xml
index ea5ab25..b019a46 100644
--- a/slices/view/src/main/res/values-bg/strings.xml
+++ b/slices/view/src/main/res/values-bg/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Още"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Показване на още"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Актуализирано <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other">Преди <xliff:g id="ID_2">%d</xliff:g> мин</item>
+      <item quantity="one">Преди <xliff:g id="ID_1">%d</xliff:g> мин</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other">Преди <xliff:g id="ID_2">%d</xliff:g> год</item>
+      <item quantity="one">Преди <xliff:g id="ID_1">%d</xliff:g> год</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other">Преди <xliff:g id="ID_2">%d</xliff:g> дни</item>
+      <item quantity="one">Преди <xliff:g id="ID_1">%d</xliff:g> ден</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Не можа да се установи връзка"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-bn/strings.xml b/slices/view/src/main/res/values-bn/strings.xml
index 8737d8c..3b8acdf 100644
--- a/slices/view/src/main/res/values-bn/strings.xml
+++ b/slices/view/src/main/res/values-bn/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>টি"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"আরও"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"আরও দেখুন"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> আপডেট করা হয়েছে"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"কানেক্ট করা যায়নি"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-bs/strings.xml b/slices/view/src/main/res/values-bs/strings.xml
index ea5ab25..cb84c18 100644
--- a/slices/view/src/main/res/values-bs/strings.xml
+++ b/slices/view/src/main/res/values-bs/strings.xml
@@ -18,4 +18,23 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Više"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Prikaži više"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Ažurirano <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="one">Prije <xliff:g id="ID_2">%d</xliff:g> min.</item>
+      <item quantity="few">Prije <xliff:g id="ID_2">%d</xliff:g> min.</item>
+      <item quantity="other">Prije <xliff:g id="ID_2">%d</xliff:g> min.</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="one">Prije <xliff:g id="ID_2">%d</xliff:g> god.</item>
+      <item quantity="few">Prije <xliff:g id="ID_2">%d</xliff:g> god.</item>
+      <item quantity="other">Prije <xliff:g id="ID_2">%d</xliff:g> god.</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="one">Prije <xliff:g id="ID_2">%d</xliff:g> dan</item>
+      <item quantity="few">Prije <xliff:g id="ID_2">%d</xliff:g> dana</item>
+      <item quantity="other">Prije <xliff:g id="ID_2">%d</xliff:g> dana</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Povezivanje nije uspjelo"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-ca/strings.xml b/slices/view/src/main/res/values-ca/strings.xml
index 8ae6f78..52e1988 100644
--- a/slices/view/src/main/res/values-ca/strings.xml
+++ b/slices/view/src/main/res/values-ca/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"<xliff:g id="NUMBER">%1$d</xliff:g> més"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Més"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Mostra\'n més"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"S\'ha actualitzat <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"No s\'ha pogut connectar"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-cs/strings.xml b/slices/view/src/main/res/values-cs/strings.xml
index 1d39c0f..c4c1fce 100644
--- a/slices/view/src/main/res/values-cs/strings.xml
+++ b/slices/view/src/main/res/values-cs/strings.xml
@@ -18,4 +18,26 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"a ještě <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Více"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Zobrazit více"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Aktualizováno <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="few">před <xliff:g id="ID_2">%d</xliff:g> min</item>
+      <item quantity="many">před <xliff:g id="ID_2">%d</xliff:g> min</item>
+      <item quantity="other">před <xliff:g id="ID_2">%d</xliff:g> min</item>
+      <item quantity="one">před <xliff:g id="ID_1">%d</xliff:g> min</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="few">před <xliff:g id="ID_2">%d</xliff:g> lety</item>
+      <item quantity="many">před <xliff:g id="ID_2">%d</xliff:g> roku</item>
+      <item quantity="other">před <xliff:g id="ID_2">%d</xliff:g> lety</item>
+      <item quantity="one">před <xliff:g id="ID_1">%d</xliff:g> rokem</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="few">před <xliff:g id="ID_2">%d</xliff:g> dny</item>
+      <item quantity="many">před <xliff:g id="ID_2">%d</xliff:g> dne</item>
+      <item quantity="other">před <xliff:g id="ID_2">%d</xliff:g> dny</item>
+      <item quantity="one">před <xliff:g id="ID_1">%d</xliff:g> dnem</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Nelze se připojit"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-da/strings.xml b/slices/view/src/main/res/values-da/strings.xml
index 40de41d..33d9c9d 100644
--- a/slices/view/src/main/res/values-da/strings.xml
+++ b/slices/view/src/main/res/values-da/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"<xliff:g id="NUMBER">%1$d</xliff:g> mere"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Mere"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Se mere"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Opdateret <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Der kunne ikke oprettes forbindelse"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-de/strings.xml b/slices/view/src/main/res/values-de/strings.xml
index 9d9bede..96da7eb 100644
--- a/slices/view/src/main/res/values-de/strings.xml
+++ b/slices/view/src/main/res/values-de/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Mehr"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Mehr anzeigen"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Aktualisiert: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Verbindung nicht möglich"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-el/strings.xml b/slices/view/src/main/res/values-el/strings.xml
index ea5ab25..31092af 100644
--- a/slices/view/src/main/res/values-el/strings.xml
+++ b/slices/view/src/main/res/values-el/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Περισσότ."</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Εμφάνιση περισσότερων"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Ενημερώθηκε <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> λεπ. πριν</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> λεπ. πριν</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> χρ. πριν</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> χρ. πριν</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ημ. πριν</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> ημ. πριν</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Αδυναμία σύνδεσης"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-en-rAU/strings.xml b/slices/view/src/main/res/values-en-rAU/strings.xml
index ea5ab25..8fa5b45 100644
--- a/slices/view/src/main/res/values-en-rAU/strings.xml
+++ b/slices/view/src/main/res/values-en-rAU/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"More"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Show more"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Updated <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> min ago</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> min ago</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> yr ago</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> yr ago</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> days ago</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> day ago</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Couldn\'t connect"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-en-rCA/strings.xml b/slices/view/src/main/res/values-en-rCA/strings.xml
index ea5ab25..8fa5b45 100644
--- a/slices/view/src/main/res/values-en-rCA/strings.xml
+++ b/slices/view/src/main/res/values-en-rCA/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"More"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Show more"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Updated <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> min ago</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> min ago</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> yr ago</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> yr ago</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> days ago</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> day ago</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Couldn\'t connect"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-en-rGB/strings.xml b/slices/view/src/main/res/values-en-rGB/strings.xml
index ea5ab25..8fa5b45 100644
--- a/slices/view/src/main/res/values-en-rGB/strings.xml
+++ b/slices/view/src/main/res/values-en-rGB/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"More"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Show more"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Updated <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> min ago</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> min ago</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> yr ago</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> yr ago</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> days ago</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> day ago</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Couldn\'t connect"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-en-rIN/strings.xml b/slices/view/src/main/res/values-en-rIN/strings.xml
index ea5ab25..8fa5b45 100644
--- a/slices/view/src/main/res/values-en-rIN/strings.xml
+++ b/slices/view/src/main/res/values-en-rIN/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"More"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Show more"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Updated <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> min ago</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> min ago</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> yr ago</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> yr ago</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> days ago</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> day ago</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Couldn\'t connect"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-en-rXC/strings.xml b/slices/view/src/main/res/values-en-rXC/strings.xml
index b793412..b24c1e6 100644
--- a/slices/view/src/main/res/values-en-rXC/strings.xml
+++ b/slices/view/src/main/res/values-en-rXC/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‎‏‏‏‎‎‏‎‎‏‏‏‏‎‏‏‏‎‎‏‏‎‏‏‏‏‏‎‎‎‏‏‏‏‎‎‏‎‎‏‎‏‏‎‏‏‏‏‎‏‎‏‏‎‏‏‎‎+ ‎‏‎‎‏‏‎<xliff:g id="NUMBER">%1$d</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎‏‏‏‎‎‎‎‏‏‏‎‎‎‎‎‏‎‏‏‎‎‎‎‏‎‎‎‎‏‎‎‏‎‏‎‎‏‏‏‏‎‏‏‏‏‎‏‏‏‏‏‏‏‏‎‏‎‏‎More‎‏‎‎‏‎"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‎‏‏‏‎‎‎‎‎‏‏‎‏‎‎‏‏‎‎‏‎‏‎‎‎‏‎‏‏‏‎‎‎‎‎‎‎‏‎‏‎‏‏‏‏‏‎‏‏‏‎‎‏‎‎‎‎‎‎Show more‎‏‎‎‏‎"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‎‏‎‎‏‎‏‏‎‎‏‎‏‎‏‏‏‎‏‏‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‎‏‎‎‎‎‎‏‏‎‎‏‎‎‏‏‏‎‏‏‎‎‎‎Updated ‎‏‎‎‏‏‎<xliff:g id="TIME">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‏‎‎‎‏‏‏‏‏‎‏‏‎‏‏‏‏‏‎‎‏‏‎‎‎‎‎‎‏‏‎‏‏‎‎‏‎‏‎‏‎‎‏‏‎‎‏‎‎‏‏‎<xliff:g id="ID_2">%d</xliff:g>‎‏‎‎‏‏‏‎ min ago‎‏‎‎‏‎</item>
+      <item quantity="one">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‏‎‎‎‏‏‏‏‏‎‏‏‎‏‏‏‏‏‎‎‏‏‎‎‎‎‎‎‏‏‎‏‏‎‎‏‎‏‎‏‎‎‏‏‎‎‏‎‎‏‏‎<xliff:g id="ID_1">%d</xliff:g>‎‏‎‎‏‏‏‎ min ago‎‏‎‎‏‎</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‎‏‎‎‏‎‎‏‏‏‎‎‎‏‎‎‏‎‏‎‎‏‏‏‏‎‎‏‏‏‏‏‏‏‎‏‎‏‎‎‏‎‏‎‎‏‎‎‏‏‎<xliff:g id="ID_2">%d</xliff:g>‎‏‎‎‏‏‏‎ yr ago‎‏‎‎‏‎</item>
+      <item quantity="one">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‎‏‎‎‏‎‎‏‏‏‎‎‎‏‎‎‏‎‏‎‎‏‏‏‏‎‎‏‏‏‏‏‏‏‎‏‎‏‎‎‏‎‏‎‎‏‎‎‏‏‎<xliff:g id="ID_1">%d</xliff:g>‎‏‎‎‏‏‏‎ yr ago‎‏‎‎‏‎</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‎‎‏‏‏‏‎‏‏‏‏‎‏‏‏‏‎‏‏‏‎‎‏‏‏‎‏‏‎‎‏‎‎‏‏‎‎‏‏‎‎‏‏‎‎‎‏‎‎‎‎‎‏‏‎‎‎‏‎‎‏‏‎<xliff:g id="ID_2">%d</xliff:g>‎‏‎‎‏‏‏‎ days ago‎‏‎‎‏‎</item>
+      <item quantity="one">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‎‎‏‏‏‏‎‏‏‏‏‎‏‏‏‏‎‏‏‏‎‎‏‏‏‎‏‏‎‎‏‎‎‏‏‎‎‏‏‎‎‏‏‎‎‎‏‎‎‎‎‎‏‏‎‎‎‏‎‎‏‏‎<xliff:g id="ID_1">%d</xliff:g>‎‏‎‎‏‏‏‎ day ago‎‏‎‎‏‎</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‏‎‎‎‏‎‎‎‎‎‎‎‎‏‎‏‎‏‏‎‎‏‏‏‏‎‎‏‎‎‎‎‎‏‏‎‎‏‏‎‎‏‎‏‎‏‏‏‎‏‏‎‏‎‏‎‎‎‎Couldn\'t connect‎‏‎‎‏‎"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-es-rUS/strings.xml b/slices/view/src/main/res/values-es-rUS/strings.xml
index a445505..124c40c 100644
--- a/slices/view/src/main/res/values-es-rUS/strings.xml
+++ b/slices/view/src/main/res/values-es-rUS/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"<xliff:g id="NUMBER">%1$d</xliff:g> más"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Más"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Mostrar más"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Última actualización: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"No se pudo establecer conexión"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-es/strings.xml b/slices/view/src/main/res/values-es/strings.xml
index dcdd33e..57685ba 100644
--- a/slices/view/src/main/res/values-es/strings.xml
+++ b/slices/view/src/main/res/values-es/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"<xliff:g id="NUMBER">%1$d</xliff:g> más"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Más"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Ver más"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Última actualización: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"No se ha podido establecer la conexión"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-et/strings.xml b/slices/view/src/main/res/values-et/strings.xml
index a1675ff..4bd8a92 100644
--- a/slices/view/src/main/res/values-et/strings.xml
+++ b/slices/view/src/main/res/values-et/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"ja veel <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Rohkem"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Kuva rohkem"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Värskendatud kell <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Ühendamine ebaõnnestus"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-eu/strings.xml b/slices/view/src/main/res/values-eu/strings.xml
index 1c79d4a..e689ada 100644
--- a/slices/view/src/main/res/values-eu/strings.xml
+++ b/slices/view/src/main/res/values-eu/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"Beste <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Gehiago"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Erakutsi gehiago"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Azken eguneratzea: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Ezin izan da konektatu"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-fa/strings.xml b/slices/view/src/main/res/values-fa/strings.xml
index 2b2abd2..cb94e5d 100644
--- a/slices/view/src/main/res/values-fa/strings.xml
+++ b/slices/view/src/main/res/values-fa/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"‎+ <xliff:g id="NUMBER">%1$d</xliff:g>‎"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"بیشتر"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"نمایش موارد بیشتر"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"زمان به‌روزرسانی <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> دقیقه قبل</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> دقیقه قبل</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> سال قبل</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> سال قبل</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> روز قبل</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> روز قبل</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"متصل نشد"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-fi/strings.xml b/slices/view/src/main/res/values-fi/strings.xml
index ea5ab25..bc37611 100644
--- a/slices/view/src/main/res/values-fi/strings.xml
+++ b/slices/view/src/main/res/values-fi/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Lisää"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Näytä lisää"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Päivitetty <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Ei yhteyttä"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-fr-rCA/strings.xml b/slices/view/src/main/res/values-fr-rCA/strings.xml
index ea5ab25..1563136 100644
--- a/slices/view/src/main/res/values-fr-rCA/strings.xml
+++ b/slices/view/src/main/res/values-fr-rCA/strings.xml
@@ -17,5 +17,12 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Plus"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Plus"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Mise à jour : <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Impossible de se connecter"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-fr/strings.xml b/slices/view/src/main/res/values-fr/strings.xml
index 73685ce..6762544 100644
--- a/slices/view/src/main/res/values-fr/strings.xml
+++ b/slices/view/src/main/res/values-fr/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"<xliff:g id="NUMBER">%1$d</xliff:g> autres"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Plus"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Afficher plus"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Dernière mise à jour : <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Impossible de se connecter"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-gl/strings.xml b/slices/view/src/main/res/values-gl/strings.xml
index 573eae9..70b31bb 100644
--- a/slices/view/src/main/res/values-gl/strings.xml
+++ b/slices/view/src/main/res/values-gl/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"<xliff:g id="NUMBER">%1$d</xliff:g> máis"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Máis"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Amosar máis"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Contido actualizado (<xliff:g id="TIME">%1$s</xliff:g>)"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other">Hai <xliff:g id="ID_2">%d</xliff:g> min</item>
+      <item quantity="one">Hai <xliff:g id="ID_1">%d</xliff:g> min</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other">Hai <xliff:g id="ID_2">%d</xliff:g> anos</item>
+      <item quantity="one">Hai <xliff:g id="ID_1">%d</xliff:g> ano</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other">Hai <xliff:g id="ID_2">%d</xliff:g> días</item>
+      <item quantity="one">Hai <xliff:g id="ID_1">%d</xliff:g> día</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Non se puido establecer conexión"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-gu/strings.xml b/slices/view/src/main/res/values-gu/strings.xml
index ea5ab25..d6a7ee8 100644
--- a/slices/view/src/main/res/values-gu/strings.xml
+++ b/slices/view/src/main/res/values-gu/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"વધુ"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"વધુ બતાવો"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> અપડેટ થયું"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"કનેક્ટ કરી શકાયું નથી"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-hi/strings.xml b/slices/view/src/main/res/values-hi/strings.xml
index ea5ab25..380025e 100644
--- a/slices/view/src/main/res/values-hi/strings.xml
+++ b/slices/view/src/main/res/values-hi/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"ज़्यादा देखें"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"ज़्यादा देखें"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> बजे अपडेट किया गया"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"कनेक्‍ट नहीं हो पाया"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-hr/strings.xml b/slices/view/src/main/res/values-hr/strings.xml
index 7ecedf6..f44b06c 100644
--- a/slices/view/src/main/res/values-hr/strings.xml
+++ b/slices/view/src/main/res/values-hr/strings.xml
@@ -18,4 +18,23 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"još <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Više"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Prikaži više"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Ažurirano <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="one">Prije <xliff:g id="ID_2">%d</xliff:g> min</item>
+      <item quantity="few">Prije <xliff:g id="ID_2">%d</xliff:g> min</item>
+      <item quantity="other">Prije <xliff:g id="ID_2">%d</xliff:g> min</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="one">Prije <xliff:g id="ID_2">%d</xliff:g> godinu</item>
+      <item quantity="few">Prije <xliff:g id="ID_2">%d</xliff:g> godine</item>
+      <item quantity="other">Prije <xliff:g id="ID_2">%d</xliff:g> godina</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="one">Prije <xliff:g id="ID_2">%d</xliff:g> dan</item>
+      <item quantity="few">Prije <xliff:g id="ID_2">%d</xliff:g> dana</item>
+      <item quantity="other">Prije <xliff:g id="ID_2">%d</xliff:g> dana</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Povezivanje nije moguće"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-hu/strings.xml b/slices/view/src/main/res/values-hu/strings.xml
index ea5ab25..c56143b 100644
--- a/slices/view/src/main/res/values-hu/strings.xml
+++ b/slices/view/src/main/res/values-hu/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Több"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Több megjelenítése"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Frissítve: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Nem sikerült kapcsolódni"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-hy/strings.xml b/slices/view/src/main/res/values-hy/strings.xml
index ea5ab25..bb6b812 100644
--- a/slices/view/src/main/res/values-hy/strings.xml
+++ b/slices/view/src/main/res/values-hy/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Ավելին"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Ցուցադրել ավելի շատ"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Թարմացվել է <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Չհաջողվեց միանալ"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-in/strings.xml b/slices/view/src/main/res/values-in/strings.xml
index ea5ab25..5641ebb 100644
--- a/slices/view/src/main/res/values-in/strings.xml
+++ b/slices/view/src/main/res/values-in/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Lainnya"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Tampilkan lainnya"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Diupdate <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> menit lalu</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> menit lalu</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> tahun lalu</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> tahun lalu</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> hari lalu</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> hari lalu</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Tidak dapat terhubung"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-is/strings.xml b/slices/view/src/main/res/values-is/strings.xml
index ea5ab25..c059c03 100644
--- a/slices/view/src/main/res/values-is/strings.xml
+++ b/slices/view/src/main/res/values-is/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Meira"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Sýna meira"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Uppfært <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Tenging mistókst"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-it/strings.xml b/slices/view/src/main/res/values-it/strings.xml
index ea5ab25..05410e8 100644
--- a/slices/view/src/main/res/values-it/strings.xml
+++ b/slices/view/src/main/res/values-it/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Altro"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Mostra altro"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Aggiornamento: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Impossibile collegarsi"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-iw/strings.xml b/slices/view/src/main/res/values-iw/strings.xml
index ea5ab25..5c55fe2 100644
--- a/slices/view/src/main/res/values-iw/strings.xml
+++ b/slices/view/src/main/res/values-iw/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"עוד"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"הצג יותר"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"עודכן ב-<xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"לא ניתן היה להתחבר"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-ja/strings.xml b/slices/view/src/main/res/values-ja/strings.xml
index ed51803..286949f 100644
--- a/slices/view/src/main/res/values-ja/strings.xml
+++ b/slices/view/src/main/res/values-ja/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"他 <xliff:g id="NUMBER">%1$d</xliff:g> 件"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"もっと見る"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"もっと見る"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"更新時刻: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"接続できませんでした"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-ka/strings.xml b/slices/view/src/main/res/values-ka/strings.xml
index ea5ab25..542c6cc 100644
--- a/slices/view/src/main/res/values-ka/strings.xml
+++ b/slices/view/src/main/res/values-ka/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"მეტი"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"მეტის ჩვენება"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"განახლების დრო: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"დაკავშირება ვერ მოხერხდა"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-kk/strings.xml b/slices/view/src/main/res/values-kk/strings.xml
index ea5ab25..231a499 100644
--- a/slices/view/src/main/res/values-kk/strings.xml
+++ b/slices/view/src/main/res/values-kk/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Тағы"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Толығырақ көрсету"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Жаңартылған уақыты: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Байланыс орнатылмады"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-km/strings.xml b/slices/view/src/main/res/values-km/strings.xml
index ea5ab25..1e78acb 100644
--- a/slices/view/src/main/res/values-km/strings.xml
+++ b/slices/view/src/main/res/values-km/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"ច្រើន​ទៀត"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"បង្ហាញ​ច្រើនទៀត"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"បាន​ធ្វើ​បច្ចុប្បន្នភាព​កាលពី <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> នាទី​មុន</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> នាទី​មុន</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ឆ្នាំ​មុន</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> ឆ្នាំ​មុន</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ថ្ងៃ​មុន</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> ថ្ងៃ​មុន</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"មិន​អាចភ្ជាប់​បានទេ"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-kn/strings.xml b/slices/view/src/main/res/values-kn/strings.xml
index ea5ab25..ed7dcd6 100644
--- a/slices/view/src/main/res/values-kn/strings.xml
+++ b/slices/view/src/main/res/values-kn/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"ಇನ್ನಷ್ಟು"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"ಹೆಚ್ಚು ತೋರಿಸಿ"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> ಅನ್ನು ಅಪ್‌ಡೇಟ್‌ ಮಾಡಲಾಗಿದೆ"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> ನಿಮಿಷದ ಹಿಂದೆ</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ನಿಮಿಷದ ಹಿಂದೆ</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> ವರ್ಷದ ಹಿಂದೆ</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ವರ್ಷದ ಹಿಂದೆ</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> ದಿನಗಳ ಹಿಂದೆ</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ದಿನಗಳ ಹಿಂದೆ</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"ಸಂಪರ್ಕಿಸಲು ಸಾಧ್ಯವಾಗುತ್ತಿಲ್ಲ"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-ko/strings.xml b/slices/view/src/main/res/values-ko/strings.xml
index 2dc0279..1ec58f0 100644
--- a/slices/view/src/main/res/values-ko/strings.xml
+++ b/slices/view/src/main/res/values-ko/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"<xliff:g id="NUMBER">%1$d</xliff:g>개 더보기"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"더보기"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"더보기"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g>에 업데이트됨"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"연결할 수 없음"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-ky/strings.xml b/slices/view/src/main/res/values-ky/strings.xml
index ea5ab25..06244ab 100644
--- a/slices/view/src/main/res/values-ky/strings.xml
+++ b/slices/view/src/main/res/values-ky/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Дагы"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Дагы көрсөтүү"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> жаңыртылды"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Туташпай койду"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-lo/strings.xml b/slices/view/src/main/res/values-lo/strings.xml
index ea5ab25..44111ea 100644
--- a/slices/view/src/main/res/values-lo/strings.xml
+++ b/slices/view/src/main/res/values-lo/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"ເພີ່ມເຕີມ"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"ສະແດງເພີ່ມເຕີມ"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"ອັບເດດເມື່ອ <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"ບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-lt/strings.xml b/slices/view/src/main/res/values-lt/strings.xml
index bb6bcb3..e4094b4 100644
--- a/slices/view/src/main/res/values-lt/strings.xml
+++ b/slices/view/src/main/res/values-lt/strings.xml
@@ -18,4 +18,26 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"Dar <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Daugiau"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Rodyti daugiau"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Atnaujinta <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="one">Prieš <xliff:g id="ID_2">%d</xliff:g> min.</item>
+      <item quantity="few">Prieš <xliff:g id="ID_2">%d</xliff:g> min.</item>
+      <item quantity="many">Prieš <xliff:g id="ID_2">%d</xliff:g> min.</item>
+      <item quantity="other">Prieš <xliff:g id="ID_2">%d</xliff:g> min.</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="one">Prieš <xliff:g id="ID_2">%d</xliff:g> m.</item>
+      <item quantity="few">Prieš <xliff:g id="ID_2">%d</xliff:g> m.</item>
+      <item quantity="many">Prieš <xliff:g id="ID_2">%d</xliff:g> m.</item>
+      <item quantity="other">Prieš <xliff:g id="ID_2">%d</xliff:g> m.</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="one">Prieš <xliff:g id="ID_2">%d</xliff:g> d.</item>
+      <item quantity="few">Prieš <xliff:g id="ID_2">%d</xliff:g> d.</item>
+      <item quantity="many">Prieš <xliff:g id="ID_2">%d</xliff:g> d.</item>
+      <item quantity="other">Prieš <xliff:g id="ID_2">%d</xliff:g> d.</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Prisijungti nepavyko"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-lv/strings.xml b/slices/view/src/main/res/values-lv/strings.xml
index 79ccb99..45be57c 100644
--- a/slices/view/src/main/res/values-lv/strings.xml
+++ b/slices/view/src/main/res/values-lv/strings.xml
@@ -18,4 +18,23 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"Vēl <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Vēl"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Rādīt vairāk"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Atjaunināts <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="zero">Pirms <xliff:g id="ID_2">%d</xliff:g> minūtēm</item>
+      <item quantity="one">Pirms <xliff:g id="ID_2">%d</xliff:g> minūtes</item>
+      <item quantity="other">Pirms <xliff:g id="ID_2">%d</xliff:g> minūtēm</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="zero">Pirms <xliff:g id="ID_2">%d</xliff:g> gadiem</item>
+      <item quantity="one">Pirms <xliff:g id="ID_2">%d</xliff:g> gada</item>
+      <item quantity="other">Pirms <xliff:g id="ID_2">%d</xliff:g> gadiem</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="zero">Pirms <xliff:g id="ID_2">%d</xliff:g> dienām</item>
+      <item quantity="one">Pirms <xliff:g id="ID_2">%d</xliff:g> dienas</item>
+      <item quantity="other">Pirms <xliff:g id="ID_2">%d</xliff:g> dienām</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Nevarēja izveidot savienojumu"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-mk/strings.xml b/slices/view/src/main/res/values-mk/strings.xml
index ea5ab25..ff88cd7 100644
--- a/slices/view/src/main/res/values-mk/strings.xml
+++ b/slices/view/src/main/res/values-mk/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Повеќе"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Прикажи повеќе"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Ажурирано <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="one">Пред <xliff:g id="ID_2">%d</xliff:g> мин.</item>
+      <item quantity="other">Пред <xliff:g id="ID_2">%d</xliff:g> мин.</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="one">Пред <xliff:g id="ID_2">%d</xliff:g> год.</item>
+      <item quantity="other">Пред <xliff:g id="ID_2">%d</xliff:g> год.</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="one">Пред <xliff:g id="ID_2">%d</xliff:g> ден</item>
+      <item quantity="other">Пред <xliff:g id="ID_2">%d</xliff:g> дена</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Не може да се поврзе"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-ml/strings.xml b/slices/view/src/main/res/values-ml/strings.xml
index ea5ab25..b67ae80 100644
--- a/slices/view/src/main/res/values-ml/strings.xml
+++ b/slices/view/src/main/res/values-ml/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"കൂടുതൽ"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"കൂടുതൽ കാണിക്കുക"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> അപ്‌ഡേറ്റ് ചെയ്‌തു"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> മിനിറ്റ് മുൻപ്</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> മിനിറ്റ് മുൻപ്</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> വർഷം മുൻപ്</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> വർഷം മുൻപ്</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ദിവസം മുൻപ്</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> ദിവസം മുൻപ്</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"കണക്റ്റ് ചെയ്യാനായില്ല"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-mn/strings.xml b/slices/view/src/main/res/values-mn/strings.xml
index ea5ab25..beaa1a3 100644
--- a/slices/view/src/main/res/values-mn/strings.xml
+++ b/slices/view/src/main/res/values-mn/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Бусад"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Дэлгэрэнгүй үзэх"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> шинэчилсэн"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Холбогдож чадсангүй"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-mr/strings.xml b/slices/view/src/main/res/values-mr/strings.xml
index ea5ab25..693c821 100644
--- a/slices/view/src/main/res/values-mr/strings.xml
+++ b/slices/view/src/main/res/values-mr/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"आणखी"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"आणखी दाखवा"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> ला अपडेट केले"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"कनेक्ट करता आले नाही"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-ms/strings.xml b/slices/view/src/main/res/values-ms/strings.xml
index ea5ab25..eced3de 100644
--- a/slices/view/src/main/res/values-ms/strings.xml
+++ b/slices/view/src/main/res/values-ms/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Lagi"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Tunjukkan lagi"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Dikemas kini pada <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Tidak dapat menyambung"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-my/strings.xml b/slices/view/src/main/res/values-my/strings.xml
index ea5ab25..4a3e3d4 100644
--- a/slices/view/src/main/res/values-my/strings.xml
+++ b/slices/view/src/main/res/values-my/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"နောက်ထပ်"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"နောက်ထပ် ပြပါ"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> က အပ်ဒိတ်လုပ်ထားသည်"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other">ပြီးခဲ့သော<xliff:g id="ID_2">%d</xliff:g>မိနစ်</item>
+      <item quantity="one">ပြီးခဲ့သော<xliff:g id="ID_1">%d</xliff:g>မိနစ်</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other">ပြီးခဲ့သော <xliff:g id="ID_2">%d</xliff:g>နှစ်</item>
+      <item quantity="one">ပြီးခဲ့သော <xliff:g id="ID_1">%d</xliff:g>နှစ်</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other">ပြီးခဲ့သော <xliff:g id="ID_2">%d</xliff:g> ရက်</item>
+      <item quantity="one">ပြီးခဲ့သော <xliff:g id="ID_1">%d</xliff:g> ရက်</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"ချိတ်ဆက်၍ မရပါ"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-nb/strings.xml b/slices/view/src/main/res/values-nb/strings.xml
index ea5ab25..6576626 100644
--- a/slices/view/src/main/res/values-nb/strings.xml
+++ b/slices/view/src/main/res/values-nb/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Mer"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Vis mer"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Oppdatert <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Kunne ikke koble til"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-ne/strings.xml b/slices/view/src/main/res/values-ne/strings.xml
index ea5ab25..beb8b31 100644
--- a/slices/view/src/main/res/values-ne/strings.xml
+++ b/slices/view/src/main/res/values-ne/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"थप"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"थप देखाउनुहोस्"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"अद्यावधिक गरिएको समय: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> मिनेटअघि</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> मिनेटअघि</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> वर्षअघि</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> वर्षअघि</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> दिनअघि</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> दिनअघि</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"जडान गर्न सकिएन"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-nl/strings.xml b/slices/view/src/main/res/values-nl/strings.xml
index ea5ab25..1ba58e9 100644
--- a/slices/view/src/main/res/values-nl/strings.xml
+++ b/slices/view/src/main/res/values-nl/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Meer"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Meer weergeven"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Geüpdatet: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Kan geen verbinding maken"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-or/strings.xml b/slices/view/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000..1099e80
--- /dev/null
+++ b/slices/view/src/main/res/values-or/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ Copyright 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"ଅଧିକ"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"ଅଧିକ ଦେଖାନ୍ତୁ"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g>ରେ ଅପଡେଟ୍ ହୋଇଥିଲା"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"କନେକ୍ଟ ହେଲାନାହିଁ"</string>
+</resources>
diff --git a/slices/view/src/main/res/values-pa/strings.xml b/slices/view/src/main/res/values-pa/strings.xml
index ea5ab25..8926d0f 100644
--- a/slices/view/src/main/res/values-pa/strings.xml
+++ b/slices/view/src/main/res/values-pa/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"ਹੋਰ"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"ਹੋਰ ਦਿਖਾਓ"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> ਅੱਪਡੇਟ ਕੀਤੀ ਗਈ"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"ਕਨੈਕਟ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-pl/strings.xml b/slices/view/src/main/res/values-pl/strings.xml
index 9d9bede..fa565f3 100644
--- a/slices/view/src/main/res/values-pl/strings.xml
+++ b/slices/view/src/main/res/values-pl/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Więcej"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Pokaż więcej"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Aktualizacja: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Nie udało się połączyć"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-pt-rBR/strings.xml b/slices/view/src/main/res/values-pt-rBR/strings.xml
index 629ba48..79cc7b7 100644
--- a/slices/view/src/main/res/values-pt-rBR/strings.xml
+++ b/slices/view/src/main/res/values-pt-rBR/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"Mais <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Mais"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Mostrar mais"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Atualizado às <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> min atrás</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> min atrás</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> ano atrás</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> anos atrás</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> dia atrás</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> dias atrás</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Não foi possível conectar"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-pt-rPT/strings.xml b/slices/view/src/main/res/values-pt-rPT/strings.xml
index ea5ab25..b81ec60 100644
--- a/slices/view/src/main/res/values-pt-rPT/strings.xml
+++ b/slices/view/src/main/res/values-pt-rPT/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Mais"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Mostrar mais"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Atualizado: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other">Há <xliff:g id="ID_2">%d</xliff:g> minutos.</item>
+      <item quantity="one">Há <xliff:g id="ID_1">%d</xliff:g> minuto.</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other">Há <xliff:g id="ID_2">%d</xliff:g> anos.</item>
+      <item quantity="one">Há <xliff:g id="ID_1">%d</xliff:g> ano.</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other">Há <xliff:g id="ID_2">%d</xliff:g> dias.</item>
+      <item quantity="one">Há <xliff:g id="ID_1">%d</xliff:g> dia.</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Não foi possível ligar."</string>
 </resources>
diff --git a/slices/view/src/main/res/values-pt/strings.xml b/slices/view/src/main/res/values-pt/strings.xml
index 629ba48..79cc7b7 100644
--- a/slices/view/src/main/res/values-pt/strings.xml
+++ b/slices/view/src/main/res/values-pt/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"Mais <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Mais"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Mostrar mais"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Atualizado às <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> min atrás</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> min atrás</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> ano atrás</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> anos atrás</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> dia atrás</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> dias atrás</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Não foi possível conectar"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-ro/strings.xml b/slices/view/src/main/res/values-ro/strings.xml
index ea5ab25..a3f9ff2 100644
--- a/slices/view/src/main/res/values-ro/strings.xml
+++ b/slices/view/src/main/res/values-ro/strings.xml
@@ -18,4 +18,23 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Mai mult"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Vedeți mai multe"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Actualizat la <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="few">Acum <xliff:g id="ID_2">%d</xliff:g> min.</item>
+      <item quantity="other">Acum <xliff:g id="ID_2">%d</xliff:g> de min.</item>
+      <item quantity="one">Acum <xliff:g id="ID_1">%d</xliff:g> min.</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="few">Acum <xliff:g id="ID_2">%d</xliff:g> ani</item>
+      <item quantity="other">Acum <xliff:g id="ID_2">%d</xliff:g> de ani</item>
+      <item quantity="one">Acum <xliff:g id="ID_1">%d</xliff:g> an</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="few">Acum <xliff:g id="ID_2">%d</xliff:g> zile</item>
+      <item quantity="other">Acum <xliff:g id="ID_2">%d</xliff:g> de zile</item>
+      <item quantity="one">Acum <xliff:g id="ID_1">%d</xliff:g> zi</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Nu s-a putut conecta"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-ru/strings.xml b/slices/view/src/main/res/values-ru/strings.xml
index 9d9bede..85953aa 100644
--- a/slices/view/src/main/res/values-ru/strings.xml
+++ b/slices/view/src/main/res/values-ru/strings.xml
@@ -18,4 +18,26 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Ещё"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Ещё"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Обновлено <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> мин. назад</item>
+      <item quantity="few"><xliff:g id="ID_2">%d</xliff:g> мин. назад</item>
+      <item quantity="many"><xliff:g id="ID_2">%d</xliff:g> мин. назад</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> мин. назад</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> г. назад</item>
+      <item quantity="few"><xliff:g id="ID_2">%d</xliff:g> г. назад</item>
+      <item quantity="many"><xliff:g id="ID_2">%d</xliff:g> лет назад</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> г. назад</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> дн. назад</item>
+      <item quantity="few"><xliff:g id="ID_2">%d</xliff:g> дн. назад</item>
+      <item quantity="many"><xliff:g id="ID_2">%d</xliff:g> дн. назад</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> дн. назад</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Ошибка подключения"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-si/strings.xml b/slices/view/src/main/res/values-si/strings.xml
index ea5ab25..be85112 100644
--- a/slices/view/src/main/res/values-si/strings.xml
+++ b/slices/view/src/main/res/values-si/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"තව"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"තව පෙන්වන්න"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> යාවත්කාලීන කරන ලදී"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"සම්බන්ධ වීමට නොහැකි විය"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-sk/strings.xml b/slices/view/src/main/res/values-sk/strings.xml
index ea5ab25..a153760 100644
--- a/slices/view/src/main/res/values-sk/strings.xml
+++ b/slices/view/src/main/res/values-sk/strings.xml
@@ -18,4 +18,26 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Viac"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Zobraziť viac"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Aktualizované <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="few">Pred <xliff:g id="ID_2">%d</xliff:g> min</item>
+      <item quantity="many">Pred <xliff:g id="ID_2">%d</xliff:g> min</item>
+      <item quantity="other">Pred <xliff:g id="ID_2">%d</xliff:g> min</item>
+      <item quantity="one">Pred <xliff:g id="ID_1">%d</xliff:g> min</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="few">Pred <xliff:g id="ID_2">%d</xliff:g> rokmi</item>
+      <item quantity="many">Pred <xliff:g id="ID_2">%d</xliff:g> rokom</item>
+      <item quantity="other">Pred <xliff:g id="ID_2">%d</xliff:g> rokmi</item>
+      <item quantity="one">Pred <xliff:g id="ID_1">%d</xliff:g> rokom</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="few">Pred <xliff:g id="ID_2">%d</xliff:g> dňami</item>
+      <item quantity="many">Pred <xliff:g id="ID_2">%d</xliff:g> dňami</item>
+      <item quantity="other">Pred <xliff:g id="ID_2">%d</xliff:g> dňami</item>
+      <item quantity="one">Pred <xliff:g id="ID_1">%d</xliff:g> dňom</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Nepodarilo sa pripojiť"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-sl/strings.xml b/slices/view/src/main/res/values-sl/strings.xml
index 59bf101..0389500 100644
--- a/slices/view/src/main/res/values-sl/strings.xml
+++ b/slices/view/src/main/res/values-sl/strings.xml
@@ -18,4 +18,26 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"in še <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Več"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Pokaži več"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Posodobljeno: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="one">pred <xliff:g id="ID_2">%d</xliff:g> minuto</item>
+      <item quantity="two">pred <xliff:g id="ID_2">%d</xliff:g> minutama</item>
+      <item quantity="few">pred <xliff:g id="ID_2">%d</xliff:g> minutami</item>
+      <item quantity="other">pred <xliff:g id="ID_2">%d</xliff:g> minutami</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="one">pred <xliff:g id="ID_2">%d</xliff:g> letom</item>
+      <item quantity="two">pred <xliff:g id="ID_2">%d</xliff:g> letoma</item>
+      <item quantity="few">pred <xliff:g id="ID_2">%d</xliff:g> leti</item>
+      <item quantity="other">pred <xliff:g id="ID_2">%d</xliff:g> leti</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="one">pred <xliff:g id="ID_2">%d</xliff:g> dnem</item>
+      <item quantity="two">pred <xliff:g id="ID_2">%d</xliff:g> dnevoma</item>
+      <item quantity="few">pred <xliff:g id="ID_2">%d</xliff:g> dnevi</item>
+      <item quantity="other">pred <xliff:g id="ID_2">%d</xliff:g> dnevi</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Povezava ni mogoča"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-sq/strings.xml b/slices/view/src/main/res/values-sq/strings.xml
index ea5ab25..0359c86 100644
--- a/slices/view/src/main/res/values-sq/strings.xml
+++ b/slices/view/src/main/res/values-sq/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Më shumë"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Shfaq më shumë"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Përditësuar <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> minuta më parë</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> minutë më parë</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> vite më parë</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> vit më parë</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ditë më parë</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> ditë më parë</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Nuk mund të lidhej"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-sr/strings.xml b/slices/view/src/main/res/values-sr/strings.xml
index a66220a..718f3d7 100644
--- a/slices/view/src/main/res/values-sr/strings.xml
+++ b/slices/view/src/main/res/values-sr/strings.xml
@@ -18,4 +18,23 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"и још <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Још"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Прикажи више"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Ажурирано <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="one">пре <xliff:g id="ID_2">%d</xliff:g> мин</item>
+      <item quantity="few">пре <xliff:g id="ID_2">%d</xliff:g> мин</item>
+      <item quantity="other">пре <xliff:g id="ID_2">%d</xliff:g> мин</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="one">пре <xliff:g id="ID_2">%d</xliff:g> год</item>
+      <item quantity="few">пре <xliff:g id="ID_2">%d</xliff:g> год</item>
+      <item quantity="other">пре <xliff:g id="ID_2">%d</xliff:g> год</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="one">пре <xliff:g id="ID_2">%d</xliff:g> дан</item>
+      <item quantity="few">пре <xliff:g id="ID_2">%d</xliff:g> дана</item>
+      <item quantity="other">пре <xliff:g id="ID_2">%d</xliff:g> дана</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Повезивање није успело"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-sv/strings.xml b/slices/view/src/main/res/values-sv/strings.xml
index cbcec4f..bc6947d 100644
--- a/slices/view/src/main/res/values-sv/strings.xml
+++ b/slices/view/src/main/res/values-sv/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"<xliff:g id="NUMBER">%1$d</xliff:g> till"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Mer"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Visa mer"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Uppdaterades <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Det gick inte att ansluta"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-sw/strings.xml b/slices/view/src/main/res/values-sw/strings.xml
index ea5ab25..609c51f 100644
--- a/slices/view/src/main/res/values-sw/strings.xml
+++ b/slices/view/src/main/res/values-sw/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Mengine"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Onyesha mengine"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Ilisasishwa <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other">Dakika <xliff:g id="ID_2">%d</xliff:g> zilizopita</item>
+      <item quantity="one">Dakika <xliff:g id="ID_1">%d</xliff:g> iliyopita</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other">Miaka <xliff:g id="ID_2">%d</xliff:g> iliyopita</item>
+      <item quantity="one">Mwaka <xliff:g id="ID_1">%d</xliff:g> uliopita</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other">Siku <xliff:g id="ID_2">%d</xliff:g> zilizopita</item>
+      <item quantity="one">Siku <xliff:g id="ID_1">%d</xliff:g> iliyopita</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Imeshindwa kuunganisha"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-ta/strings.xml b/slices/view/src/main/res/values-ta/strings.xml
index ea5ab25..5b36b79 100644
--- a/slices/view/src/main/res/values-ta/strings.xml
+++ b/slices/view/src/main/res/values-ta/strings.xml
@@ -18,4 +18,13 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"மேலும்"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"மேலும் காட்டு"</string>
+    <!-- no translation found for abc_slice_updated (8155085405396453848) -->
+    <skip />
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <!-- no translation found for abc_slice_error (4188371422904147368) -->
+    <skip />
 </resources>
diff --git a/slices/view/src/main/res/values-te/strings.xml b/slices/view/src/main/res/values-te/strings.xml
index ea5ab25..82137b8 100644
--- a/slices/view/src/main/res/values-te/strings.xml
+++ b/slices/view/src/main/res/values-te/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"మరింత"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"మరింత చూపు"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"అప్‌డేట్ చేసిన సమయం <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> నిమి క్రితం</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> నిమి క్రితం</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> సం క్రితం</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> సం క్రితం</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> రోజుల క్రితం</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> రోజు క్రితం</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"కనెక్ట్ చేయడం సాధ్యపడలేదు"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-th/strings.xml b/slices/view/src/main/res/values-th/strings.xml
index ea5ab25..3ae19da 100644
--- a/slices/view/src/main/res/values-th/strings.xml
+++ b/slices/view/src/main/res/values-th/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"เพิ่มเติม"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"แสดงเพิ่ม"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"อัปเดตเมื่อ <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> นาทีที่แล้ว</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> นาทีที่แล้ว</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ปีที่แล้ว</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> ปีที่แล้ว</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> วันที่แล้ว</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> วันที่แล้ว</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"เชื่อมต่อไม่ได้"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-tl/strings.xml b/slices/view/src/main/res/values-tl/strings.xml
index ea5ab25..d2d5526 100644
--- a/slices/view/src/main/res/values-tl/strings.xml
+++ b/slices/view/src/main/res/values-tl/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Higit pa"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Magpakita pa"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Na-update noong <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> minuto ang nakalipas</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> na minuto ang nakalipas</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> taon ang nakalipas</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> na taon ang nakalipas</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> araw ang nakalipas</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> na araw ang nakalipas</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Hindi makakonekta"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-tr/strings.xml b/slices/view/src/main/res/values-tr/strings.xml
index ea5ab25..84da5fb 100644
--- a/slices/view/src/main/res/values-tr/strings.xml
+++ b/slices/view/src/main/res/values-tr/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Diğer"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Daha fazla göster"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Güncellenme zamanı: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> dk. önce</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> dk. önce</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> yıl önce</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> yıl önce</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> gün önce</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> gün önce</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Bağlanılamadı"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-uk/strings.xml b/slices/view/src/main/res/values-uk/strings.xml
index ea5ab25..0706d80 100644
--- a/slices/view/src/main/res/values-uk/strings.xml
+++ b/slices/view/src/main/res/values-uk/strings.xml
@@ -18,4 +18,26 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Більше"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Показати більше"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Оновлено: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> хвилину тому</item>
+      <item quantity="few"><xliff:g id="ID_2">%d</xliff:g> хвилини тому</item>
+      <item quantity="many"><xliff:g id="ID_2">%d</xliff:g> хвилин тому</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> хвилини тому</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> рік тому</item>
+      <item quantity="few"><xliff:g id="ID_2">%d</xliff:g> роки тому</item>
+      <item quantity="many"><xliff:g id="ID_2">%d</xliff:g> років тому</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> року тому</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> день тому</item>
+      <item quantity="few"><xliff:g id="ID_2">%d</xliff:g> дні тому</item>
+      <item quantity="many"><xliff:g id="ID_2">%d</xliff:g> днів тому</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> дня тому</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Не вдалося під’єднатися"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-ur/strings.xml b/slices/view/src/main/res/values-ur/strings.xml
index ce427c0..4f51700 100644
--- a/slices/view/src/main/res/values-ur/strings.xml
+++ b/slices/view/src/main/res/values-ur/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"‎‎‎‎‎+ <xliff:g id="NUMBER">%1$d</xliff:g>‎‎"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"مزید"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"مزید دکھائیں"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> اپ ڈیٹ کیا گیا"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"منسلک نہیں ہو سکا"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-uz/strings.xml b/slices/view/src/main/res/values-uz/strings.xml
index ea5ab25..2250c91 100644
--- a/slices/view/src/main/res/values-uz/strings.xml
+++ b/slices/view/src/main/res/values-uz/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Yana"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Yana"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Yangilandi: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Ulanib bo‘lmadi"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-vi/strings.xml b/slices/view/src/main/res/values-vi/strings.xml
index ea5ab25..c9129be 100644
--- a/slices/view/src/main/res/values-vi/strings.xml
+++ b/slices/view/src/main/res/values-vi/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Thêm"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Hiển thị thêm"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Đã cập nhật lúc <xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"Không thể kết nối"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-zh-rCN/strings.xml b/slices/view/src/main/res/values-zh-rCN/strings.xml
index ea5ab25..2ecb835 100644
--- a/slices/view/src/main/res/values-zh-rCN/strings.xml
+++ b/slices/view/src/main/res/values-zh-rCN/strings.xml
@@ -18,4 +18,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"更多"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"显示更多"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"更新时间:<xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+    <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+    <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+    <string name="abc_slice_error" msgid="4188371422904147368">"无法连接"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-zh-rHK/strings.xml b/slices/view/src/main/res/values-zh-rHK/strings.xml
index ea5ab25..d1ac276 100644
--- a/slices/view/src/main/res/values-zh-rHK/strings.xml
+++ b/slices/view/src/main/res/values-zh-rHK/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"更多"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"顯示更多"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"更新時間:<xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> 分鐘前</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> 分鐘前</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> 年前</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> 年前</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> 天前</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> 天前</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"無法連線"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-zh-rTW/strings.xml b/slices/view/src/main/res/values-zh-rTW/strings.xml
index ea5ab25..d1ac276 100644
--- a/slices/view/src/main/res/values-zh-rTW/strings.xml
+++ b/slices/view/src/main/res/values-zh-rTW/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"更多"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"顯示更多"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"更新時間:<xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> 分鐘前</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> 分鐘前</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> 年前</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> 年前</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> 天前</item>
+      <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> 天前</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"無法連線"</string>
 </resources>
diff --git a/slices/view/src/main/res/values-zu/strings.xml b/slices/view/src/main/res/values-zu/strings.xml
index ea5ab25..966bb1b 100644
--- a/slices/view/src/main/res/values-zu/strings.xml
+++ b/slices/view/src/main/res/values-zu/strings.xml
@@ -18,4 +18,20 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+    <string name="abc_slice_more" msgid="1983560225998630901">"Okuningi"</string>
+    <string name="abc_slice_show_more" msgid="1567717014004692768">"Bonisa okuningi"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"Kubuyekezwe ngo-<xliff:g id="TIME">%1$s</xliff:g>"</string>
+    <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> iminithi eledlule</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> iminithi eledlule</item>
+    </plurals>
+    <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> unyaka owedlule</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> unyaka owedlule</item>
+    </plurals>
+    <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+      <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> izinsuku ezedlule</item>
+      <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> izinsuku ezedlule</item>
+    </plurals>
+    <string name="abc_slice_error" msgid="4188371422904147368">"Ayikwazanga ukuxhuma"</string>
 </resources>
diff --git a/slices/view/src/main/res/values/attrs.xml b/slices/view/src/main/res/values/attrs.xml
index f3e621b..b73c5a5 100644
--- a/slices/view/src/main/res/values/attrs.xml
+++ b/slices/view/src/main/res/values/attrs.xml
@@ -30,17 +30,33 @@
         <attr name="headerTitleSize" format="dimension" />
         <!-- Text size to use for subtitle text in the header of the slice. -->
         <attr name="headerSubtitleSize" format="dimension"/>
+        <!-- Vertical padding to use between header title text and header subtitle text. -->
+        <attr name="headerTextVerticalPadding" format="dimension" />
+
         <!-- Text size to use for title text in a non-header row of the slice. -->
         <attr name="titleSize" format="dimension" />
         <!-- Text size to use for subtitle text in a non-header row of the slice. -->
         <attr name="subtitleSize" format="dimension" />
+        <!-- Vertical padding to use between title text and subtitle text. -->
+        <attr name="textVerticalPadding" format="dimension" />
+
         <!-- Text size to use for title text in a grid row of the slice. -->
         <attr name="gridTitleSize" format="dimension" />
         <!-- Text size to use for the subtitle text in a grid row of the slice. -->
         <attr name="gridSubtitleSize" format="dimension" />
+        <!-- Vertical padding to use between title text and subtitle text in a grid cell. -->
+        <attr name="gridTextVerticalPadding" format="dimension" />
+        <!-- A grid row with all images goes right to the edge of the view if it's the first or
+         last row of a slice. Use this to specify padding to apply to the top of the grid row in
+         this situation. -->
+        <attr name="gridTopPadding" format="dimension" />
+        <!-- A grid row with all images goes right to the edge of the view if it's the first or
+         last row of a slice. Use this to specify padding to apply to the bottom of the grid row in
+         this situation. -->
+        <attr name="gridBottomPadding" format="dimension" />
     </declare-styleable>
 
     <!-- To apply a style for all slices shown within an activity or app you
          may set this in your app theme pointing to your custom SliceView style.-->
-    <attr name="sliceViewStyle" format="reference"/>
+    <attr name="sliceViewStyle" format="reference" />
 </resources>
\ No newline at end of file
diff --git a/slices/view/src/main/res/values/dimens.xml b/slices/view/src/main/res/values/dimens.xml
index f3313bf..2894fcc 100644
--- a/slices/view/src/main/res/values/dimens.xml
+++ b/slices/view/src/main/res/values/dimens.xml
@@ -58,6 +58,10 @@
     <dimen name="abc_slice_grid_small_image_text_height">120dp</dimen>
     <!-- Gutter between cells in a grid row-->
     <dimen name="abc_slice_grid_gutter">4dp</dimen>
+    <!-- Space between image and text items in grid row -->
+    <dimen name="abc_slice_grid_text_padding">10dp</dimen>
+    <!-- Space between text and text items in grid row -->
+    <dimen name="abc_slice_grid_text_inner_padding">2dp</dimen>
 
     <!-- Big picture -->
     <!-- Min height of row showing a single large image -->
diff --git a/swiperefreshlayout/api/current.txt b/swiperefreshlayout/api/current.txt
index 018d30e..49e8991 100644
--- a/swiperefreshlayout/api/current.txt
+++ b/swiperefreshlayout/api/current.txt
@@ -58,7 +58,9 @@
     method public void setProgressViewOffset(boolean, int, int);
     method public void setRefreshing(boolean);
     method public void setSize(int);
+    method public void setSlingshotDistance(int);
     field public static final int DEFAULT = 1; // 0x1
+    field public static final int DEFAULT_SLINGSHOT_DISTANCE = -1; // 0xffffffff
     field public static final int LARGE = 0; // 0x0
     field protected int mFrom;
     field protected int mOriginalOffsetTop;
diff --git a/swiperefreshlayout/src/main/java/androidx/swiperefreshlayout/widget/SwipeRefreshLayout.java b/swiperefreshlayout/src/main/java/androidx/swiperefreshlayout/widget/SwipeRefreshLayout.java
index cd99498..5df8cdd 100644
--- a/swiperefreshlayout/src/main/java/androidx/swiperefreshlayout/widget/SwipeRefreshLayout.java
+++ b/swiperefreshlayout/src/main/java/androidx/swiperefreshlayout/widget/SwipeRefreshLayout.java
@@ -36,6 +36,7 @@
 import androidx.annotation.ColorRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.Px;
 import androidx.annotation.VisibleForTesting;
 import androidx.core.content.ContextCompat;
 import androidx.core.view.NestedScrollingChild;
@@ -73,6 +74,8 @@
     // Maps to ProgressBar default style
     public static final int DEFAULT = CircularProgressDrawable.DEFAULT;
 
+    public static final int DEFAULT_SLINGSHOT_DISTANCE = -1;
+
     @VisibleForTesting
     static final int CIRCLE_DIAMETER = 40;
     @VisibleForTesting
@@ -149,6 +152,8 @@
 
     int mSpinnerOffsetEnd;
 
+    int mCustomSlingshotDistance;
+
     CircularProgressDrawable mProgress;
 
     private Animation mScaleAnimation;
@@ -294,6 +299,18 @@
     }
 
     /**
+     * Sets a custom slingshot distance.
+     *
+     * @param slingshotDistance The distance in pixels that the refresh indicator can be pulled
+     *                          beyond its resting position. Use
+     *                          {@link #DEFAULT_SLINGSHOT_DISTANCE} to reset to the default value.
+     *
+     */
+    public void setSlingshotDistance(@Px int slingshotDistance) {
+        mCustomSlingshotDistance = slingshotDistance;
+    }
+
+    /**
      * One of DEFAULT, or LARGE.
      */
     public void setSize(int size) {
@@ -902,8 +919,11 @@
         float dragPercent = Math.min(1f, Math.abs(originalDragPercent));
         float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3;
         float extraOS = Math.abs(overscrollTop) - mTotalDragDistance;
-        float slingshotDist = mUsingCustomStart ? mSpinnerOffsetEnd - mOriginalOffsetTop
-                : mSpinnerOffsetEnd;
+        float slingshotDist = mCustomSlingshotDistance > 0
+                ? mCustomSlingshotDistance
+                : (mUsingCustomStart
+                        ? mSpinnerOffsetEnd - mOriginalOffsetTop
+                        : mSpinnerOffsetEnd);
         float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2)
                 / slingshotDist);
         float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow(
diff --git a/testutils-ktx/NO_DOCS b/testutils-ktx/NO_DOCS
new file mode 100644
index 0000000..4dad694
--- /dev/null
+++ b/testutils-ktx/NO_DOCS
@@ -0,0 +1,17 @@
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+Having this file, named NO_DOCS, in a directory will prevent
+Android javadocs from being generated for java files under
+the directory. This is especially useful for test projects.
diff --git a/testutils-ktx/OWNERS b/testutils-ktx/OWNERS
new file mode 100644
index 0000000..e450f4c
--- /dev/null
+++ b/testutils-ktx/OWNERS
@@ -0,0 +1 @@
+jakew@google.com
diff --git a/testutils-ktx/build.gradle b/testutils-ktx/build.gradle
new file mode 100644
index 0000000..5f33da1
--- /dev/null
+++ b/testutils-ktx/build.gradle
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+    id("SupportKotlinLibraryPlugin")
+}
+
+dependencies {
+    compile(KOTLIN_STDLIB)
+    compile(TRUTH)
+}
diff --git a/testutils-ktx/src/main/java/androidx/testutils/assertions.kt b/testutils-ktx/src/main/java/androidx/testutils/assertions.kt
new file mode 100644
index 0000000..b67948a
--- /dev/null
+++ b/testutils-ktx/src/main/java/androidx/testutils/assertions.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.testutils
+
+import com.google.common.truth.ThrowableSubject
+import com.google.common.truth.Truth.assertThat
+
+inline fun <reified T : Throwable> assertThrows(body: () -> Unit): ThrowableSubject {
+    try {
+        body()
+    } catch (e: Throwable) {
+        if (e is T) {
+            return assertThat(e)
+        }
+        throw e
+    }
+    throw AssertionError("Body completed successfully. Expected ${T::class.java.simpleName}.")
+}
+
+fun fail(message: String? = null): Nothing = throw AssertionError(message)
diff --git a/transition/src/main/java/androidx/transition/TransitionManager.java b/transition/src/main/java/androidx/transition/TransitionManager.java
index 517d2e3..579f4b9 100644
--- a/transition/src/main/java/androidx/transition/TransitionManager.java
+++ b/transition/src/main/java/androidx/transition/TransitionManager.java
@@ -193,12 +193,16 @@
     static ArrayMap<ViewGroup, ArrayList<Transition>> getRunningTransitions() {
         WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>> runningTransitions =
                 sRunningTransitions.get();
-        if (runningTransitions == null || runningTransitions.get() == null) {
-            ArrayMap<ViewGroup, ArrayList<Transition>> transitions = new ArrayMap<>();
-            runningTransitions = new WeakReference<>(transitions);
-            sRunningTransitions.set(runningTransitions);
+        if (runningTransitions != null) {
+            ArrayMap<ViewGroup, ArrayList<Transition>> transitions = runningTransitions.get();
+            if (transitions != null) {
+                return transitions;
+            }
         }
-        return runningTransitions.get();
+        ArrayMap<ViewGroup, ArrayList<Transition>> transitions = new ArrayMap<>();
+        runningTransitions = new WeakReference<>(transitions);
+        sRunningTransitions.set(runningTransitions);
+        return transitions;
     }
 
     private static void sceneChangeRunTransition(final ViewGroup sceneRoot,
diff --git a/tv-provider/src/main/java/androidx/tvprovider/media/tv/TvContractUtils.java b/tv-provider/src/main/java/androidx/tvprovider/media/tv/TvContractUtils.java
index 3483d0f..99383c0 100644
--- a/tv-provider/src/main/java/androidx/tvprovider/media/tv/TvContractUtils.java
+++ b/tv-provider/src/main/java/androidx/tvprovider/media/tv/TvContractUtils.java
@@ -50,7 +50,7 @@
         if (TextUtils.isEmpty(commaSeparatedRatings)) {
             return EMPTY;
         }
-        String[] ratings = commaSeparatedRatings.split("\\s*,\\s*");
+        String[] ratings = commaSeparatedRatings.split("\\s*,\\s*", -1);
         List<TvContentRating> contentRatings = new ArrayList<>(ratings.length);
         for (String rating : ratings) {
             try {
diff --git a/v7/appcompat/res/values-af/strings.xml b/v7/appcompat/res/values-af/strings.xml
index 15ed3b2..b7dd9bc 100644
--- a/v7/appcompat/res/values-af/strings.xml
+++ b/v7/appcompat/res/values-af/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"AAN"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"AF"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Soek"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Kieslys+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Simbool+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funksie+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"spasiebalk"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-am/strings.xml b/v7/appcompat/res/values-am/strings.xml
index 42bbc06..485ffad 100644
--- a/v7/appcompat/res/values-am/strings.xml
+++ b/v7/appcompat/res/values-am/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"በርቷል"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ጠፍቷል"</string>
     <string name="search_menu_title" msgid="146198913615257606">"ፈልግ"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"ምናሌ+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"ሰርዝ"</string>
 </resources>
diff --git a/v7/appcompat/res/values-ar/strings.xml b/v7/appcompat/res/values-ar/strings.xml
index 3278162..a7683ce 100644
--- a/v7/appcompat/res/values-ar/strings.xml
+++ b/v7/appcompat/res/values-ar/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"تشغيل"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"إيقاف"</string>
     <string name="search_menu_title" msgid="146198913615257606">"البحث"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"القائمة+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-as/strings.xml b/v7/appcompat/res/values-as/strings.xml
index 1f90618..a3574d7 100644
--- a/v7/appcompat/res/values-as/strings.xml
+++ b/v7/appcompat/res/values-as/strings.xml
@@ -29,13 +29,19 @@
     <string name="abc_searchview_description_voice" msgid="893419373245838918">"কণ্ঠধ্বনিৰ যোগেৰে সন্ধান কৰক"</string>
     <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"এটা এপ্ বাছনি কৰক"</string>
     <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"সকলো চাওক"</string>
-    <!-- no translation found for abc_shareactionprovider_share_with_application (3300176832234831527) -->
-    <skip />
+    <string name="abc_shareactionprovider_share_with_application" msgid="3300176832234831527">"<xliff:g id="APPLICATION_NAME">%s</xliff:g>ৰ জৰিয়তে শ্বেয়াৰ কৰক"</string>
     <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"ইয়াৰ জৰিয়তে শ্বেয়াৰ কৰক"</string>
-    <!-- no translation found for abc_capital_on (3405795526292276155) -->
-    <skip />
-    <!-- no translation found for abc_capital_off (121134116657445385) -->
-    <skip />
-    <!-- no translation found for search_menu_title (146198913615257606) -->
-    <skip />
+    <string name="abc_capital_on" msgid="3405795526292276155">"অন কৰক"</string>
+    <string name="abc_capital_off" msgid="121134116657445385">"অফ কৰক"</string>
+    <string name="search_menu_title" msgid="146198913615257606">"অনুসন্ধান কৰক"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"মেনু+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"মেটা+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"CTRL+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"শ্বিফ্ট+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"ফাংশ্বন+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"স্পেচ"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"এণ্টাৰ"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"মচক"</string>
 </resources>
diff --git a/v7/appcompat/res/values-az/strings.xml b/v7/appcompat/res/values-az/strings.xml
index 29e00dd..aae3b94 100644
--- a/v7/appcompat/res/values-az/strings.xml
+++ b/v7/appcompat/res/values-az/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"AKTİV"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"DEAKTİV"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Axtarış"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menyu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funksiya+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"kosmos"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"daxil olun"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"silin"</string>
 </resources>
diff --git a/v7/appcompat/res/values-b+sr+Latn/strings.xml b/v7/appcompat/res/values-b+sr+Latn/strings.xml
index c3462f6..734525b 100644
--- a/v7/appcompat/res/values-b+sr+Latn/strings.xml
+++ b/v7/appcompat/res/values-b+sr+Latn/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"UKLJUČI"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ISKLJUČI"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Pretraži"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"taster za razmak"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-be/strings.xml b/v7/appcompat/res/values-be/strings.xml
index 99ee19f..f04076a 100644
--- a/v7/appcompat/res/values-be/strings.xml
+++ b/v7/appcompat/res/values-be/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"УКЛ."</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ВЫКЛ."</string>
     <string name="search_menu_title" msgid="146198913615257606">"Пошук"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Меню +"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift +"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn +"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Прабел"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-bg/strings.xml b/v7/appcompat/res/values-bg/strings.xml
index 1d37d0d..2c607a1 100644
--- a/v7/appcompat/res/values-bg/strings.xml
+++ b/v7/appcompat/res/values-bg/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ВКЛ."</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ИЗКЛ."</string>
     <string name="search_menu_title" msgid="146198913615257606">"Търсене"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"клавиша за интервал"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-bn/strings.xml b/v7/appcompat/res/values-bn/strings.xml
index 2ea7591..a6be993 100644
--- a/v7/appcompat/res/values-bn/strings.xml
+++ b/v7/appcompat/res/values-bn/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"চালু"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"বন্ধ"</string>
     <string name="search_menu_title" msgid="146198913615257606">"খুঁজুন"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"মেনু+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"স্পেস"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"মুছুন"</string>
 </resources>
diff --git a/v7/appcompat/res/values-bs/strings.xml b/v7/appcompat/res/values-bs/strings.xml
index 07d1411..91281fc 100644
--- a/v7/appcompat/res/values-bs/strings.xml
+++ b/v7/appcompat/res/values-bs/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"UKLJUČI"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ISKLJUČI"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Pretraži"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"razmaknica"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-ca/strings.xml b/v7/appcompat/res/values-ca/strings.xml
index 03ebec3..c4f3b4d 100644
--- a/v7/appcompat/res/values-ca/strings.xml
+++ b/v7/appcompat/res/values-ca/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ACTIVAT"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"DESACTIVAT"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Cerca"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menú+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Maj+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funció+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Espai"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Retorn"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Supr"</string>
 </resources>
diff --git a/v7/appcompat/res/values-cs/strings.xml b/v7/appcompat/res/values-cs/strings.xml
index 05cd4e0..9111883 100644
--- a/v7/appcompat/res/values-cs/strings.xml
+++ b/v7/appcompat/res/values-cs/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ZAPNUTO"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"VYPNUTO"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Hledat"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"mezerník"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-da/strings.xml b/v7/appcompat/res/values-da/strings.xml
index 813885a..f05e142 100644
--- a/v7/appcompat/res/values-da/strings.xml
+++ b/v7/appcompat/res/values-da/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"TIL"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"FRA"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Søg"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"mellemrum"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-de/strings.xml b/v7/appcompat/res/values-de/strings.xml
index 0b57259..fb0dd6a 100644
--- a/v7/appcompat/res/values-de/strings.xml
+++ b/v7/appcompat/res/values-de/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"An"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"Aus"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Suchen"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menütaste +"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta-Taste +"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Strg +"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Umschalttaste +"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym-Taste +"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funktionstaste +"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Leertaste +"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Eingabetaste"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Löschen"</string>
 </resources>
diff --git a/v7/appcompat/res/values-el/strings.xml b/v7/appcompat/res/values-el/strings.xml
index ec7a666..b7da605 100644
--- a/v7/appcompat/res/values-el/strings.xml
+++ b/v7/appcompat/res/values-el/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ΕΝΕΡΓΟΠΟΙΗΣΗ"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ΑΠΕΝΕΡΓΟΠΟΙΗΣΗ"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Αναζήτηση"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"διάστημα"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-en-rAU/strings.xml b/v7/appcompat/res/values-en-rAU/strings.xml
index a4d048c..a315670 100644
--- a/v7/appcompat/res/values-en-rAU/strings.xml
+++ b/v7/appcompat/res/values-en-rAU/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ON"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"OFF"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Search"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-en-rCA/strings.xml b/v7/appcompat/res/values-en-rCA/strings.xml
index a4d048c..a315670 100644
--- a/v7/appcompat/res/values-en-rCA/strings.xml
+++ b/v7/appcompat/res/values-en-rCA/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ON"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"OFF"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Search"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-en-rGB/strings.xml b/v7/appcompat/res/values-en-rGB/strings.xml
index a4d048c..a315670 100644
--- a/v7/appcompat/res/values-en-rGB/strings.xml
+++ b/v7/appcompat/res/values-en-rGB/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ON"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"OFF"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Search"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-en-rIN/strings.xml b/v7/appcompat/res/values-en-rIN/strings.xml
index a4d048c..a315670 100644
--- a/v7/appcompat/res/values-en-rIN/strings.xml
+++ b/v7/appcompat/res/values-en-rIN/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ON"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"OFF"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Search"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-en-rXC/strings.xml b/v7/appcompat/res/values-en-rXC/strings.xml
index b1d5f93..2e8e581 100644
--- a/v7/appcompat/res/values-en-rXC/strings.xml
+++ b/v7/appcompat/res/values-en-rXC/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‏‏‏‎‏‎‎‎‎‏‏‏‏‎‏‎‎‎‎‏‏‏‏‏‏‎‎‏‎‎‎‏‎‏‎‎‏‎‎‎‏‎‎‎‎‎‎‎‎‏‏‏‎‏‏‏‎‏‏‎ON‎‏‎‎‏‎"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‎‏‏‏‎‏‎‏‏‏‎‎‏‎‏‏‎‏‎‏‏‎‏‎‏‏‎‎‏‎‏‏‎‎‏‏‎‎‎‏‏‎‎‎‎‏‏‏‎‏‎‎‎‎‎‏‎‎‏‎OFF‎‏‎‎‏‎"</string>
     <string name="search_menu_title" msgid="146198913615257606">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‎‏‎‎‎‎‎‎‏‏‏‎‏‏‎‎‏‏‏‎‎‏‎‎‎‏‏‎‎‏‎‏‎‎‎‏‎‎‏‎‏‎‏‏‏‏‎‎‎‎‎‎‎‎‎‎‏‏‎‎Search‎‏‎‎‏‎"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‎‏‏‎‎‎‎‏‎‎‏‏‎‏‏‎‏‎‎‏‏‎‏‎‏‏‏‎‏‎‎‏‏‏‏‏‏‎‎‏‎‏‎‏‎‎‎‎‏‏‎‎‎‏‏‎‏‎Menu+‎‏‎‎‏‎"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‏‎‎‎‎‏‎‎‏‏‎‏‎‎‏‎‏‏‎‏‎‎‎‎‎‏‏‎‎‎‎‎‎‏‏‎‎‏‎‏‎‎‏‏‎‎‎‎‏‏‎‏‎‏‎‏‎‏‎Meta+‎‏‎‎‏‎"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‎‎‏‏‎‎‎‏‎‏‎‏‏‏‏‏‏‎‏‎‎‎‏‎‎‏‎‏‏‎‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‎‎‏‎‎‎‎‎Ctrl+‎‏‎‎‏‎"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‎‎‎‎‏‎‎‏‎‏‎‏‎‎‎‎‏‎‎‏‎‏‎‎‏‏‏‏‏‏‎‏‏‏‎‎‎‎‏‏‏‎‏‎‎‎‏‎‏‏‏‎‏‏‏‏‏‎Alt+‎‏‎‎‏‎"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‎‎‎‏‏‎‎‏‏‎‎‏‏‏‎‏‎‎‏‏‎‎‎‎‎‏‏‎‏‎‏‏‎‎‎‎‎‎‎‎‏‎‎‏‎‎‎‏‏‏‎‏‎‎‎Shift+‎‏‎‎‏‎"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‎‏‏‏‏‏‎‏‎‏‎‏‏‎‎‎‏‎‏‎‏‎‏‎‎‏‎‎‏‏‏‏‏‏‏‎‎‏‎‏‎‎‎‎‎‏‎‎‎‎‎‎‏‎Sym+‎‏‎‎‏‎"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‏‎‏‎‎‎‎‎‏‎‎‎‎‏‏‏‏‎‎‎‏‎‎‎‏‏‏‎‏‎‎‎‎‎‏‏‎‏‏‏‎‎‏‏‎‏‏‎‎‎‎‏‎‏‎‎‏‏‎Function+‎‏‎‎‏‎"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‎‏‎‎‎‎‎‎‏‎‎‏‎‎‏‏‏‏‎‏‏‎‎‎‏‏‏‏‎‏‏‏‏‎‎‎‏‏‏‏‎‏‏‎‏‎‏‏‎‏‏‏‏‎‏‎‏‎‎space‎‏‎‎‏‎"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‏‎‎‎‎‎‏‏‏‎‏‎‎‏‏‎‏‏‎‎‎‎‎‎‎‏‏‏‏‏‏‎‎‏‎‎‏‏‏‏‎‎‏‎‏‏‎‎‎‎‏‏‏‎‎‎enter‎‏‎‎‏‎"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‎‎‎‎‎‏‏‎‎‏‎‎‎‎‏‏‎‎‎‎‏‏‏‎‎‎‏‎‎‏‏‎‎‏‏‏‏‏‏‎‎‏‏‎‏‏‎‎‎‎‎‎‎‎‏‏‎‎delete‎‏‎‎‏‎"</string>
 </resources>
diff --git a/v7/appcompat/res/values-es-rUS/strings.xml b/v7/appcompat/res/values-es-rUS/strings.xml
index 0cc8a70..5d58df0 100644
--- a/v7/appcompat/res/values-es-rUS/strings.xml
+++ b/v7/appcompat/res/values-es-rUS/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ACTIVADO"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"DESACTIVADO"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Buscar"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menú+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Mayúscula+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Función+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"espacio"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"intro"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"borrar"</string>
 </resources>
diff --git a/v7/appcompat/res/values-es/strings.xml b/v7/appcompat/res/values-es/strings.xml
index 3e0828b..5238229 100644
--- a/v7/appcompat/res/values-es/strings.xml
+++ b/v7/appcompat/res/values-es/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ACTIVADO"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"DESACTIVADO"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Buscar"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menú +"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Mayús +"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Función +"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Espacio"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Intro"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Eliminar"</string>
 </resources>
diff --git a/v7/appcompat/res/values-et/strings.xml b/v7/appcompat/res/values-et/strings.xml
index 3a3dcbf..b944f77 100644
--- a/v7/appcompat/res/values-et/strings.xml
+++ b/v7/appcompat/res/values-et/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"SEES"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"VÄLJAS"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Otsing"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menüü +"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Tõstuklahv +"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funktsiooniklahv +"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"tühik"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"sisestusklahv"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"kustutamisklahv"</string>
 </resources>
diff --git a/v7/appcompat/res/values-eu/strings.xml b/v7/appcompat/res/values-eu/strings.xml
index a651036..5a45e61 100644
--- a/v7/appcompat/res/values-eu/strings.xml
+++ b/v7/appcompat/res/values-eu/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"AKTIBATUTA"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"DESAKTIBATUTA"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Bilatu"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menua +"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ktrl +"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Maius +"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funtzioa +"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Zuriunea"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Sartu"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Ezabatu"</string>
 </resources>
diff --git a/v7/appcompat/res/values-fa/strings.xml b/v7/appcompat/res/values-fa/strings.xml
index 9b844b0..35e6596 100644
--- a/v7/appcompat/res/values-fa/strings.xml
+++ b/v7/appcompat/res/values-fa/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"روشن"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"خاموش"</string>
     <string name="search_menu_title" msgid="146198913615257606">"جستجو"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"منو+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"‎Meta+‎"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"‎Ctrl+‎"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"‎Alt+‎"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"‎Shift+‎"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"‎Sym+‎"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"‎Function+‎"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"کلید فاصله"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-fi/strings.xml b/v7/appcompat/res/values-fi/strings.xml
index e9bd952..829b6bb 100644
--- a/v7/appcompat/res/values-fi/strings.xml
+++ b/v7/appcompat/res/values-fi/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"KÄYTÖSSÄ"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"POIS KÄYTÖSTÄ"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Haku"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Valikko+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Vaihto+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"välilyönti"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-fr-rCA/strings.xml b/v7/appcompat/res/values-fr-rCA/strings.xml
index a3e763b..9e83462 100644
--- a/v7/appcompat/res/values-fr-rCA/strings.xml
+++ b/v7/appcompat/res/values-fr-rCA/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ACTIVÉ"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"DÉSACTIVÉ"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Rechercher"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Méta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Maj+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fonction+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"espace"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"entrée"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"supprimer"</string>
 </resources>
diff --git a/v7/appcompat/res/values-fr/strings.xml b/v7/appcompat/res/values-fr/strings.xml
index 1e412ec..5df86b5 100644
--- a/v7/appcompat/res/values-fr/strings.xml
+++ b/v7/appcompat/res/values-fr/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ACTIVÉ"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"DÉSACTIVÉ"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Rechercher"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Méta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Maj+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fonction+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"espace"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"entrée"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"supprimer"</string>
 </resources>
diff --git a/v7/appcompat/res/values-gl/strings.xml b/v7/appcompat/res/values-gl/strings.xml
index 2af80a1..c500af5 100644
--- a/v7/appcompat/res/values-gl/strings.xml
+++ b/v7/appcompat/res/values-gl/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ACTIVAR"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"DESACTIVAR"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Buscar"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menú +"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Maiús +"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sim +"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Función +"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"espazo"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Intro"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"eliminar"</string>
 </resources>
diff --git a/v7/appcompat/res/values-gu/strings.xml b/v7/appcompat/res/values-gu/strings.xml
index 7a243ed..390d59b 100644
--- a/v7/appcompat/res/values-gu/strings.xml
+++ b/v7/appcompat/res/values-gu/strings.xml
@@ -18,7 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_action_mode_done" msgid="4076576682505996667">"થઈ ગયું"</string>
     <string name="abc_action_bar_home_description" msgid="4600421777120114993">"હોમ પર નેવિગેટ કરો"</string>
-    <string name="abc_action_bar_up_description" msgid="1594238315039666878">"ઉપર નેવિગેટ કરો"</string>
+    <string name="abc_action_bar_up_description" msgid="1594238315039666878">"ઉપર નૅવિગેટ કરો"</string>
     <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"વધુ વિકલ્પો"</string>
     <string name="abc_toolbar_collapse_description" msgid="1603543279005712093">"સંકુચિત કરો"</string>
     <string name="abc_searchview_description_search" msgid="8264924765203268293">"શોધો"</string>
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ચાલુ"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"બંધ"</string>
     <string name="search_menu_title" msgid="146198913615257606">"શોધો"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"મેનૂ+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Spacebar"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"ડિલીટ કરો"</string>
 </resources>
diff --git a/v7/appcompat/res/values-hi/strings.xml b/v7/appcompat/res/values-hi/strings.xml
index a31ab90..16dc7ce 100644
--- a/v7/appcompat/res/values-hi/strings.xml
+++ b/v7/appcompat/res/values-hi/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"चालू"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"बंद"</string>
     <string name="search_menu_title" msgid="146198913615257606">"सर्च"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-hr/strings.xml b/v7/appcompat/res/values-hr/strings.xml
index 27a1c2e..9253fb9 100644
--- a/v7/appcompat/res/values-hr/strings.xml
+++ b/v7/appcompat/res/values-hr/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"UKLJUČENO"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ISKLJUČENO"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Pretraživanje"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"razmaknica"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-hu/strings.xml b/v7/appcompat/res/values-hu/strings.xml
index d3e413f..714ef47 100644
--- a/v7/appcompat/res/values-hu/strings.xml
+++ b/v7/appcompat/res/values-hu/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"BE"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"KI"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Keresés"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Szóköz"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-hy/strings.xml b/v7/appcompat/res/values-hy/strings.xml
index 1c41ef6..59c9ee5 100644
--- a/v7/appcompat/res/values-hy/strings.xml
+++ b/v7/appcompat/res/values-hy/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ՄԻԱՑՎԱԾ"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ԱՆՋԱՏՎԱԾ"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Որոնել"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"բացատ"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-in/strings.xml b/v7/appcompat/res/values-in/strings.xml
index 2e9fbb7..1ae3f90 100644
--- a/v7/appcompat/res/values-in/strings.xml
+++ b/v7/appcompat/res/values-in/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"AKTIF"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"NONAKTIF"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Telusuri"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"spasi"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-is/strings.xml b/v7/appcompat/res/values-is/strings.xml
index 3f61d84..3ae59da 100644
--- a/v7/appcompat/res/values-is/strings.xml
+++ b/v7/appcompat/res/values-is/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"KVEIKT"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"SLÖKKT"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Leita"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Valmynd+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Aðgerðarlykill+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"bilslá"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-it/strings.xml b/v7/appcompat/res/values-it/strings.xml
index fbd2c58..04b64ce 100644
--- a/v7/appcompat/res/values-it/strings.xml
+++ b/v7/appcompat/res/values-it/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ON"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"OFF"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Ricerca"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"MENU +"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"META +"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"CTRL +"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"ALT +"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"MAIUSC +"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"SYM +"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"FUNZIONE +"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"barra spaziatrice"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"INVIO"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"CANC"</string>
 </resources>
diff --git a/v7/appcompat/res/values-iw/strings.xml b/v7/appcompat/res/values-iw/strings.xml
index 0a7d0bb..d0f2211 100644
--- a/v7/appcompat/res/values-iw/strings.xml
+++ b/v7/appcompat/res/values-iw/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"פועל"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"כבוי"</string>
     <string name="search_menu_title" msgid="146198913615257606">"חיפוש"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"תפריט+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+‎"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"מקש רווח"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-ja/strings.xml b/v7/appcompat/res/values-ja/strings.xml
index c4d0e20..db163f3 100644
--- a/v7/appcompat/res/values-ja/strings.xml
+++ b/v7/appcompat/res/values-ja/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ON"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"OFF"</string>
     <string name="search_menu_title" msgid="146198913615257606">"検索"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-ka/strings.xml b/v7/appcompat/res/values-ka/strings.xml
index 3b077a3..6d9c08b 100644
--- a/v7/appcompat/res/values-ka/strings.xml
+++ b/v7/appcompat/res/values-ka/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ჩართულია"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"გამორთულია"</string>
     <string name="search_menu_title" msgid="146198913615257606">"ძიება"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"შეყვანა"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"წაშლა"</string>
 </resources>
diff --git a/v7/appcompat/res/values-kk/strings.xml b/v7/appcompat/res/values-kk/strings.xml
index c32045b..9d206bc 100644
--- a/v7/appcompat/res/values-kk/strings.xml
+++ b/v7/appcompat/res/values-kk/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ҚОСУЛЫ"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ӨШІРУЛІ"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Іздеу"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Mәзір+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"бос орын"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-km/strings.xml b/v7/appcompat/res/values-km/strings.xml
index ffe289a..f42da7a 100644
--- a/v7/appcompat/res/values-km/strings.xml
+++ b/v7/appcompat/res/values-km/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"បើក"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"បិទ"</string>
     <string name="search_menu_title" msgid="146198913615257606">"ស្វែងរក"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-kn/strings.xml b/v7/appcompat/res/values-kn/strings.xml
index 4218bd6..ce28303 100644
--- a/v7/appcompat/res/values-kn/strings.xml
+++ b/v7/appcompat/res/values-kn/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ಆನ್"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ಆಫ್"</string>
     <string name="search_menu_title" msgid="146198913615257606">"ಹುಡುಕಿ"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-ko/strings.xml b/v7/appcompat/res/values-ko/strings.xml
index 6c84a2a..db0903f 100644
--- a/v7/appcompat/res/values-ko/strings.xml
+++ b/v7/appcompat/res/values-ko/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"사용"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"사용 안함"</string>
     <string name="search_menu_title" msgid="146198913615257606">"검색"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"스페이스바"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"입력"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"삭제"</string>
 </resources>
diff --git a/v7/appcompat/res/values-ky/strings.xml b/v7/appcompat/res/values-ky/strings.xml
index 66202f7..1aba4f0 100644
--- a/v7/appcompat/res/values-ky/strings.xml
+++ b/v7/appcompat/res/values-ky/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"КҮЙҮК"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ӨЧҮК"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Издөө"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"боштук"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-lo/strings.xml b/v7/appcompat/res/values-lo/strings.xml
index 1b92df0..c53a987 100644
--- a/v7/appcompat/res/values-lo/strings.xml
+++ b/v7/appcompat/res/values-lo/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ເປີດ"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ປິດ"</string>
     <string name="search_menu_title" msgid="146198913615257606">"ຊອກຫາ"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-lt/strings.xml b/v7/appcompat/res/values-lt/strings.xml
index 5793069..ab9f25c 100644
--- a/v7/appcompat/res/values-lt/strings.xml
+++ b/v7/appcompat/res/values-lt/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ĮJUNGTI"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"IŠJUNGTA"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Paieška"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"„Menu“ +"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"„Meta“ +"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"„Ctrl“ +"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"„Alt“ +"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"„Shift“ +"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"„Sym“ +"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"„Function“ +"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"tarpo klavišas"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"„Enter“"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"„Delete“"</string>
 </resources>
diff --git a/v7/appcompat/res/values-lv/strings.xml b/v7/appcompat/res/values-lv/strings.xml
index 67e18d3..67904a1 100644
--- a/v7/appcompat/res/values-lv/strings.xml
+++ b/v7/appcompat/res/values-lv/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"IESLĒGTS"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"IZSLĒGTS"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Meklēt"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Poga Izvēlne +"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta taustiņš +"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Vadīšanas taustiņš +"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alternēšanas taustiņš +"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Pārslēgšanas taustiņš +"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Simbolu taustiņš +"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funkcijas taustiņš +"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"atstarpes taustiņš"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"ievadīšanas taustiņš"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"dzēšanas taustiņš"</string>
 </resources>
diff --git a/v7/appcompat/res/values-mk/strings.xml b/v7/appcompat/res/values-mk/strings.xml
index b12a235..408edb1 100644
--- a/v7/appcompat/res/values-mk/strings.xml
+++ b/v7/appcompat/res/values-mk/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ВКЛУЧЕНО"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ИСКЛУЧЕНО"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Пребарај"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Мени+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"копче Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"копче Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"копче Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"копче Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"копче Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"копче Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"вселена"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"копче enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"избриши"</string>
 </resources>
diff --git a/v7/appcompat/res/values-ml/strings.xml b/v7/appcompat/res/values-ml/strings.xml
index 9033f8a..e880ebb 100644
--- a/v7/appcompat/res/values-ml/strings.xml
+++ b/v7/appcompat/res/values-ml/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ഓൺ"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ഓഫ്"</string>
     <string name="search_menu_title" msgid="146198913615257606">"തിരയുക"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"മെനു+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"മെറ്റ+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"ഫംഗ്ഷന്‍+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"സ്‌പെയ്‌സ്"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"ഇല്ലാതാക്കുക"</string>
 </resources>
diff --git a/v7/appcompat/res/values-mn/strings.xml b/v7/appcompat/res/values-mn/strings.xml
index 56036ea..856e9f6 100644
--- a/v7/appcompat/res/values-mn/strings.xml
+++ b/v7/appcompat/res/values-mn/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ИДЭВХТЭЙ"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ИДЭВХГҮЙ"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Хайлт"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Цэс+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Мета+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Функц+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"зай"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"оруулах"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"устгах"</string>
 </resources>
diff --git a/v7/appcompat/res/values-mr/strings.xml b/v7/appcompat/res/values-mr/strings.xml
index 26ada80..ed3463c 100644
--- a/v7/appcompat/res/values-mr/strings.xml
+++ b/v7/appcompat/res/values-mr/strings.xml
@@ -30,8 +30,18 @@
     <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"एक अ‍ॅप निवडा"</string>
     <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"सर्व पहा"</string>
     <string name="abc_shareactionprovider_share_with_application" msgid="3300176832234831527">"<xliff:g id="APPLICATION_NAME">%s</xliff:g> सह शेअर करा"</string>
-    <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"यांच्यासह सामायिक करा"</string>
+    <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"यांच्यासह शेअर करा"</string>
     <string name="abc_capital_on" msgid="3405795526292276155">"चालू"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"बंद"</string>
     <string name="search_menu_title" msgid="146198913615257606">"शोधा"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"मेनू+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"spacebar"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"एंटर करा"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"हटवा"</string>
 </resources>
diff --git a/v7/appcompat/res/values-ms/strings.xml b/v7/appcompat/res/values-ms/strings.xml
index 18f84ce..8af4c13 100644
--- a/v7/appcompat/res/values-ms/strings.xml
+++ b/v7/appcompat/res/values-ms/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"HIDUP"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"MATI"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Cari"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fungsi+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"ruang"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"padam"</string>
 </resources>
diff --git a/v7/appcompat/res/values-my/strings.xml b/v7/appcompat/res/values-my/strings.xml
index cbc8791..cfd6625 100644
--- a/v7/appcompat/res/values-my/strings.xml
+++ b/v7/appcompat/res/values-my/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ဖွင့်"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ပိတ်"</string>
     <string name="search_menu_title" msgid="146198913615257606">"ရှာဖွေပါ"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-nb/strings.xml b/v7/appcompat/res/values-nb/strings.xml
index 6005234..f9fd80e 100644
--- a/v7/appcompat/res/values-nb/strings.xml
+++ b/v7/appcompat/res/values-nb/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"PÅ"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"AV"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Søk"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Meny+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funksjon+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"mellomrom"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-ne/strings.xml b/v7/appcompat/res/values-ne/strings.xml
index 96b1042..0a0de14 100644
--- a/v7/appcompat/res/values-ne/strings.xml
+++ b/v7/appcompat/res/values-ne/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"सक्रिय गर्नुहोस्"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"निष्क्रिय पार्नुहोस्"</string>
     <string name="search_menu_title" msgid="146198913615257606">"खोज्नुहोस्"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-nl/strings.xml b/v7/appcompat/res/values-nl/strings.xml
index e0d2044..ffcb3d9 100644
--- a/v7/appcompat/res/values-nl/strings.xml
+++ b/v7/appcompat/res/values-nl/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"AAN"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"UIT"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Zoeken"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu +"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift +"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Functie +"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"spatie"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"verwijderen"</string>
 </resources>
diff --git a/v7/appcompat/res/values-or/strings.xml b/v7/appcompat/res/values-or/strings.xml
index 4dc6eb4..86e4fb7 100644
--- a/v7/appcompat/res/values-or/strings.xml
+++ b/v7/appcompat/res/values-or/strings.xml
@@ -29,13 +29,19 @@
     <string name="abc_searchview_description_voice" msgid="893419373245838918">"ଭଏସ୍‌ ସର୍ଚ୍ଚ"</string>
     <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"ଗୋଟିଏ ଆପ୍‍ ବାଛନ୍ତୁ"</string>
     <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"ସବୁ ଦେଖନ୍ତୁ"</string>
-    <!-- no translation found for abc_shareactionprovider_share_with_application (3300176832234831527) -->
-    <skip />
+    <string name="abc_shareactionprovider_share_with_application" msgid="3300176832234831527">"<xliff:g id="APPLICATION_NAME">%s</xliff:g> ସହ ଶେୟାର୍‍ କରନ୍ତୁ"</string>
     <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"ଏହାଙ୍କ ସହ ଶେୟାର୍‌ କରନ୍ତୁ"</string>
-    <!-- no translation found for abc_capital_on (3405795526292276155) -->
-    <skip />
-    <!-- no translation found for abc_capital_off (121134116657445385) -->
-    <skip />
-    <!-- no translation found for search_menu_title (146198913615257606) -->
-    <skip />
+    <string name="abc_capital_on" msgid="3405795526292276155">"ଅନ୍"</string>
+    <string name="abc_capital_off" msgid="121134116657445385">"ଅଫ୍"</string>
+    <string name="search_menu_title" msgid="146198913615257606">"ସର୍ଚ୍ଚ କରନ୍ତୁ"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"ମେନୁ"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"ସ୍ପେସ୍‍"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"ଏଣ୍ଟର୍"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"ଡିଲିଟ୍‍"</string>
 </resources>
diff --git a/v7/appcompat/res/values-pa/strings.xml b/v7/appcompat/res/values-pa/strings.xml
index 7f28ac8..fc1fdba 100644
--- a/v7/appcompat/res/values-pa/strings.xml
+++ b/v7/appcompat/res/values-pa/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ਤੇ"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ਬੰਦ"</string>
     <string name="search_menu_title" msgid="146198913615257606">"ਖੋਜੋ"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-pl/strings.xml b/v7/appcompat/res/values-pl/strings.xml
index d706241..af26367 100644
--- a/v7/appcompat/res/values-pl/strings.xml
+++ b/v7/appcompat/res/values-pl/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"WŁ."</string>
     <string name="abc_capital_off" msgid="121134116657445385">"WYŁ."</string>
     <string name="search_menu_title" msgid="146198913615257606">"Szukaj"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funkcyjny+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"spacja"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-pt-rBR/strings.xml b/v7/appcompat/res/values-pt-rBR/strings.xml
index 90461ec..17d8593 100644
--- a/v7/appcompat/res/values-pt-rBR/strings.xml
+++ b/v7/appcompat/res/values-pt-rBR/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ATIVAR"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"DESATIVAR"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Pesquisar"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"espaço"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-pt-rPT/strings.xml b/v7/appcompat/res/values-pt-rPT/strings.xml
index 40f6499..f8c0fa7 100644
--- a/v7/appcompat/res/values-pt-rPT/strings.xml
+++ b/v7/appcompat/res/values-pt-rPT/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ATIVADO"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"DESATIVADO"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Pesquisar"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu +"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift +"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Função +"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"espaço"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"eliminar"</string>
 </resources>
diff --git a/v7/appcompat/res/values-pt/strings.xml b/v7/appcompat/res/values-pt/strings.xml
index 90461ec..17d8593 100644
--- a/v7/appcompat/res/values-pt/strings.xml
+++ b/v7/appcompat/res/values-pt/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ATIVAR"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"DESATIVAR"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Pesquisar"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"espaço"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-ro/strings.xml b/v7/appcompat/res/values-ro/strings.xml
index 6d04be9..86495fd 100644
--- a/v7/appcompat/res/values-ro/strings.xml
+++ b/v7/appcompat/res/values-ro/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ACTIVAT"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"DEZACTIVAȚI"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Căutați"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Meniu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funcție+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"spațiu"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-ru/strings.xml b/v7/appcompat/res/values-ru/strings.xml
index 2b28958..c505bf5 100644
--- a/v7/appcompat/res/values-ru/strings.xml
+++ b/v7/appcompat/res/values-ru/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ВКЛ."</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ОТКЛ."</string>
     <string name="search_menu_title" msgid="146198913615257606">"Поиск"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Меню +"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift +"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn +"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Пробел"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Ввод"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-si/strings.xml b/v7/appcompat/res/values-si/strings.xml
index 7631288..c154686 100644
--- a/v7/appcompat/res/values-si/strings.xml
+++ b/v7/appcompat/res/values-si/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ක්‍රියාත්මකයි"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ක්‍රියාවිරහිතයි"</string>
     <string name="search_menu_title" msgid="146198913615257606">"සොයන්න"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"මකන්න"</string>
 </resources>
diff --git a/v7/appcompat/res/values-sk/strings.xml b/v7/appcompat/res/values-sk/strings.xml
index 03faf14..67184b0 100644
--- a/v7/appcompat/res/values-sk/strings.xml
+++ b/v7/appcompat/res/values-sk/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ZAPNUTÉ"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"VYPNUTÉ"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Vyhľadávanie"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"medzerník"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"odstrániť"</string>
 </resources>
diff --git a/v7/appcompat/res/values-sl/strings.xml b/v7/appcompat/res/values-sl/strings.xml
index 22b8bd4..e38e5ea 100644
--- a/v7/appcompat/res/values-sl/strings.xml
+++ b/v7/appcompat/res/values-sl/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"VKLOPLJENO"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"IZKLOPLJENO"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Iskanje"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Meni +"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift +"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn +"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"preslednica"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-sq/strings.xml b/v7/appcompat/res/values-sq/strings.xml
index 1a1f02e..8b958a2 100644
--- a/v7/appcompat/res/values-sq/strings.xml
+++ b/v7/appcompat/res/values-sq/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"AKTIV"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"JOAKTIV"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Kërko"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menyja+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funksioni+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"hapësirë"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-sr/strings.xml b/v7/appcompat/res/values-sr/strings.xml
index 5678341..4c702bc 100644
--- a/v7/appcompat/res/values-sr/strings.xml
+++ b/v7/appcompat/res/values-sr/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"УКЉУЧИ"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ИСКЉУЧИ"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Претражи"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"тастер за размак"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-sv/strings.xml b/v7/appcompat/res/values-sv/strings.xml
index 62d470f..72b7f88 100644
--- a/v7/appcompat/res/values-sv/strings.xml
+++ b/v7/appcompat/res/values-sv/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"PÅ"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"AV"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Sök"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Meny + "</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta + "</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl + "</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt + "</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Skift + "</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Symbol + "</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funktion + "</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"blanksteg"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"retur"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-sw/strings.xml b/v7/appcompat/res/values-sw/strings.xml
index c575ae0..1c623b7 100644
--- a/v7/appcompat/res/values-sw/strings.xml
+++ b/v7/appcompat/res/values-sw/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"IMEWASHWA"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"IMEZIMWA"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Tafuta"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menyu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"nafasi"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"futa"</string>
 </resources>
diff --git a/v7/appcompat/res/values-ta/strings.xml b/v7/appcompat/res/values-ta/strings.xml
index 971a3db..7aa031a 100644
--- a/v7/appcompat/res/values-ta/strings.xml
+++ b/v7/appcompat/res/values-ta/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ஆன்"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ஆஃப்"</string>
     <string name="search_menu_title" msgid="146198913615257606">"தேடு"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"மெனு மற்றும்"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"மெட்டா மற்றும்"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"கண்ட்ரோல் மற்றும்"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"ஆல்ட் மற்றும்"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"ஷிஃப்ட் மற்றும்"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"சிம்பல் மற்றும்"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"ஃபங்ஷன் மற்றும்"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"ஸ்பேஸ்"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"எண்டர்"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"டெலிட்"</string>
 </resources>
diff --git a/v7/appcompat/res/values-te/strings.xml b/v7/appcompat/res/values-te/strings.xml
index f7d7577..818f0e3 100644
--- a/v7/appcompat/res/values-te/strings.xml
+++ b/v7/appcompat/res/values-te/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"ఆన్ చేయి"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ఆఫ్ చేయి"</string>
     <string name="search_menu_title" msgid="146198913615257606">"వెతుకు"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"స్పేస్"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-th/strings.xml b/v7/appcompat/res/values-th/strings.xml
index f8ea1cd..5ecec86 100644
--- a/v7/appcompat/res/values-th/strings.xml
+++ b/v7/appcompat/res/values-th/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"เปิด"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ปิด"</string>
     <string name="search_menu_title" msgid="146198913615257606">"ค้นหา"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"เมนู+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-tl/strings.xml b/v7/appcompat/res/values-tl/strings.xml
index 1ad2689..3ef5df6 100644
--- a/v7/appcompat/res/values-tl/strings.xml
+++ b/v7/appcompat/res/values-tl/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"I-ON"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"I-OFF"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Maghanap"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-tr/strings.xml b/v7/appcompat/res/values-tr/strings.xml
index fae41d3..153e2c2 100644
--- a/v7/appcompat/res/values-tr/strings.xml
+++ b/v7/appcompat/res/values-tr/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"AÇ"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"KAPAT"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Ara"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menü+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Üst Karakter+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"İşlev+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"boşluk"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"sil"</string>
 </resources>
diff --git a/v7/appcompat/res/values-uk/strings.xml b/v7/appcompat/res/values-uk/strings.xml
index afc74ff..7aed219 100644
--- a/v7/appcompat/res/values-uk/strings.xml
+++ b/v7/appcompat/res/values-uk/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"УВІМК."</string>
     <string name="abc_capital_off" msgid="121134116657445385">"ВИМК."</string>
     <string name="search_menu_title" msgid="146198913615257606">"Пошук"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"пробіл"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-ur/strings.xml b/v7/appcompat/res/values-ur/strings.xml
index 60ec34a..997d594 100644
--- a/v7/appcompat/res/values-ur/strings.xml
+++ b/v7/appcompat/res/values-ur/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"آن"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"آف"</string>
     <string name="search_menu_title" msgid="146198913615257606">"تلاش"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+‎"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+‎"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+‎"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+‎"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+‎"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+‎"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+‎"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-uz/strings.xml b/v7/appcompat/res/values-uz/strings.xml
index 0417cba..f925bd6 100644
--- a/v7/appcompat/res/values-uz/strings.xml
+++ b/v7/appcompat/res/values-uz/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"YONIQ"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"O‘CHIQ"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Qidirish"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menyu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Probel"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-vi/strings.xml b/v7/appcompat/res/values-vi/strings.xml
index 4560b4b..e07efed 100644
--- a/v7/appcompat/res/values-vi/strings.xml
+++ b/v7/appcompat/res/values-vi/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"BẬT"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"TẮT"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Tìm kiếm"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"phím cách"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
 </resources>
diff --git a/v7/appcompat/res/values-zh-rCN/strings.xml b/v7/appcompat/res/values-zh-rCN/strings.xml
index 7b23457..6dc1ea2 100644
--- a/v7/appcompat/res/values-zh-rCN/strings.xml
+++ b/v7/appcompat/res/values-zh-rCN/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"开启"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"关闭"</string>
     <string name="search_menu_title" msgid="146198913615257606">"搜索"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"空格键"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter 键"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete 键"</string>
 </resources>
diff --git a/v7/appcompat/res/values-zh-rHK/strings.xml b/v7/appcompat/res/values-zh-rHK/strings.xml
index fc32117..ce753fd 100644
--- a/v7/appcompat/res/values-zh-rHK/strings.xml
+++ b/v7/appcompat/res/values-zh-rHK/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"開啟"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"關閉"</string>
     <string name="search_menu_title" msgid="146198913615257606">"搜尋"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu +"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift +"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn +"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"空白鍵"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter 鍵"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"刪除"</string>
 </resources>
diff --git a/v7/appcompat/res/values-zh-rTW/strings.xml b/v7/appcompat/res/values-zh-rTW/strings.xml
index 35be873..0f29b8e 100644
--- a/v7/appcompat/res/values-zh-rTW/strings.xml
+++ b/v7/appcompat/res/values-zh-rTW/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"開啟"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"關閉"</string>
     <string name="search_menu_title" msgid="146198913615257606">"搜尋"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu +"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift +"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn +"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"空格鍵"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter 鍵"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete 鍵"</string>
 </resources>
diff --git a/v7/appcompat/res/values-zu/strings.xml b/v7/appcompat/res/values-zu/strings.xml
index e84ba7a..e3032f1 100644
--- a/v7/appcompat/res/values-zu/strings.xml
+++ b/v7/appcompat/res/values-zu/strings.xml
@@ -34,4 +34,14 @@
     <string name="abc_capital_on" msgid="3405795526292276155">"VULIWE"</string>
     <string name="abc_capital_off" msgid="121134116657445385">"VALIWE"</string>
     <string name="search_menu_title" msgid="146198913615257606">"Sesha"</string>
+    <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Imenyu+"</string>
+    <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+    <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+    <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+    <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+    <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+    <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+    <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+    <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+    <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"susa"</string>
 </resources>
diff --git a/v7/appcompat/res/values/bools.xml b/v7/appcompat/res/values/bools.xml
index c38c0ee..793719b 100644
--- a/v7/appcompat/res/values/bools.xml
+++ b/v7/appcompat/res/values/bools.xml
@@ -18,8 +18,6 @@
 
     <bool name="abc_action_bar_embed_tabs">true</bool>
 
-    <bool name="abc_config_showMenuShortcutsWhenKeyboardPresent">false</bool>
-
     <!-- Whether to allow vertically stacked button bars. This is disabled for
          configurations with a small (e.g. less than 320dp) screen height. -->
     <bool name="abc_allow_stacked_button_bar">false</bool>
diff --git a/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java b/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
index 7991c4e..c32beab 100644
--- a/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
+++ b/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
@@ -38,6 +38,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.appcompat.view.ActionMode;
 import androidx.appcompat.widget.Toolbar;
+import androidx.appcompat.widget.VectorEnabledTintResources;
 import androidx.core.view.WindowCompat;
 import androidx.fragment.app.FragmentActivity;
 
@@ -128,8 +129,6 @@
     @NightMode
     private static int sDefaultNightMode = MODE_NIGHT_FOLLOW_SYSTEM;
 
-    private static boolean sCompatVectorFromResourcesEnabled = false;
-
     /** @hide */
     @RestrictTo(LIBRARY_GROUP)
     @IntDef({MODE_NIGHT_NO, MODE_NIGHT_YES, MODE_NIGHT_AUTO, MODE_NIGHT_FOLLOW_SYSTEM,
@@ -510,7 +509,7 @@
      * <p>Please note: this only takes effect in Activities created after this call.</p>
      */
     public static void setCompatVectorFromResourcesEnabled(boolean enabled) {
-        sCompatVectorFromResourcesEnabled = enabled;
+        VectorEnabledTintResources.setCompatVectorFromResourcesEnabled(enabled);
     }
 
     /**
@@ -520,6 +519,6 @@
      * @see #setCompatVectorFromResourcesEnabled(boolean)
      */
     public static boolean isCompatVectorFromResourcesEnabled() {
-        return sCompatVectorFromResourcesEnabled;
+        return VectorEnabledTintResources.isCompatVectorFromResourcesEnabled();
     }
 }
diff --git a/v7/appcompat/src/main/java/androidx/appcompat/view/menu/MenuBuilder.java b/v7/appcompat/src/main/java/androidx/appcompat/view/menu/MenuBuilder.java
index a1a20e5..2b5e653 100644
--- a/v7/appcompat/src/main/java/androidx/appcompat/view/menu/MenuBuilder.java
+++ b/v7/appcompat/src/main/java/androidx/appcompat/view/menu/MenuBuilder.java
@@ -35,14 +35,15 @@
 import android.view.MenuItem;
 import android.view.SubMenu;
 import android.view.View;
+import android.view.ViewConfiguration;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
-import androidx.appcompat.R;
 import androidx.core.content.ContextCompat;
 import androidx.core.internal.view.SupportMenu;
 import androidx.core.internal.view.SupportMenuItem;
 import androidx.core.view.ActionProvider;
+import androidx.core.view.ViewConfigurationCompat;
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
@@ -816,7 +817,8 @@
     private void setShortcutsVisibleInner(boolean shortcutsVisible) {
         mShortcutsVisible = shortcutsVisible
                 && mResources.getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS
-                && mResources.getBoolean(R.bool.abc_config_showMenuShortcutsWhenKeyboardPresent);
+                && ViewConfigurationCompat.shouldShowMenuShortcutsWhenKeyboardPresent(
+                        ViewConfiguration.get(mContext), mContext);
     }
 
     /**
diff --git a/v7/appcompat/src/main/java/androidx/appcompat/widget/VectorEnabledTintResources.java b/v7/appcompat/src/main/java/androidx/appcompat/widget/VectorEnabledTintResources.java
index 15fcf88..af61860 100644
--- a/v7/appcompat/src/main/java/androidx/appcompat/widget/VectorEnabledTintResources.java
+++ b/v7/appcompat/src/main/java/androidx/appcompat/widget/VectorEnabledTintResources.java
@@ -25,7 +25,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
-import androidx.appcompat.app.AppCompatDelegate;
 
 import java.lang.ref.WeakReference;
 
@@ -37,9 +36,10 @@
  */
 @RestrictTo(LIBRARY_GROUP)
 public class VectorEnabledTintResources extends Resources {
+    private static boolean sCompatVectorFromResourcesEnabled = false;
 
     public static boolean shouldBeUsed() {
-        return AppCompatDelegate.isCompatVectorFromResourcesEnabled()
+        return isCompatVectorFromResourcesEnabled()
                 && Build.VERSION.SDK_INT <= MAX_SDK_WHERE_REQUIRED;
     }
 
@@ -74,4 +74,22 @@
     final Drawable superGetDrawable(int id) {
         return super.getDrawable(id);
     }
+
+    /**
+     * Sets whether vector drawables on older platforms (< API 21) can be used within
+     * {@link android.graphics.drawable.DrawableContainer} resources.
+     */
+    public static void setCompatVectorFromResourcesEnabled(boolean enabled) {
+        sCompatVectorFromResourcesEnabled = enabled;
+    }
+
+    /**
+     * Returns whether vector drawables on older platforms (< API 21) can be accessed from within
+     * resources.
+     *
+     * @see #setCompatVectorFromResourcesEnabled(boolean)
+     */
+    public static boolean isCompatVectorFromResourcesEnabled() {
+        return sCompatVectorFromResourcesEnabled;
+    }
 }
\ No newline at end of file
diff --git a/wear/res/values-as/strings.xml b/wear/res/values-as/strings.xml
new file mode 100644
index 0000000..2a51efa
--- /dev/null
+++ b/wear/res/values-as/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="ws_navigation_drawer_content_description" msgid="7216697245762194759">"নেভিগেশ্বন ড্ৰৱাৰ"</string>
+    <string name="ws_action_drawer_content_description" msgid="1837365417701148489">"কাৰ্য ড্ৰৱাৰ"</string>
+</resources>
diff --git a/wear/res/values-or/strings.xml b/wear/res/values-or/strings.xml
new file mode 100644
index 0000000..53e75b3
--- /dev/null
+++ b/wear/res/values-or/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="ws_navigation_drawer_content_description" msgid="7216697245762194759">"ନେଭିଗେଶନ୍ ପ୍ୟାନେଲ୍"</string>
+    <string name="ws_action_drawer_content_description" msgid="1837365417701148489">"କାର୍ଯ୍ୟକାରୀ ପ୍ୟାନେଲ୍"</string>
+</resources>
diff --git a/webkit-codegen/src/test/java/androidx/webkit/internal/codegen/BoundaryInterfaceTest.java b/webkit-codegen/src/test/java/androidx/webkit/internal/codegen/BoundaryInterfaceTest.java
index 00736a7..ba5a9de 100644
--- a/webkit-codegen/src/test/java/androidx/webkit/internal/codegen/BoundaryInterfaceTest.java
+++ b/webkit-codegen/src/test/java/androidx/webkit/internal/codegen/BoundaryInterfaceTest.java
@@ -18,6 +18,9 @@
 
 import static org.junit.Assert.assertEquals;
 
+import androidx.webkit.internal.codegen.representations.ClassRepr;
+import androidx.webkit.internal.codegen.representations.MethodRepr;
+
 import com.android.tools.lint.LintCoreProjectEnvironment;
 
 import com.intellij.psi.PsiClass;
@@ -27,15 +30,13 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
 import java.util.Arrays;
 
-import androidx.webkit.internal.codegen.representations.ClassRepr;
-import androidx.webkit.internal.codegen.representations.MethodRepr;
-
 @RunWith(JUnit4.class)
 public class BoundaryInterfaceTest {
     private LintCoreProjectEnvironment mProjectEnv;
@@ -58,10 +59,12 @@
         testBoundaryInterfaceGeneration("SingleClassAndMethod");
     }
 
+    @Ignore
     @Test public void testWebkitReturnTypeGeneratesInvocationHandler() {
         testBoundaryInterfaceGeneration("WebKitTypeAsMethodParameter");
     }
 
+    @Ignore
     @Test public void testWebkitMethodParameterTypeGeneratesInvocationHandler() {
         testBoundaryInterfaceGeneration("WebKitTypeAsMethodReturn");
     }
diff --git a/webkit/api/current.txt b/webkit/api/current.txt
new file mode 100644
index 0000000..e4e9b01
--- /dev/null
+++ b/webkit/api/current.txt
@@ -0,0 +1,118 @@
+package androidx.webkit {
+
+  public abstract class SafeBrowsingResponseCompat {
+    method public abstract void backToSafety(boolean);
+    method public abstract void proceed(boolean);
+    method public abstract void showInterstitial(boolean);
+  }
+
+  public abstract class ServiceWorkerClientCompat {
+    ctor public ServiceWorkerClientCompat();
+    method public abstract android.webkit.WebResourceResponse shouldInterceptRequest(android.webkit.WebResourceRequest);
+  }
+
+  public abstract class ServiceWorkerControllerCompat {
+    method public static androidx.webkit.ServiceWorkerControllerCompat getInstance();
+    method public abstract androidx.webkit.ServiceWorkerWebSettingsCompat getServiceWorkerWebSettings();
+    method public abstract void setServiceWorkerClient(androidx.webkit.ServiceWorkerClientCompat);
+  }
+
+  public abstract class ServiceWorkerWebSettingsCompat {
+    method public abstract boolean getAllowContentAccess();
+    method public abstract boolean getAllowFileAccess();
+    method public abstract boolean getBlockNetworkLoads();
+    method public abstract int getCacheMode();
+    method public abstract void setAllowContentAccess(boolean);
+    method public abstract void setAllowFileAccess(boolean);
+    method public abstract void setBlockNetworkLoads(boolean);
+    method public abstract void setCacheMode(int);
+  }
+
+  public class WebMessageCompat {
+    ctor public WebMessageCompat(java.lang.String);
+    ctor public WebMessageCompat(java.lang.String, androidx.webkit.WebMessagePortCompat[]);
+    method public java.lang.String getData();
+    method public androidx.webkit.WebMessagePortCompat[] getPorts();
+  }
+
+  public abstract class WebMessagePortCompat {
+    method public abstract void close();
+    method public abstract void postMessage(androidx.webkit.WebMessageCompat);
+    method public abstract void setWebMessageCallback(androidx.webkit.WebMessagePortCompat.WebMessageCallbackCompat);
+    method public abstract void setWebMessageCallback(android.os.Handler, androidx.webkit.WebMessagePortCompat.WebMessageCallbackCompat);
+  }
+
+  public static abstract class WebMessagePortCompat.WebMessageCallbackCompat {
+    ctor public WebMessagePortCompat.WebMessageCallbackCompat();
+    method public void onMessage(androidx.webkit.WebMessagePortCompat, androidx.webkit.WebMessageCompat);
+  }
+
+  public abstract class WebResourceErrorCompat {
+    method public abstract java.lang.CharSequence getDescription();
+    method public abstract int getErrorCode();
+  }
+
+  public class WebResourceRequestCompat {
+    method public static boolean isRedirect(android.webkit.WebResourceRequest);
+  }
+
+  public class WebSettingsCompat {
+    method public static int getDisabledActionModeMenuItems(android.webkit.WebSettings);
+    method public static boolean getOffscreenPreRaster(android.webkit.WebSettings);
+    method public static boolean getSafeBrowsingEnabled(android.webkit.WebSettings);
+    method public static void setDisabledActionModeMenuItems(android.webkit.WebSettings, int);
+    method public static void setOffscreenPreRaster(android.webkit.WebSettings, boolean);
+    method public static void setSafeBrowsingEnabled(android.webkit.WebSettings, boolean);
+  }
+
+  public class WebViewClientCompat extends android.webkit.WebViewClient {
+    ctor public WebViewClientCompat();
+    method public final void onReceivedError(android.webkit.WebView, android.webkit.WebResourceRequest, android.webkit.WebResourceError);
+    method public void onReceivedError(android.webkit.WebView, android.webkit.WebResourceRequest, androidx.webkit.WebResourceErrorCompat);
+    method public final void onSafeBrowsingHit(android.webkit.WebView, android.webkit.WebResourceRequest, int, android.webkit.SafeBrowsingResponse);
+    method public void onSafeBrowsingHit(android.webkit.WebView, android.webkit.WebResourceRequest, int, androidx.webkit.SafeBrowsingResponseCompat);
+  }
+
+  public class WebViewCompat {
+    method public static androidx.webkit.WebMessagePortCompat[] createWebMessageChannel(android.webkit.WebView);
+    method public static android.content.pm.PackageInfo getCurrentWebViewPackage(android.content.Context);
+    method public static android.net.Uri getSafeBrowsingPrivacyPolicyUrl();
+    method public static void postVisualStateCallback(android.webkit.WebView, long, androidx.webkit.WebViewCompat.VisualStateCallback);
+    method public static void postWebMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri);
+    method public static void setSafeBrowsingWhitelist(java.util.List<java.lang.String>, android.webkit.ValueCallback<java.lang.Boolean>);
+    method public static void startSafeBrowsing(android.content.Context, android.webkit.ValueCallback<java.lang.Boolean>);
+  }
+
+  public static abstract interface WebViewCompat.VisualStateCallback {
+    method public abstract void onComplete(long);
+  }
+
+  public class WebViewFeature {
+    method public static boolean isFeatureSupported(java.lang.String);
+    field public static final java.lang.String DISABLED_ACTION_MODE_MENU_ITEMS = "DISABLED_ACTION_MODE_MENU_ITEMS";
+    field public static final java.lang.String OFF_SCREEN_PRERASTER = "OFF_SCREEN_PRERASTER";
+    field public static final java.lang.String RECEIVE_HTTP_ERROR = "RECEIVE_HTTP_ERROR";
+    field public static final java.lang.String RECEIVE_WEB_RESOURCE_ERROR = "RECEIVE_WEB_RESOURCE_ERROR";
+    field public static final java.lang.String SAFE_BROWSING_ENABLE = "SAFE_BROWSING_ENABLE";
+    field public static final java.lang.String SAFE_BROWSING_HIT = "SAFE_BROWSING_HIT";
+    field public static final java.lang.String SAFE_BROWSING_PRIVACY_POLICY_URL = "SAFE_BROWSING_PRIVACY_POLICY_URL";
+    field public static final java.lang.String SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY = "SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY";
+    field public static final java.lang.String SAFE_BROWSING_RESPONSE_PROCEED = "SAFE_BROWSING_RESPONSE_PROCEED";
+    field public static final java.lang.String SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL = "SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL";
+    field public static final java.lang.String SAFE_BROWSING_WHITELIST = "SAFE_BROWSING_WHITELIST";
+    field public static final java.lang.String SERVICE_WORKER_BASIC_USAGE = "SERVICE_WORKER_BASIC_USAGE";
+    field public static final java.lang.String SERVICE_WORKER_BLOCK_NETWORK_LOADS = "SERVICE_WORKER_BLOCK_NETWORK_LOADS";
+    field public static final java.lang.String SERVICE_WORKER_CACHE_MODE = "SERVICE_WORKER_CACHE_MODE";
+    field public static final java.lang.String SERVICE_WORKER_CONTENT_ACCESS = "SERVICE_WORKER_CONTENT_ACCESS";
+    field public static final java.lang.String SERVICE_WORKER_FILE_ACCESS = "SERVICE_WORKER_FILE_ACCESS";
+    field public static final java.lang.String SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST = "SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST";
+    field public static final java.lang.String SHOULD_OVERRIDE_WITH_REDIRECTS = "SHOULD_OVERRIDE_WITH_REDIRECTS";
+    field public static final java.lang.String START_SAFE_BROWSING = "START_SAFE_BROWSING";
+    field public static final java.lang.String VISUAL_STATE_CALLBACK = "VISUAL_STATE_CALLBACK";
+    field public static final java.lang.String WEB_RESOURCE_ERROR_GET_CODE = "WEB_RESOURCE_ERROR_GET_CODE";
+    field public static final java.lang.String WEB_RESOURCE_ERROR_GET_DESCRIPTION = "WEB_RESOURCE_ERROR_GET_DESCRIPTION";
+    field public static final java.lang.String WEB_RESOURCE_REQUEST_IS_REDIRECT = "WEB_RESOURCE_REQUEST_IS_REDIRECT";
+  }
+
+}
+
diff --git a/webkit/build.gradle b/webkit/build.gradle
index 06f3801..baed9cb 100644
--- a/webkit/build.gradle
+++ b/webkit/build.gradle
@@ -38,14 +38,17 @@
         // Allow compiling the WebView support library boundary interfaces from this project.
         main.java.srcDirs += new File(webviewBoundaryInterfacesDir, "src").getAbsolutePath()
     }
+
+    buildTypes.all {
+        consumerProguardFiles new File(webviewBoundaryInterfacesDir, "proguard.flags") , 'proguard-rules.pro'
+    }
 }
 
 supportLibrary {
     name = "WebView Support Library"
-    publish = false
+    publish = true
     mavenVersion = LibraryVersions.SUPPORT_LIBRARY
     mavenGroup = LibraryGroups.WEBKIT
     inceptionYear = "2017"
     description = "The WebView Support Library is a static library you can add to your Android application in order to use android.webkit APIs that are not available for older platform versions."
-    minSdkVersion = 21
 }
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/androidTest/java/androidx/webkit/BoundaryInterfaceTest.java b/webkit/src/androidTest/java/androidx/webkit/BoundaryInterfaceTest.java
new file mode 100644
index 0000000..3f9857c
--- /dev/null
+++ b/webkit/src/androidTest/java/androidx/webkit/BoundaryInterfaceTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.webkit.internal.WebViewFeatureInternal;
+import androidx.webkit.internal.WebViewGlueCommunicator;
+import androidx.webkit.internal.WebkitToCompatConverter;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests related to the interface between the support library and the Chromium support library glue.
+ */
+@RunWith(AndroidJUnit4.class)
+public class BoundaryInterfaceTest {
+
+    /**
+     * Test ensuring that we can create a {@link androidx.webkit.internal.WebkitToCompatConverter}.
+     * This test catches cases where we try to pass post-L android.webkit classes across the
+     * boundary - doing so will fail when we (at run-time) create a {@link java.lang.reflect.Proxy}
+     * for {@link org.chromium.support_lib_boundary.WebkitToCompatConverterBoundaryInterface} since
+     * that proxy will need to look up all the classes referenced from
+     * {@link org.chromium.support_lib_boundary.WebkitToCompatConverterBoundaryInterface}.
+     */
+    @SmallTest
+    @Test
+    public void testCreateWebkitToCompatConverter() {
+        // Use the SERVICE_WORKER_BASIC_USAGE feature as a proxy for knowing whether the current
+        // WebView APK is compatible with the support library.
+        if (WebViewFeatureInternal.SERVICE_WORKER_BASIC_USAGE.isSupportedByWebView()) {
+            WebkitToCompatConverter converter = WebViewGlueCommunicator.getCompatConverter();
+        }
+    }
+
+}
diff --git a/webkit/src/androidTest/java/androidx/webkit/IncompatibilityTest.java b/webkit/src/androidTest/java/androidx/webkit/IncompatibilityTest.java
new file mode 100644
index 0000000..09424c6
--- /dev/null
+++ b/webkit/src/androidTest/java/androidx/webkit/IncompatibilityTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.webkit.internal.WebViewFeatureInternal;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests ensuring that Android versions/setups that are incompatible with the WebView Support
+ * Library are handled gracefully.
+ *
+ * Only L+ Android versions are compatible with the WebView Support Library, so any tests in this
+ * class that guarantee certain behaviour for incompatible Android versions will only be run on
+ * pre-L devices.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class IncompatibilityTest {
+    @Test
+    @SdkSuppress(maxSdkVersion = 20)
+    public void testPreLDeviceHasNoWebViewFeatures() {
+        assertEquals(0, WebViewFeatureInternal.getWebViewApkFeaturesForTesting().length);
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 20)
+    public void testPreLDeviceDoesNotSupportVisualStateCallback() {
+        assertFalse(WebViewFeature.isFeatureSupported(WebViewFeature.VISUAL_STATE_CALLBACK));
+    }
+}
diff --git a/webkit/src/androidTest/java/androidx/webkit/PollingCheck.java b/webkit/src/androidTest/java/androidx/webkit/PollingCheck.java
new file mode 100644
index 0000000..4c59a3e
--- /dev/null
+++ b/webkit/src/androidTest/java/androidx/webkit/PollingCheck.java
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+
+package androidx.webkit;
+
+import junit.framework.Assert;
+
+import java.util.concurrent.Callable;
+
+/**
+ * A class for checking a specific statement {@link #check()} through polling, either until the
+ * statement is true, or until timing out.
+ *
+ * Copy-pasted from CTS: com.android.compatibility.common.util.PollingCheck.
+ */
+public abstract class PollingCheck {
+    private static final long TIME_SLICE = 50;
+    private long mTimeout = 3000;
+
+    public PollingCheck(long timeout) {
+        mTimeout = timeout;
+    }
+
+    protected abstract boolean check();
+
+    public void run() {
+        if (check()) {
+            return;
+        }
+
+        long timeout = mTimeout;
+        while (timeout > 0) {
+            try {
+                Thread.sleep(TIME_SLICE);
+            } catch (InterruptedException e) {
+                Assert.fail("unexpected InterruptedException");
+            }
+
+            if (check()) {
+                return;
+            }
+
+            timeout -= TIME_SLICE;
+        }
+
+        Assert.fail("unexpected timeout");
+    }
+
+    public static void check(CharSequence message, long timeout, Callable<Boolean> condition)
+            throws Exception {
+        while (timeout > 0) {
+            if (condition.call()) {
+                return;
+            }
+
+            Thread.sleep(TIME_SLICE);
+            timeout -= TIME_SLICE;
+        }
+
+        Assert.fail(message.toString());
+    }
+}
diff --git a/webkit/src/androidTest/java/androidx/webkit/PostMessageTest.java b/webkit/src/androidTest/java/androidx/webkit/PostMessageTest.java
new file mode 100644
index 0000000..4e74419
--- /dev/null
+++ b/webkit/src/androidTest/java/androidx/webkit/PostMessageTest.java
@@ -0,0 +1,251 @@
+/*
+ * 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.
+ */
+
+package androidx.webkit;
+
+import static junit.framework.Assert.assertEquals;
+
+import android.net.Uri;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.runner.AndroidJUnit4;
+import android.webkit.WebView;
+
+import androidx.webkit.WebMessagePortCompat.WebMessageCallbackCompat;
+
+import junit.framework.Assert;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class PostMessageTest {
+    public static final long TIMEOUT = 6000L;
+
+    private WebView mWebView;
+    private WebViewOnUiThread mOnUiThread;
+
+    private static final String WEBVIEW_MESSAGE = "from_webview";
+    private static final String BASE_URI = "http://www.example.com";
+
+    @Before
+    public void setUp() throws Exception {
+        mOnUiThread = new WebViewOnUiThread();
+        mOnUiThread.getSettings().setJavaScriptEnabled(true);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mOnUiThread != null) {
+            mOnUiThread.cleanUp();
+        }
+    }
+
+    private static final String TITLE_FROM_POST_MESSAGE =
+            "<!DOCTYPE html><html><body>"
+                    + "    <script>"
+                    + "        var received = '';"
+                    + "        onmessage = function (e) {"
+                    + "            received += e.data;"
+                    + "            document.title = received; };"
+                    + "    </script>"
+                    + "</body></html>";
+
+    // Acks each received message from the message channel with a seq number.
+    private static final String CHANNEL_MESSAGE =
+            "<!DOCTYPE html><html><body>"
+                    + "    <script>"
+                    + "        var counter = 0;"
+                    + "        onmessage = function (e) {"
+                    + "            var myPort = e.ports[0];"
+                    + "            myPort.onmessage = function (f) {"
+                    + "                myPort.postMessage(f.data + counter++);"
+                    + "            }"
+                    + "        }"
+                    + "   </script>"
+                    + "</body></html>";
+
+    private void loadPage(String data) {
+        mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(BASE_URI, data,
+                "text/html", "UTF-8", null);
+    }
+
+    private void waitForTitle(final String title) {
+        new PollingCheck(TIMEOUT) {
+            @Override
+            protected boolean check() {
+                return mOnUiThread.getTitle().equals(title);
+            }
+        }.run();
+    }
+
+    // Post a string message to main frame and make sure it is received.
+    @Test
+    @SdkSuppress(minSdkVersion = 23) // TODO(gsennton) activate this test for pre-M devices when we
+    // can pre-install a WebView APK containing support for the WebView Support Library, see
+    // b/73454652.
+    public void testSimpleMessageToMainFrame() throws Throwable {
+        verifyPostMessageToOrigin(Uri.parse(BASE_URI));
+    }
+
+    // Post a string message to main frame passing a wildcard as target origin
+    @Test
+    @SdkSuppress(minSdkVersion = 23) // TODO (gsennton) remove this restriction when we can
+    // pre-install a WebView APK containing support for the WebView Support Library, see b/73454652.
+    public void testWildcardOriginMatchesAnything() throws Throwable {
+        verifyPostMessageToOrigin(Uri.parse("*"));
+    }
+
+    // Post a string message to main frame passing an empty string as target origin
+    @Test
+    @SdkSuppress(minSdkVersion = 23) // TODO(gsennton) activate this test for pre-M devices when we
+    // can pre-install a WebView APK containing support for the WebView Support Library, see
+    // b/73454652.
+    public void testEmptyStringOriginMatchesAnything() throws Throwable {
+        verifyPostMessageToOrigin(Uri.parse(""));
+    }
+
+    private void verifyPostMessageToOrigin(Uri origin) throws Throwable {
+        loadPage(TITLE_FROM_POST_MESSAGE);
+        WebMessageCompat message = new WebMessageCompat(WEBVIEW_MESSAGE);
+        mOnUiThread.postWebMessageCompat(message, origin);
+        waitForTitle(WEBVIEW_MESSAGE);
+    }
+
+    // Post multiple messages to main frame and make sure they are received in
+    // correct order.
+    @Test
+    @SdkSuppress(minSdkVersion = 23) // TODO(gsennton) activate this test for pre-M devices when we
+    // can pre-install a WebView APK containing support for the WebView Support Library, see
+    // b/73454652.
+    public void testMultipleMessagesToMainFrame() throws Throwable {
+        loadPage(TITLE_FROM_POST_MESSAGE);
+        for (int i = 0; i < 10; i++) {
+            mOnUiThread.postWebMessageCompat(new WebMessageCompat(Integer.toString(i)),
+                    Uri.parse(BASE_URI));
+        }
+        waitForTitle("0123456789");
+    }
+
+    // Create a message channel and make sure it can be used for data transfer to/from js.
+    @Test
+    @SdkSuppress(minSdkVersion = 23) // TODO(gsennton) activate this test for pre-M devices when we
+    // can pre-install a WebView APK containing support for the WebView Support Library, see
+    // b/73454652.
+    public void testMessageChannel() throws Throwable {
+        loadPage(CHANNEL_MESSAGE);
+        final WebMessagePortCompat[] channel = mOnUiThread.createWebMessageChannelCompat();
+        WebMessageCompat message =
+                new WebMessageCompat(WEBVIEW_MESSAGE, new WebMessagePortCompat[]{channel[1]});
+        mOnUiThread.postWebMessageCompat(message, Uri.parse(BASE_URI));
+        final int messageCount = 3;
+        final CountDownLatch latch = new CountDownLatch(messageCount);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                for (int i = 0; i < messageCount; i++) {
+                    channel[0].postMessage(new WebMessageCompat(WEBVIEW_MESSAGE + i));
+                }
+                channel[0].setWebMessageCallback(new WebMessageCallbackCompat() {
+                    @Override
+                    public void onMessage(WebMessagePortCompat port, WebMessageCompat message) {
+                        int i = messageCount - (int) latch.getCount();
+                        assertEquals(WEBVIEW_MESSAGE + i + i, message.getData());
+                        latch.countDown();
+                    }
+                });
+            }
+        });
+        // Wait for all the responses to arrive.
+        boolean ignore = latch.await(TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
+    }
+
+    // Test that a message port that is closed cannot used to send a message
+    @Test
+    @SdkSuppress(minSdkVersion = 23) // TODO(gsennton) activate this test for pre-M devices when we
+    // can pre-install a WebView APK containing support for the WebView Support Library, see
+    // b/73454652.
+    public void testClose() throws Throwable {
+        loadPage(CHANNEL_MESSAGE);
+        final WebMessagePortCompat[] channel = mOnUiThread.createWebMessageChannelCompat();
+        WebMessageCompat message =
+                new WebMessageCompat(WEBVIEW_MESSAGE, new WebMessagePortCompat[]{channel[1]});
+        mOnUiThread.postWebMessageCompat(message, Uri.parse(BASE_URI));
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    channel[0].close();
+                    channel[0].postMessage(new WebMessageCompat(WEBVIEW_MESSAGE));
+                } catch (IllegalStateException ex) {
+                    // expect to receive an exception
+                    return;
+                }
+                Assert.fail("A closed port cannot be used to transfer messages");
+            }
+        });
+    }
+
+    // Sends a new message channel from JS to Java.
+    private static final String CHANNEL_FROM_JS =
+            "<!DOCTYPE html><html><body>"
+                    + "    <script>"
+                    + "        var counter = 0;"
+                    + "        var mc = new MessageChannel();"
+                    + "        var received = '';"
+                    + "        mc.port1.onmessage = function (e) {"
+                    + "               received = e.data;"
+                    + "               document.title = e.data;"
+                    + "        };"
+                    + "        onmessage = function (e) {"
+                    + "            var myPort = e.ports[0];"
+                    + "            myPort.postMessage('', [mc.port2]);"
+                    + "        };"
+                    + "   </script>"
+                    + "</body></html>";
+
+    // Test a message port created in JS can be received and used for message transfer.
+    @Test
+    @SdkSuppress(minSdkVersion = 23) // TODO(gsennton) activate this test for pre-M devices when we
+    // can pre-install a WebView APK containing support for the WebView Support Library, see
+    // b/73454652.
+    public void testReceiveMessagePort() throws Throwable {
+        final String hello = "HELLO";
+        loadPage(CHANNEL_FROM_JS);
+        final WebMessagePortCompat[] channel = mOnUiThread.createWebMessageChannelCompat();
+        WebMessageCompat message =
+                new WebMessageCompat(WEBVIEW_MESSAGE, new WebMessagePortCompat[]{channel[1]});
+        mOnUiThread.postWebMessageCompat(message, Uri.parse(BASE_URI));
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                channel[0].setWebMessageCallback(new WebMessageCallbackCompat() {
+                    @Override
+                    public void onMessage(WebMessagePortCompat port, WebMessageCompat message) {
+                        message.getPorts()[0].postMessage(new WebMessageCompat(hello));
+                    }
+                });
+            }
+        });
+        waitForTitle(hello);
+    }
+}
diff --git a/webkit/src/androidTest/java/androidx/webkit/ServiceWorkerClientCompatTest.java b/webkit/src/androidTest/java/androidx/webkit/ServiceWorkerClientCompatTest.java
new file mode 100644
index 0000000..d3a55ae
--- /dev/null
+++ b/webkit/src/androidTest/java/androidx/webkit/ServiceWorkerClientCompatTest.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.webkit.JavascriptInterface;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebView;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class ServiceWorkerClientCompatTest {
+
+    // The BASE_URL does not matter since the tests will intercept the load, but it should be https
+    // for the Service Worker registration to succeed.
+    private static final String BASE_URL = "https://www.example.com/";
+    private static final String INDEX_URL = BASE_URL + "index.html";
+    private static final String SW_URL = BASE_URL + "sw.js";
+    private static final String FETCH_URL = BASE_URL + "fetch.html";
+
+    private static final String JS_INTERFACE_NAME = "Android";
+    private static final int POLLING_TIMEOUT = 10 * 1000;
+
+    // static HTML page always injected instead of the url loaded.
+    private static final String INDEX_RAW_HTML =
+            "<!DOCTYPE html>\n"
+                    + "<html>\n"
+                    + "  <body>\n"
+                    + "    <script>\n"
+                    + "      navigator.serviceWorker.register('sw.js').then(function(reg) {\n"
+                    + "         " + JS_INTERFACE_NAME + ".registrationSuccess();\n"
+                    + "      }).catch(function(err) {\n"
+                    + "         console.error(err);\n"
+                    + "      });\n"
+                    + "    </script>\n"
+                    + "  </body>\n"
+                    + "</html>\n";
+    private static final String SW_RAW_HTML = "fetch('fetch.html');";
+    private static final String SW_UNREGISTER_RAW_JS =
+            "navigator.serviceWorker.getRegistration().then(function(r) {"
+                    + "  r.unregister().then(function(success) {"
+                    + "    if (success) " + JS_INTERFACE_NAME + ".unregisterSuccess();"
+                    + "    else console.error('unregister() was not successful');"
+                    + "  });"
+                    + "}).catch(function(err) {"
+                    + "   console.error(err);"
+                    + "});";
+
+    private JavascriptStatusReceiver mJavascriptStatusReceiver;
+    private WebViewOnUiThread mOnUiThread;
+
+    // Both this test and WebViewOnUiThread need to override some of the methods on WebViewClient,
+    // so this test subclasses the WebViewClient from WebViewOnUiThread.
+    private static class InterceptClient extends WebViewOnUiThread.WaitForLoadedClient {
+
+        InterceptClient(WebViewOnUiThread webViewOnUiThread) throws Exception {
+            super(webViewOnUiThread);
+        }
+
+        @Override
+        public WebResourceResponse shouldInterceptRequest(WebView view,
+                WebResourceRequest request) {
+            // Only return content for INDEX_URL, deny all other requests.
+            try {
+                if (request.getUrl().toString().equals(INDEX_URL)) {
+                    return new WebResourceResponse("text/html", "utf-8",
+                            new ByteArrayInputStream(INDEX_RAW_HTML.getBytes("UTF-8")));
+                }
+            } catch (java.io.UnsupportedEncodingException e) { }
+            return new WebResourceResponse("text/html", "UTF-8", null);
+        }
+    }
+
+    public static class InterceptServiceWorkerClient extends ServiceWorkerClientCompat {
+        private List<WebResourceRequest> mInterceptedRequests = new ArrayList<WebResourceRequest>();
+
+        @Override
+        public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
+            // Records intercepted requests and only return content for SW_URL.
+            mInterceptedRequests.add(request);
+            try {
+                if (request.getUrl().toString().equals(SW_URL)) {
+                    return new WebResourceResponse("application/javascript", "utf-8",
+                            new ByteArrayInputStream(SW_RAW_HTML.getBytes("UTF-8")));
+                }
+            } catch (java.io.UnsupportedEncodingException e) { }
+            return new WebResourceResponse("text/html", "UTF-8", null);
+        }
+
+        List<WebResourceRequest> getInterceptedRequests() {
+            return mInterceptedRequests;
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mOnUiThread = new WebViewOnUiThread();
+        mOnUiThread.getSettings().setJavaScriptEnabled(true);
+
+        mJavascriptStatusReceiver = new JavascriptStatusReceiver();
+        mOnUiThread.addJavascriptInterface(mJavascriptStatusReceiver, JS_INTERFACE_NAME);
+        mOnUiThread.setWebViewClient(new InterceptClient(mOnUiThread));
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mOnUiThread != null) {
+            mOnUiThread.cleanUp();
+        }
+    }
+
+    // Test correct invocation of shouldInterceptRequest for Service Workers.
+    @Test
+    public void testServiceWorkerClientInterceptCallback() throws Exception {
+        assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_BASIC_USAGE));
+        assumeTrue(WebViewFeature.isFeatureSupported(
+                WebViewFeature.SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST));
+
+        final InterceptServiceWorkerClient mInterceptServiceWorkerClient =
+                new InterceptServiceWorkerClient();
+        ServiceWorkerControllerCompat swController = ServiceWorkerControllerCompat.getInstance();
+        swController.setServiceWorkerClient(mInterceptServiceWorkerClient);
+
+        mOnUiThread.loadUrlAndWaitForCompletion(INDEX_URL);
+
+        Callable<Boolean> registrationSuccess = new Callable<Boolean>() {
+            @Override
+            public Boolean call() {
+                return mJavascriptStatusReceiver.mRegistrationSuccess;
+            }
+        };
+        PollingCheck.check("JS could not register Service Worker", POLLING_TIMEOUT,
+                registrationSuccess);
+
+        Callable<Boolean> receivedRequest = new Callable<Boolean>() {
+            @Override
+            public Boolean call() {
+                return mInterceptServiceWorkerClient.getInterceptedRequests().size() >= 2;
+            }
+        };
+        PollingCheck.check("Service Worker intercept callbacks not invoked", POLLING_TIMEOUT,
+                receivedRequest);
+
+        List<WebResourceRequest> requests = mInterceptServiceWorkerClient.getInterceptedRequests();
+        assertEquals(2, requests.size());
+        assertEquals(SW_URL, requests.get(0).getUrl().toString());
+        assertEquals(FETCH_URL, requests.get(1).getUrl().toString());
+
+        // Clean-up, make sure to unregister the Service Worker.
+        mOnUiThread.evaluateJavascript(SW_UNREGISTER_RAW_JS, null);
+        Callable<Boolean> unregisterSuccess = new Callable<Boolean>() {
+            @Override
+            public Boolean call() {
+                return mJavascriptStatusReceiver.mUnregisterSuccess;
+            }
+        };
+        PollingCheck.check("JS could not unregister Service Worker", POLLING_TIMEOUT,
+                unregisterSuccess);
+    }
+
+    // Object added to the page via AddJavascriptInterface() that is used by the test Javascript to
+    // notify back to Java if the Service Worker registration was successful.
+    public static final class JavascriptStatusReceiver {
+        public volatile boolean mRegistrationSuccess = false;
+        public volatile boolean mUnregisterSuccess = false;
+
+        @JavascriptInterface
+        public void registrationSuccess() {
+            mRegistrationSuccess = true;
+        }
+
+        @JavascriptInterface
+        public void unregisterSuccess() {
+            mUnregisterSuccess = true;
+        }
+    }
+}
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatTest.java b/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatTest.java
index 1e5c152..5dd9b54 100644
--- a/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatTest.java
+++ b/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatTest.java
@@ -19,8 +19,8 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 
-import android.os.Build;
 import android.support.test.filters.MediumTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.webkit.WebSettings;
@@ -41,9 +41,7 @@
 
     @Test
     public void testOffscreenPreRaster() {
-        // TODO(gsennton) activate this test for pre-M devices when we can pre-install a WebView APK
-        // containing support for the WebView Support Library, see b/73454652.
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return;
+        assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.OFF_SCREEN_PRERASTER));
 
         assertFalse(WebSettingsCompat.getOffscreenPreRaster(mWebViewOnUiThread.getSettings()));
 
@@ -53,9 +51,7 @@
 
     @Test
     public void testEnableSafeBrowsing() throws Throwable {
-        // TODO(gsennton) activate this test for old devices when we can pre-install a WebView APK
-        // containing support for the WebView Support Library, see b/73454652.
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
+        assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE));
 
         WebSettingsCompat.setSafeBrowsingEnabled(mWebViewOnUiThread.getSettings(), false);
         assertFalse(WebSettingsCompat.getSafeBrowsingEnabled(mWebViewOnUiThread.getSettings()));
@@ -63,9 +59,8 @@
 
     @Test
     public void testDisabledActionModeMenuItems() throws Throwable {
-        // TODO(gsennton) activate this test for old devices when we can pre-install a WebView APK
-        // containing support for the WebView Support Library, see b/73454652.
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return;
+        assumeTrue(
+                WebViewFeature.isFeatureSupported(WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS));
 
         assertEquals(WebSettings.MENU_ITEM_NONE,
                 WebSettingsCompat.getDisabledActionModeMenuItems(mWebViewOnUiThread.getSettings()));
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebViewClientCompatTest.java b/webkit/src/androidTest/java/androidx/webkit/WebViewClientCompatTest.java
new file mode 100644
index 0000000..1344f4b
--- /dev/null
+++ b/webkit/src/androidTest/java/androidx/webkit/WebViewClientCompatTest.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.webkit.ValueCallback;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class WebViewClientCompatTest {
+    private WebViewOnUiThread mWebViewOnUiThread;
+
+    private static final long TEST_TIMEOUT = 20000L;
+    private static final String TEST_URL = "http://www.example.com/";
+    private static final String TEST_SAFE_BROWSING_URL =
+            "chrome://safe-browsing/match?type=malware";
+
+    @Before
+    public void setUp() {
+        mWebViewOnUiThread = new WebViewOnUiThread();
+    }
+
+    @Test
+    public void testShouldOverrideUrlLoadingDefault() {
+        // This never calls into chromium, so we don't need to do any feature checks.
+
+        final MockWebViewClient webViewClient = new MockWebViewClient();
+
+        // Create any valid WebResourceRequest, the return values don't matter much.
+        final WebResourceRequest resourceRequest = new WebResourceRequest() {
+            @Override
+            public Uri getUrl() {
+                return Uri.parse(TEST_URL);
+            }
+
+            @Override
+            public boolean isForMainFrame() {
+                return false;
+            }
+
+            @Override
+            public boolean isRedirect() {
+                return false;
+            }
+
+            @Override
+            public boolean hasGesture() {
+                return false;
+            }
+
+            @Override
+            public String getMethod() {
+                return "GET";
+            }
+
+            @Override
+            public Map<String, String> getRequestHeaders() {
+                return new HashMap<String, String>();
+            }
+        };
+
+        Assert.assertFalse(webViewClient.shouldOverrideUrlLoading(
+                mWebViewOnUiThread.getWebViewOnCurrentThread(), resourceRequest));
+    }
+
+    @Test
+    public void testShouldOverrideUrlLoading() throws InterruptedException {
+        Assume.assumeTrue(
+                WebViewFeature.isFeatureSupported(WebViewFeature.SHOULD_OVERRIDE_WITH_REDIRECTS));
+        Assume.assumeTrue(
+                WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT));
+
+        String data = "<html><body>"
+                + "<a href=\"" + TEST_URL + "\" id=\"link\">new page</a>"
+                + "</body></html>";
+        mWebViewOnUiThread.loadDataAndWaitForCompletion(data, "text/html", null);
+        final CountDownLatch pageFinishedLatch = new CountDownLatch(1);
+        final MockWebViewClient webViewClient = new MockWebViewClient() {
+            @Override
+            public void onPageFinished(WebView view, String url) {
+                super.onPageFinished(view, url);
+                pageFinishedLatch.countDown();
+            }
+        };
+        mWebViewOnUiThread.setWebViewClient(webViewClient);
+        mWebViewOnUiThread.getSettings().setJavaScriptEnabled(true);
+        clickOnLinkUsingJs("link", mWebViewOnUiThread);
+        pageFinishedLatch.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+        Assert.assertEquals(TEST_URL,
+                webViewClient.getLastShouldOverrideResourceRequest().getUrl().toString());
+
+        WebResourceRequest request = webViewClient.getLastShouldOverrideResourceRequest();
+        Assert.assertNotNull(request);
+        Assert.assertTrue(request.isForMainFrame());
+        Assert.assertFalse(WebResourceRequestCompat.isRedirect(request));
+        Assert.assertFalse(request.hasGesture());
+    }
+
+    private void clickOnLinkUsingJs(final String linkId, WebViewOnUiThread webViewOnUiThread)
+            throws InterruptedException {
+        final CountDownLatch callbackLatch = new CountDownLatch(1);
+        ValueCallback<String> callback = new ValueCallback<String>() {
+            @Override
+            public void onReceiveValue(String value) {
+                callbackLatch.countDown();
+            }
+        };
+        webViewOnUiThread.evaluateJavascript(
+                "document.getElementById('" + linkId + "').click();"
+                        + "console.log('element with id [" + linkId + "] clicked');", callback);
+        Assert.assertTrue(callbackLatch.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testOnReceivedError() throws Exception {
+        Assume.assumeTrue(
+                WebViewFeature.isFeatureSupported(WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR));
+        Assume.assumeTrue(
+                WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE));
+
+        final MockWebViewClient webViewClient = new MockWebViewClient();
+        mWebViewOnUiThread.setWebViewClient(webViewClient);
+
+        String wrongUri = "invalidscheme://some/resource";
+        Assert.assertNull(webViewClient.getOnReceivedResourceError());
+        mWebViewOnUiThread.loadUrlAndWaitForCompletion(wrongUri);
+        Assert.assertNotNull(webViewClient.getOnReceivedResourceError());
+        Assert.assertEquals(WebViewClient.ERROR_UNSUPPORTED_SCHEME,
+                webViewClient.getOnReceivedResourceError().getErrorCode());
+    }
+
+    @Test
+    public void testOnReceivedErrorForSubresource() throws Exception {
+        Assume.assumeTrue(
+                WebViewFeature.isFeatureSupported(WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR));
+
+        final MockWebViewClient webViewClient = new MockWebViewClient();
+        mWebViewOnUiThread.setWebViewClient(webViewClient);
+
+        Assert.assertNull(webViewClient.getOnReceivedResourceError());
+        String data = "<html>"
+                + "  <body>"
+                + "    <img src=\"invalidscheme://some/resource\" />"
+                + "  </body>"
+                + "</html>";
+
+        mWebViewOnUiThread.loadDataAndWaitForCompletion(data, "text/html", null);
+        Assert.assertNotNull(webViewClient.getOnReceivedResourceError());
+        Assert.assertEquals(WebViewClient.ERROR_UNSUPPORTED_SCHEME,
+                webViewClient.getOnReceivedResourceError().getErrorCode());
+    }
+
+    @Test
+    public void testOnSafeBrowsingHitBackToSafety() throws Throwable {
+        Assume.assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_HIT));
+        Assume.assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE));
+        Assume.assumeTrue(WebViewFeature.isFeatureSupported(
+                WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY));
+        Assume.assumeTrue(
+                WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE));
+
+        final SafeBrowsingBackToSafetyClient backToSafetyWebViewClient =
+                new SafeBrowsingBackToSafetyClient();
+        mWebViewOnUiThread.setWebViewClient(backToSafetyWebViewClient);
+        WebSettingsCompat.setSafeBrowsingEnabled(mWebViewOnUiThread.getSettings(), true);
+
+        // Load any page
+        String data = "<html><body>some safe page</body></html>";
+        mWebViewOnUiThread.loadDataAndWaitForCompletion(data, "text/html", null);
+        final String originalUrl = mWebViewOnUiThread.getUrl();
+
+        enableSafeBrowsingAndLoadUnsafePage(backToSafetyWebViewClient);
+
+        // Back to safety should produce a network error
+        Assert.assertNotNull(backToSafetyWebViewClient.getOnReceivedResourceError());
+        Assert.assertEquals(WebViewClient.ERROR_UNSAFE_RESOURCE,
+                backToSafetyWebViewClient.getOnReceivedResourceError().getErrorCode());
+
+        // Check that we actually navigated backward
+        Assert.assertEquals(originalUrl, mWebViewOnUiThread.getUrl());
+    }
+
+    @Test
+    public void testOnSafeBrowsingHitProceed() throws Throwable {
+        Assume.assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_HIT));
+        Assume.assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE));
+        Assume.assumeTrue(WebViewFeature.isFeatureSupported(
+                WebViewFeature.SAFE_BROWSING_RESPONSE_PROCEED));
+
+        final SafeBrowsingProceedClient proceedWebViewClient = new SafeBrowsingProceedClient();
+        mWebViewOnUiThread.setWebViewClient(proceedWebViewClient);
+        WebSettingsCompat.setSafeBrowsingEnabled(mWebViewOnUiThread.getSettings(), true);
+
+        // Load any page
+        String data = "<html><body>some safe page</body></html>";
+        mWebViewOnUiThread.loadDataAndWaitForCompletion(data, "text/html", null);
+
+        enableSafeBrowsingAndLoadUnsafePage(proceedWebViewClient);
+
+        // Check that we actually proceeded
+        Assert.assertEquals(TEST_SAFE_BROWSING_URL, mWebViewOnUiThread.getUrl());
+    }
+
+    private void enableSafeBrowsingAndLoadUnsafePage(SafeBrowsingClient client) throws Throwable {
+        // Note: Safe Browsing depends on user opt-in as well, so we can't assume it's actually
+        // enabled. #getSafeBrowsingEnabled will tell us the true state of whether Safe Browsing is
+        // enabled.
+        boolean deviceSupportsSafeBrowsing =
+                WebSettingsCompat.getSafeBrowsingEnabled(mWebViewOnUiThread.getSettings());
+        Assume.assumeTrue(deviceSupportsSafeBrowsing);
+
+        Assert.assertNull(client.getOnReceivedResourceError());
+        mWebViewOnUiThread.loadUrlAndWaitForCompletion(TEST_SAFE_BROWSING_URL);
+
+        Assert.assertEquals(TEST_SAFE_BROWSING_URL,
+                client.getOnSafeBrowsingHitRequest().getUrl().toString());
+        Assert.assertTrue(client.getOnSafeBrowsingHitRequest().isForMainFrame());
+    }
+
+    @Test
+    public void testOnPageCommitVisibleCalled() throws Exception {
+        Assume.assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.VISUAL_STATE_CALLBACK));
+
+        final CountDownLatch callbackLatch = new CountDownLatch(1);
+
+        mWebViewOnUiThread.setWebViewClient(new WebViewClientCompat() {
+            @Override
+            public void onPageCommitVisible(WebView view, String url) {
+                Assert.assertEquals(url, "about:blank");
+                callbackLatch.countDown();
+            }
+        });
+
+        mWebViewOnUiThread.loadUrl("about:blank");
+        Assert.assertTrue(callbackLatch.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS));
+    }
+
+    private class MockWebViewClient extends WebViewOnUiThread.WaitForLoadedClient {
+        private boolean mOnPageStartedCalled;
+        private boolean mOnPageFinishedCalled;
+        private boolean mOnLoadResourceCalled;
+        private WebResourceErrorCompat mOnReceivedResourceError;
+        private WebResourceResponse mOnReceivedHttpError;
+        private WebResourceRequest mLastShouldOverrideResourceRequest;
+
+        MockWebViewClient() {
+            super(mWebViewOnUiThread);
+        }
+
+        public WebResourceErrorCompat getOnReceivedResourceError() {
+            return mOnReceivedResourceError;
+        }
+
+        public WebResourceResponse getOnReceivedHttpError() {
+            return mOnReceivedHttpError;
+        }
+
+        public WebResourceRequest getLastShouldOverrideResourceRequest() {
+            return mLastShouldOverrideResourceRequest;
+        }
+
+        @Override
+        public void onPageStarted(WebView view, String url, Bitmap favicon) {
+            super.onPageStarted(view, url, favicon);
+            mOnPageStartedCalled = true;
+        }
+
+        @Override
+        public void onPageFinished(WebView view, String url) {
+            super.onPageFinished(view, url);
+            Assert.assertTrue(mOnPageStartedCalled);
+            Assert.assertTrue(mOnLoadResourceCalled);
+            mOnPageFinishedCalled = true;
+        }
+
+        @Override
+        public void onLoadResource(WebView view, String url) {
+            super.onLoadResource(view, url);
+            Assert.assertTrue(mOnPageStartedCalled);
+            mOnLoadResourceCalled = true;
+        }
+
+        @Override
+        @SuppressWarnings("deprecation")
+        public void onReceivedError(WebView view, int errorCode,
+                String description, String failingUrl) {
+            // This can be called if a test runs for a WebView which does not support the {@link
+            // WebViewFeature#RECEIVE_WEB_RESOURCE_ERROR} feature.
+        }
+
+        @Override
+        public void onReceivedError(WebView view, WebResourceRequest request,
+                WebResourceErrorCompat error) {
+            mOnReceivedResourceError = error;
+        }
+
+        @Override
+        public void onReceivedHttpError(WebView view, WebResourceRequest request,
+                WebResourceResponse errorResponse) {
+            super.onReceivedHttpError(view, request, errorResponse);
+            mOnReceivedHttpError = errorResponse;
+        }
+
+        @Override
+        @SuppressWarnings("deprecation")
+        public boolean shouldOverrideUrlLoading(WebView view, String url) {
+            // This can be called if a test runs for a WebView which does not support the {@link
+            // WebViewFeature#SHOULD_OVERRIDE_WITH_REDIRECTS} feature.
+            return false;
+        }
+
+        @Override
+        public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
+            mLastShouldOverrideResourceRequest = request;
+            return false;
+        }
+    }
+
+    private class SafeBrowsingClient extends MockWebViewClient {
+        private WebResourceRequest mOnSafeBrowsingHitRequest;
+        private int mOnSafeBrowsingHitThreatType;
+
+        public WebResourceRequest getOnSafeBrowsingHitRequest() {
+            return mOnSafeBrowsingHitRequest;
+        }
+
+        public void setOnSafeBrowsingHitRequest(WebResourceRequest request) {
+            mOnSafeBrowsingHitRequest = request;
+        }
+
+        public int getOnSafeBrowsingHitThreatType() {
+            return mOnSafeBrowsingHitThreatType;
+        }
+
+        public void setOnSafeBrowsingHitThreatType(int type) {
+            mOnSafeBrowsingHitThreatType = type;
+        }
+    }
+
+    private class SafeBrowsingBackToSafetyClient extends SafeBrowsingClient {
+        @Override
+        public void onSafeBrowsingHit(WebView view, WebResourceRequest request,
+                int threatType, SafeBrowsingResponseCompat response) {
+            // Immediately go back to safety to return the network error code
+            setOnSafeBrowsingHitRequest(request);
+            setOnSafeBrowsingHitThreatType(threatType);
+            response.backToSafety(/* report */ true);
+        }
+    }
+
+    private class SafeBrowsingProceedClient extends SafeBrowsingClient {
+        @Override
+        public void onSafeBrowsingHit(WebView view, WebResourceRequest request,
+                int threatType, SafeBrowsingResponseCompat response) {
+            // Proceed through Safe Browsing warnings
+            setOnSafeBrowsingHitRequest(request);
+            setOnSafeBrowsingHitThreatType(threatType);
+            response.proceed(/* report */ true);
+        }
+    }
+}
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebViewCompatTest.java b/webkit/src/androidTest/java/androidx/webkit/WebViewCompatTest.java
index 3c64b9c..03aa946 100644
--- a/webkit/src/androidTest/java/androidx/webkit/WebViewCompatTest.java
+++ b/webkit/src/androidTest/java/androidx/webkit/WebViewCompatTest.java
@@ -19,8 +19,10 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import android.content.Context;
 import android.content.ContextWrapper;
@@ -29,13 +31,9 @@
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.MediumTest;
 import android.support.test.runner.AndroidJUnit4;
-import android.webkit.SafeBrowsingResponse;
 import android.webkit.ValueCallback;
 import android.webkit.WebResourceRequest;
 import android.webkit.WebView;
-import android.webkit.WebViewClient;
-
-import androidx.core.os.BuildCompat;
 
 import org.junit.Assert;
 import org.junit.Before;
@@ -63,9 +61,7 @@
 
     @Test
     public void testVisualStateCallbackCalled() throws Exception {
-        // TODO(gsennton) activate this test for pre-P devices when we can pre-install a WebView APK
-        // containing support for the WebView Support Library, see b/73454652.
-        if (!BuildCompat.isAtLeastP()) return;
+        assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.VISUAL_STATE_CALLBACK));
 
         final CountDownLatch callbackLatch = new CountDownLatch(1);
         final long kRequest = 100;
@@ -85,6 +81,8 @@
 
     @Test
     public void testCheckThread() {
+        // Skip this test if VisualStateCallback is not supported.
+        assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.VISUAL_STATE_CALLBACK));
         try {
             WebViewCompat.postVisualStateCallback(mWebViewOnUiThread.getWebViewOnCurrentThread(), 5,
                     new WebViewCompat.VisualStateCallback() {
@@ -117,9 +115,7 @@
 
     @Test
     public void testStartSafeBrowsingUseApplicationContext() throws Exception {
-        // TODO(gsennton) activate this test for pre-P devices when we can pre-install a WebView APK
-        // containing support for the WebView Support Library, see b/73454652.
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) return;
+        assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING));
 
         final MockContext ctx =
                 new MockContext(InstrumentationRegistry.getTargetContext().getApplicationContext());
@@ -137,18 +133,14 @@
 
     @Test
     public void testStartSafeBrowsingWithNullCallbackDoesntCrash() throws Exception {
-        // TODO(gsennton) activate this test for pre-P devices when we can pre-install a WebView APK
-        // containing support for the WebView Support Library, see b/73454652.
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) return;
+        assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING));
 
         WebViewCompat.startSafeBrowsing(InstrumentationRegistry.getTargetContext(), null);
     }
 
     @Test
     public void testStartSafeBrowsingInvokesCallback() throws Exception {
-        // TODO(gsennton) activate this test for pre-P devices when we can pre-install a WebView APK
-        // containing support for the WebView Support Library, see b/73454652.
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) return;
+        assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING));
 
         final CountDownLatch resultLatch = new CountDownLatch(1);
         WebViewCompat.startSafeBrowsing(
@@ -166,9 +158,7 @@
 
     @Test
     public void testSetSafeBrowsingWhitelistWithMalformedList() throws Exception {
-        // TODO(gsennton) activate this test for pre-P devices when we can pre-install a WebView APK
-        // containing support for the WebView Support Library, see b/73454652.
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) return;
+        assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_WHITELIST));
 
         List whitelist = new ArrayList<String>();
         // Protocols are not supported in the whitelist
@@ -186,9 +176,9 @@
 
     @Test
     public void testSetSafeBrowsingWhitelistWithValidList() throws Exception {
-        // TODO(gsennton) activate this test for pre-P devices when we can pre-install a WebView APK
-        // containing support for the WebView Support Library, see b/73454652.
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) return;
+        assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_WHITELIST));
+        // This test relies on the onSafeBrowsingHit callback to verify correctness.
+        assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_HIT));
 
         List whitelist = new ArrayList<String>();
         whitelist.add("safe-browsing");
@@ -203,7 +193,7 @@
         assertTrue(resultLatch.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS));
 
         final CountDownLatch resultLatch2 = new CountDownLatch(1);
-        mWebViewOnUiThread.setWebViewClient(new WebViewClient() {
+        mWebViewOnUiThread.setWebViewClient(new WebViewClientCompat() {
             @Override
             public void onPageFinished(WebView view, String url) {
                 resultLatch2.countDown();
@@ -211,7 +201,7 @@
 
             @Override
             public void onSafeBrowsingHit(WebView view, WebResourceRequest request, int threatType,
-                    SafeBrowsingResponse callback) {
+                    SafeBrowsingResponseCompat callback) {
                 Assert.fail("Should not invoke onSafeBrowsingHit");
             }
         });
@@ -224,9 +214,8 @@
 
     @Test
     public void testGetSafeBrowsingPrivacyPolicyUrl() throws Exception {
-        // TODO(gsennton) activate this test for pre-P devices when we can pre-install a WebView APK
-        // containing support for the WebView Support Library, see b/73454652.
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) return;
+        assumeTrue(
+                WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL));
 
         assertNotNull(WebViewCompat.getSafeBrowsingPrivacyPolicyUrl());
         try {
@@ -235,4 +224,22 @@
             Assert.fail("The privacy policy URL should be a well-formed URL");
         }
     }
+
+    /**
+     * WebViewCompat.getCurrentWebViewPackage should be null on pre-L devices.
+     * On L+ devices WebViewCompat.getCurrentWebViewPackage should be null only in exceptional
+     * circumstances - like when the WebView APK is being updated, or for Wear devices. The L+
+     * devices used in support library testing should have a non-null WebView package.
+     */
+    @Test
+    public void testGetCurrentWebViewPackage() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+            assertNull(WebViewCompat.getCurrentWebViewPackage(
+                    InstrumentationRegistry.getTargetContext()));
+        } else {
+            assertNotNull(
+                    WebViewCompat.getCurrentWebViewPackage(
+                            InstrumentationRegistry.getTargetContext()));
+        }
+    }
 }
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java b/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java
index 9b4c9e9..bd83752 100644
--- a/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java
+++ b/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java
@@ -16,12 +16,45 @@
 
 package androidx.webkit;
 
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Looper;
+import android.os.SystemClock;
 import android.support.test.InstrumentationRegistry;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient;
 import android.webkit.WebSettings;
 import android.webkit.WebView;
-import android.webkit.WebViewClient;
 
-public class WebViewOnUiThread {
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.concurrent.Callable;
+
+class WebViewOnUiThread {
+    /**
+     * The maximum time, in milliseconds (10 seconds) to wait for a load
+     * to be triggered.
+     */
+    private static final long LOAD_TIMEOUT = 10000;
+
+    /**
+     * Set to true after onPageFinished is called.
+     */
+    private boolean mLoaded;
+
+    /**
+     * The progress, in percentage, of the page load. Valid values are between
+     * 0 and 100.
+     */
+    private int mProgress;
+
+    /**
+     * The WebView that calls will be made on.
+     */
     private WebView mWebView;
 
     public WebViewOnUiThread() {
@@ -29,6 +62,100 @@
             @Override
             public void run() {
                 mWebView = new WebView(InstrumentationRegistry.getTargetContext());
+                mWebView.setWebViewClient(new WaitForLoadedClient(WebViewOnUiThread.this));
+                mWebView.setWebChromeClient(new WaitForProgressClient(WebViewOnUiThread.this));
+            }
+        });
+    }
+
+    /**
+     * Called after a test is complete and the WebView should be disengaged from
+     * the tests.
+     */
+    public void cleanUp() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.clearHistory();
+                mWebView.clearCache(true);
+                mWebView.setWebChromeClient(null);
+                mWebView.setWebViewClient(null);
+                mWebView.destroy();
+            }
+        });
+    }
+
+    /**
+     * Called from WaitForLoadedClient.
+     */
+    synchronized void onPageStarted() {}
+
+    /**
+     * Called from WaitForLoadedClient, this is used to indicate that
+     * the page is loaded, but not drawn yet.
+     */
+    synchronized void onPageFinished() {
+        mLoaded = true;
+        this.notifyAll();
+    }
+
+    /**
+     * Called from the WebChrome client, this sets the current progress
+     * for a page.
+     * @param progress The progress made so far between 0 and 100.
+     */
+    synchronized void onProgressChanged(int progress) {
+        mProgress = progress;
+        this.notifyAll();
+    }
+
+    public void setWebViewClient(final WebViewClientCompat webviewClient) {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.setWebViewClient(webviewClient);
+            }
+        });
+    }
+
+    public WebMessagePortCompat[] createWebMessageChannelCompat() {
+        return getValue(new ValueGetter<WebMessagePortCompat[]>() {
+            @Override
+            public WebMessagePortCompat[] capture() {
+                return WebViewCompat.createWebMessageChannel(mWebView);
+            }
+        });
+    }
+
+    public void postWebMessageCompat(final WebMessageCompat message, final Uri targetOrigin) {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                WebViewCompat.postWebMessage(mWebView, message, targetOrigin);
+            }
+        });
+    }
+
+    public void addJavascriptInterface(final Object object, final String name) {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.addJavascriptInterface(object, name);
+            }
+        });
+    }
+
+    /**
+     * Calls loadUrl on the WebView and then waits onPageFinished
+     * and onProgressChange to reach 100.
+     * Test fails if the load timeout elapses.
+     * @param url The URL to load.
+     */
+    void loadUrlAndWaitForCompletion(final String url) {
+        callAndWait(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.loadUrl(url);
             }
         });
     }
@@ -42,11 +169,83 @@
         });
     }
 
-    public void setWebViewClient(final WebViewClient webviewClient) {
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+    /**
+     * Calls {@link WebView#loadData} on the WebView and then waits onPageFinished
+     * and onProgressChange to reach 100.
+     * Test fails if the load timeout elapses.
+     * @param data The data to load.
+     * @param mimeType The mimeType to pass to loadData.
+     * @param encoding The encoding to pass to loadData.
+     */
+    public void loadDataAndWaitForCompletion(@NonNull final String data,
+            @Nullable final String mimeType, @Nullable final String encoding) {
+        callAndWait(new Runnable() {
             @Override
             public void run() {
-                mWebView.setWebViewClient(webviewClient);
+                mWebView.loadData(data, mimeType, encoding);
+            }
+        });
+    }
+
+    public void loadDataWithBaseURLAndWaitForCompletion(final String baseUrl,
+            final String data, final String mimeType, final String encoding,
+            final String historyUrl) {
+        callAndWait(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding,
+                        historyUrl);
+            }
+        });
+    }
+
+    /**
+     * Use this only when JavaScript causes a page load to wait for the
+     * page load to complete. Otherwise use loadUrlAndWaitForCompletion or
+     * similar functions.
+     */
+    void waitForLoadCompletion() {
+        waitForCriteria(LOAD_TIMEOUT,
+                new Callable<Boolean>() {
+                    @Override
+                    public Boolean call() {
+                        return isLoaded();
+                    }
+                });
+        clearLoad();
+    }
+
+    private void waitForCriteria(long timeout, Callable<Boolean> doneCriteria) {
+        if (isUiThread()) {
+            waitOnUiThread(timeout, doneCriteria);
+        } else {
+            waitOnTestThread(timeout, doneCriteria);
+        }
+    }
+
+    public String getTitle() {
+        return getValue(new ValueGetter<String>() {
+            @Override
+            public String capture() {
+                return mWebView.getTitle();
+            }
+        });
+    }
+
+    public WebSettings getSettings() {
+        return getValue(new ValueGetter<WebSettings>() {
+            @Override
+            public WebSettings capture() {
+                return mWebView.getSettings();
+            }
+        });
+    }
+
+    public String getUrl() {
+        return getValue(new ValueGetter<String>() {
+            @Override
+            public String capture() {
+                return mWebView.getUrl();
             }
         });
     }
@@ -61,16 +260,16 @@
         });
     }
 
-    public WebSettings getSettings() {
-        return getValue(new ValueGetter<WebSettings>() {
+    void evaluateJavascript(final String script, final ValueCallback<String> result) {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
             @Override
-            public WebSettings capture() {
-                return mWebView.getSettings();
+            public void run() {
+                mWebView.evaluateJavascript(script, result);
             }
         });
     }
 
-    public WebView getWebViewOnCurrentThread() {
+    WebView getWebViewOnCurrentThread() {
         return mWebView;
     }
 
@@ -93,4 +292,151 @@
             return mValue;
         }
     }
+
+    /**
+     * Returns true if the current thread is the UI thread based on the
+     * Looper.
+     */
+    private static boolean isUiThread() {
+        return (Looper.myLooper() == Looper.getMainLooper());
+    }
+
+    /**
+     * @return Whether or not the load has finished.
+     */
+    private synchronized boolean isLoaded() {
+        return mLoaded && mProgress == 100;
+    }
+
+    /**
+     * Makes a WebView call, waits for completion and then resets the
+     * load state in preparation for the next load call.
+     * @param call The call to make on the UI thread prior to waiting.
+     */
+    private void callAndWait(Runnable call) {
+        assertTrue("WebViewOnUiThread.load*AndWaitForCompletion calls "
+                        + "may not be mixed with load* calls directly on WebView "
+                        + "without calling waitForLoadCompletion after the load",
+                !isLoaded());
+        clearLoad(); // clear any extraneous signals from a previous load.
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(call);
+        waitForLoadCompletion();
+    }
+
+    /**
+     * Called whenever a load has been completed so that a subsequent call to
+     * waitForLoadCompletion doesn't return immediately.
+     */
+    private synchronized void clearLoad() {
+        mLoaded = false;
+        mProgress = 0;
+    }
+
+    /**
+     * Uses a polling mechanism, while pumping messages to check when the
+     * criteria is met.
+     */
+    private void waitOnUiThread(long timeout, final Callable<Boolean> doneCriteria) {
+        new PollingCheck(timeout) {
+            @Override
+            protected boolean check() {
+                pumpMessages();
+                try {
+                    return doneCriteria.call();
+                } catch (Exception e) {
+                    fail("Unexpected error while checking the criteria: " + e.getMessage());
+                    return true;
+                }
+            }
+        }.run();
+    }
+
+    /**
+     * Uses a wait/notify to check when the criteria is met.
+     */
+    private synchronized void waitOnTestThread(long timeout, Callable<Boolean> doneCriteria) {
+        try {
+            long waitEnd = SystemClock.uptimeMillis() + timeout;
+            long timeRemaining = timeout;
+            while (!doneCriteria.call() && timeRemaining > 0) {
+                this.wait(timeRemaining);
+                timeRemaining = waitEnd - SystemClock.uptimeMillis();
+            }
+            assertTrue("Action failed to complete before timeout", doneCriteria.call());
+        } catch (InterruptedException e) {
+            // We'll just drop out of the loop and fail
+        } catch (Exception e) {
+            fail("Unexpected error while checking the criteria: " + e.getMessage());
+        }
+    }
+
+    /**
+     * Pumps all currently-queued messages in the UI thread and then exits.
+     * This is useful to force processing while running tests in the UI thread.
+     */
+    private void pumpMessages() {
+        class ExitLoopException extends RuntimeException {
+        }
+
+        // Force loop to exit when processing this. Loop.quit() doesn't
+        // work because this is the main Loop.
+        mWebView.getHandler().post(new Runnable() {
+            @Override
+            public void run() {
+                throw new ExitLoopException(); // exit loop!
+            }
+        });
+        try {
+            // Pump messages until our message gets through.
+            Looper.loop();
+        } catch (ExitLoopException e) {
+        }
+    }
+
+    /**
+     * A WebChromeClient used to capture the onProgressChanged for use
+     * in waitFor functions. If a test must override the WebChromeClient,
+     * it can derive from this class or call onProgressChanged
+     * directly.
+     */
+    public static class WaitForProgressClient extends WebChromeClient {
+        private WebViewOnUiThread mOnUiThread;
+
+        WaitForProgressClient(WebViewOnUiThread onUiThread) {
+            mOnUiThread = onUiThread;
+        }
+
+        @Override
+        public void onProgressChanged(WebView view, int newProgress) {
+            super.onProgressChanged(view, newProgress);
+            mOnUiThread.onProgressChanged(newProgress);
+        }
+    }
+
+    /**
+     * A WebViewClient that captures the onPageFinished for use in
+     * waitFor functions. Using initializeWebView sets the WaitForLoadedClient
+     * into the WebView. If a test needs to set a specific WebViewClient and
+     * needs the waitForCompletion capability then it should derive from
+     * WaitForLoadedClient or call WebViewOnUiThread.onPageFinished.
+     */
+    public static class WaitForLoadedClient extends WebViewClientCompat {
+        private WebViewOnUiThread mOnUiThread;
+
+        WaitForLoadedClient(WebViewOnUiThread onUiThread) {
+            mOnUiThread = onUiThread;
+        }
+
+        @Override
+        public void onPageFinished(WebView view, String url) {
+            super.onPageFinished(view, url);
+            mOnUiThread.onPageFinished();
+        }
+
+        @Override
+        public void onPageStarted(WebView view, String url, Bitmap favicon) {
+            super.onPageStarted(view, url, favicon);
+            mOnUiThread.onPageStarted();
+        }
+    }
 }
diff --git a/webkit/src/main/java/androidx/webkit/SafeBrowsingResponseCompat.java b/webkit/src/main/java/androidx/webkit/SafeBrowsingResponseCompat.java
new file mode 100644
index 0000000..76121f3
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/SafeBrowsingResponseCompat.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.webkit.SafeBrowsingResponse;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RequiresFeature;
+import androidx.webkit.internal.WebViewFeatureInternal;
+
+import org.chromium.support_lib_boundary.SafeBrowsingResponseBoundaryInterface;
+import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
+
+import java.lang.reflect.InvocationHandler;
+
+/**
+ * Compatibility version of {@link SafeBrowsingResponse}.
+ */
+public abstract class SafeBrowsingResponseCompat {
+    /**
+     * Display the default interstitial.
+     *
+     * @param allowReporting {@code true} if the interstitial should show a reporting checkbox.
+     */
+    @RequiresFeature(name = WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public abstract void showInterstitial(boolean allowReporting);
+
+    /**
+     * Act as if the user clicked "visit this unsafe site."
+     *
+     * @param report {@code true} to enable Safe Browsing reporting.
+     */
+    @RequiresFeature(name = WebViewFeature.SAFE_BROWSING_RESPONSE_PROCEED,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public abstract void proceed(boolean report);
+
+    /**
+     * Act as if the user clicked "back to safety."
+     *
+     * @param report {@code true} to enable Safe Browsing reporting.
+     */
+    @RequiresFeature(name = WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public abstract void backToSafety(boolean report);
+
+    /**
+     * This class cannot be created by applications. The support library should instantiate this
+     * with {@link #fromInvocationHandler} or {@link #fromSafeBrowsingResponse}.
+     */
+    private SafeBrowsingResponseCompat() {
+    }
+
+    /**
+     * Conversion helper to create a SafeBrowsingResponseCompat which delegates calls to {@param
+     * handler}. The InvocationHandler must be created by {@link
+     * BoundaryInterfaceReflectionUtil#createInvocationHandlerFor} using {@link
+     * SafeBrowsingResponseBoundaryInterface}.
+     *
+     * @param handler The InvocationHandler that chromium passed in the callback.
+     */
+    @NonNull
+    /* package */ static SafeBrowsingResponseCompat fromInvocationHandler(
+            @NonNull InvocationHandler handler) {
+        final SafeBrowsingResponseBoundaryInterface responseDelegate =
+                BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+                        SafeBrowsingResponseBoundaryInterface.class, handler);
+        return new SafeBrowsingResponseCompat() {
+            @Override
+            public void showInterstitial(boolean allowReporting) {
+                final WebViewFeatureInternal webViewFeature =
+                        WebViewFeatureInternal.getFeature(
+                                WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL);
+                if (!webViewFeature.isSupportedByWebView()) {
+                    throw WebViewFeatureInternal.getUnsupportedOperationException();
+                }
+                responseDelegate.showInterstitial(allowReporting);
+            }
+
+            @Override
+            public void proceed(boolean report) {
+                final WebViewFeatureInternal webViewFeature =
+                        WebViewFeatureInternal.getFeature(
+                                WebViewFeature.SAFE_BROWSING_RESPONSE_PROCEED);
+                if (!webViewFeature.isSupportedByWebView()) {
+                    throw WebViewFeatureInternal.getUnsupportedOperationException();
+                }
+                responseDelegate.proceed(report);
+            }
+
+            @Override
+            public void backToSafety(boolean report) {
+                final WebViewFeatureInternal webViewFeature =
+                        WebViewFeatureInternal.getFeature(
+                                WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY);
+                if (!webViewFeature.isSupportedByWebView()) {
+                    throw WebViewFeatureInternal.getUnsupportedOperationException();
+                }
+                responseDelegate.backToSafety(report);
+            }
+        };
+    }
+
+    /**
+     * Conversion helper to create a SafeBrowsingResponseCompat which delegates calls to {@param
+     * response}.
+     *
+     * @param response The SafeBrowsingResponse that chromium passed in the callback.
+     */
+    @NonNull
+    @RequiresApi(27)
+    /* package */ static SafeBrowsingResponseCompat fromSafeBrowsingResponse(
+            @NonNull final SafeBrowsingResponse response) {
+        // Frameworks support is implied by the API level.
+        return new SafeBrowsingResponseCompat() {
+            @Override
+            public void showInterstitial(boolean allowReporting) {
+                response.showInterstitial(allowReporting);
+            }
+
+            @Override
+            public void proceed(boolean report) {
+                response.proceed(report);
+            }
+
+            @Override
+            public void backToSafety(boolean report) {
+                response.backToSafety(report);
+            }
+        };
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/ServiceWorkerClientCompat.java b/webkit/src/main/java/androidx/webkit/ServiceWorkerClientCompat.java
new file mode 100644
index 0000000..5c3c5ff
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/ServiceWorkerClientCompat.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Base class for clients to capture Service Worker related callbacks,
+ * see {@link ServiceWorkerControllerCompat} for usage example.
+ */
+public abstract class ServiceWorkerClientCompat {
+    /**
+     *
+     * Notify the host application of a resource request and allow the
+     * application to return the data. If the return value is {@code null}, the
+     * Service Worker will continue to load the resource as usual.
+     * Otherwise, the return response and data will be used.
+     *
+     * <p class="note"><b>Note:</b> This method is called on a thread other than the UI thread so
+     * clients should exercise caution when accessing private data or the view system.
+     *
+     * @param request Object containing the details of the request.
+     * @return A {@link android.webkit.WebResourceResponse} containing the
+     * response information or {@code null} if the WebView should load the
+     * resource itself.
+     * @see android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView,
+     * WebResourceRequest)
+     *
+     * This method is called only if
+     * {@link WebViewFeature#SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST} is supported. You can check
+     * whether that flag is supported using {@link WebViewFeature#isFeatureSupported(String)}.
+     *
+     */
+    public abstract WebResourceResponse shouldInterceptRequest(@NonNull WebResourceRequest request);
+}
diff --git a/webkit/src/main/java/androidx/webkit/ServiceWorkerControllerCompat.java b/webkit/src/main/java/androidx/webkit/ServiceWorkerControllerCompat.java
new file mode 100644
index 0000000..79b714a
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/ServiceWorkerControllerCompat.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresFeature;
+import androidx.annotation.RestrictTo;
+import androidx.webkit.internal.ServiceWorkerControllerImpl;
+
+/**
+ * Manages Service Workers used by WebView.
+ *
+ * <p>Example usage:
+ * <pre class="prettyprint">
+ * ServiceWorkerControllerCompat swController = ServiceWorkerControllerCompat.getInstance();
+ * swController.setServiceWorkerClient(new ServiceWorkerClientCompat() {
+ *   {@literal @}Override
+ *   public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
+ *     // Capture request here and generate response or allow pass-through
+ *     // by returning null.
+ *     return null;
+ *   }
+ * });
+ * </pre>
+ */
+public abstract class ServiceWorkerControllerCompat {
+    /**
+     *
+     * @hide Don't allow apps to sub-class this class.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public ServiceWorkerControllerCompat() {}
+
+    /**
+     * Returns the default ServiceWorkerController instance. At present there is
+     * only one ServiceWorkerController instance for all WebView instances,
+     * however this restriction may be relaxed in the future.
+     *
+     * @return the default ServiceWorkerController instance
+     */
+    @NonNull
+    @RequiresFeature(name = WebViewFeature.SERVICE_WORKER_BASIC_USAGE,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public static ServiceWorkerControllerCompat getInstance() {
+        return LAZY_HOLDER.INSTANCE;
+    }
+
+    private static class LAZY_HOLDER {
+        static final ServiceWorkerControllerCompat INSTANCE = new ServiceWorkerControllerImpl();
+    }
+
+    /**
+     *
+     * Gets the settings for all service workers.
+     *
+     * @return the current {@link ServiceWorkerWebSettingsCompat}
+     *
+     */
+    @NonNull
+    public abstract ServiceWorkerWebSettingsCompat getServiceWorkerWebSettings();
+
+    /**
+     *
+     * Sets the client to capture service worker related callbacks.
+     *
+     * A {@link ServiceWorkerClientCompat} should be set before any service workers are
+     * active, e.g. a safe place is before any WebView instances are created or
+     * pages loaded.
+     *
+     */
+    public abstract void setServiceWorkerClient(@Nullable ServiceWorkerClientCompat client);
+}
diff --git a/webkit/src/main/java/androidx/webkit/ServiceWorkerWebSettingsCompat.java b/webkit/src/main/java/androidx/webkit/ServiceWorkerWebSettingsCompat.java
new file mode 100644
index 0000000..6763db4
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/ServiceWorkerWebSettingsCompat.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.webkit.WebSettings;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RequiresFeature;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Manages settings state for all Service Workers. These settings are not tied to
+ * the lifetime of any WebView because service workers can outlive WebView instances.
+ * The settings are similar to {@link WebSettings} but only settings relevant to
+ * Service Workers are supported.
+ */
+public abstract class ServiceWorkerWebSettingsCompat {
+    /**
+     * @hide Don't allow apps to sub-class this class.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public ServiceWorkerWebSettingsCompat() {}
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @IntDef(value = {
+            WebSettings.LOAD_DEFAULT,
+            WebSettings.LOAD_CACHE_ELSE_NETWORK,
+            WebSettings.LOAD_NO_CACHE,
+            WebSettings.LOAD_CACHE_ONLY
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface CacheMode {}
+
+    /**
+     *
+     * Overrides the way the cache is used, see {@link WebSettings#setCacheMode}.
+     *
+     * @param mode the mode to use. One of {@link WebSettings#LOAD_DEFAULT},
+     * {@link WebSettings#LOAD_CACHE_ELSE_NETWORK}, {@link WebSettings#LOAD_NO_CACHE}
+     * or {@link WebSettings#LOAD_CACHE_ONLY}. The default value is
+     * {@link WebSettings#LOAD_DEFAULT}.
+     *
+     */
+    @RequiresFeature(name = WebViewFeature.SERVICE_WORKER_CACHE_MODE,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public abstract void setCacheMode(@CacheMode int mode);
+
+    /**
+     *
+     * Gets the current setting for overriding the cache mode.
+     *
+     * @return the current setting for overriding the cache mode
+     * @see #setCacheMode
+     *
+     */
+    @RequiresFeature(name = WebViewFeature.SERVICE_WORKER_CACHE_MODE,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public abstract @CacheMode int getCacheMode();
+
+    /**
+     *
+     * Enables or disables content URL access from Service Workers, see
+     * {@link WebSettings#setAllowContentAccess}.
+     *
+     */
+    @RequiresFeature(name = WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public abstract void setAllowContentAccess(boolean allow);
+
+    /**
+     *
+     * Gets whether Service Workers support content URL access.
+     *
+     * @see #setAllowContentAccess
+     *
+     */
+    @RequiresFeature(name = WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public abstract boolean getAllowContentAccess();
+
+    /**
+     *
+     * Enables or disables file access within Service Workers, see
+     * {@link WebSettings#setAllowFileAccess}.
+     *
+     */
+    @RequiresFeature(name = WebViewFeature.SERVICE_WORKER_FILE_ACCESS,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public abstract void setAllowFileAccess(boolean allow);
+
+    /**
+     *
+     * Gets whether Service Workers support file access.
+     *
+     * @see #setAllowFileAccess
+     *
+     */
+    @RequiresFeature(name = WebViewFeature.SERVICE_WORKER_FILE_ACCESS,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public abstract boolean getAllowFileAccess();
+
+    /**
+     *
+     * Sets whether Service Workers should not load resources from the network,
+     * see {@link WebSettings#setBlockNetworkLoads}.
+     *
+     * @param flag {@code true} means block network loads by the Service Workers
+     *
+     */
+    @RequiresFeature(name = WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public abstract void setBlockNetworkLoads(boolean flag);
+
+    /**
+     *
+     * Gets whether Service Workers are prohibited from loading any resources from the network.
+     *
+     * @return {@code true} if the Service Workers are not allowed to load any resources from the
+     * network
+     * @see #setBlockNetworkLoads
+     *
+     */
+    @RequiresFeature(name = WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public abstract boolean getBlockNetworkLoads();
+}
diff --git a/webkit/src/main/java/androidx/webkit/WebMessageCompat.java b/webkit/src/main/java/androidx/webkit/WebMessageCompat.java
new file mode 100644
index 0000000..e1b1193
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/WebMessageCompat.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import androidx.annotation.Nullable;
+
+/**
+ * The Java representation of the HTML5 PostMessage event. See
+ * https://html.spec.whatwg.org/multipage/comms.html#the-messageevent-interfaces
+ * for definition of a MessageEvent in HTML5.
+ */
+public class WebMessageCompat {
+
+    private String mData;
+    private WebMessagePortCompat[] mPorts;
+
+    /**
+     * Creates a WebMessage.
+     * @param data  the data of the message.
+     */
+    public WebMessageCompat(@Nullable String data) {
+        mData = data;
+    }
+
+    /**
+     * Creates a WebMessage.
+     * @param data  the data of the message.
+     * @param ports  the ports that are sent with the message.
+     */
+    public WebMessageCompat(@Nullable String data, @Nullable WebMessagePortCompat[] ports) {
+        mData = data;
+        mPorts = ports;
+    }
+
+    /**
+     * Returns the data of the message.
+     */
+    public @Nullable String getData() {
+        return mData;
+    }
+
+    /**
+     * Returns the ports that are sent with the message, or {@code null} if no port
+     * is sent.
+     */
+    @Nullable
+    public WebMessagePortCompat[] getPorts() {
+        return mPorts;
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/WebMessagePortCompat.java b/webkit/src/main/java/androidx/webkit/WebMessagePortCompat.java
new file mode 100644
index 0000000..f6efd0a
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/WebMessagePortCompat.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.os.Handler;
+import android.webkit.WebMessagePort;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+/**
+ * <p>The Java representation of the
+ * <a href="https://html.spec.whatwg.org/multipage/comms.html#messageport">
+ * HTML5 message ports.</a>
+ *
+ * <p>A Message port represents one endpoint of a Message Channel. In Android
+ * webview, there is no separate Message Channel object. When a message channel
+ * is created, both ports are tangled to each other and started, and then
+ * returned in a MessagePort array, see {@link WebViewCompat#createWebMessageChannel}
+ * for creating a message channel.
+ *
+ * <p>When a message port is first created or received via transfer, it does not
+ * have a WebMessageCallback to receive web messages. The messages are queued until
+ * a WebMessageCallback is set.
+ *
+ * <p>A message port should be closed when it is not used by the embedder application
+ * anymore. A closed port cannot be transferred or cannot be reopened to send
+ * messages. Close can be called multiple times.
+ *
+ * <p>When a port is transferred to JS, it cannot be used to send or receive messages
+ * at the Java side anymore. Different from HTML5 Spec, a port cannot be transferred
+ * if one of these has ever happened: i. a message callback was set, ii. a message was
+ * posted on it. A transferred port cannot be closed by the application, since
+ * the ownership is also transferred.
+ *
+ * <p>It is possible to transfer both ports of a channel to JS, for example for
+ * communication between subframes.
+ */
+public abstract class WebMessagePortCompat {
+    /**
+     * The listener for handling MessagePort events. The message callback
+     * methods are called on the main thread. If the embedder application
+     * wants to receive the messages on a different thread, it can do this
+     * by passing a Handler in
+     *  {@link WebMessagePortCompat#setWebMessageCallback(Handler, WebMessageCallbackCompat)}.
+     * In the latter case, the application should be extra careful for thread safety
+     * since WebMessagePort methods should be called on main thread.
+     */
+    public abstract static class WebMessageCallbackCompat {
+        /**
+         * Message callback for receiving onMessage events.
+         *
+         * @param port  the WebMessagePort that the message is destined for
+         * @param message  the message from the entangled port.
+         */
+        public void onMessage(@NonNull WebMessagePortCompat port,
+                @Nullable WebMessageCompat message) { }
+    }
+
+    /**
+     * @hide disallow app devs to extend this class.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public WebMessagePortCompat() { }
+
+    /**
+     * Post a WebMessage to the entangled port.
+     *
+     * @param message  the message from Java to JS.
+     *
+     * @throws IllegalStateException If message port is already transferred or closed.
+     */
+    public abstract void postMessage(@NonNull WebMessageCompat message);
+
+    /**
+     * Close the message port and free any resources associated with it.
+     */
+    public abstract void close();
+
+    /**
+     * Sets a callback to receive message events on the main thread.
+     *
+     * @param callback  the message callback.
+     */
+    public abstract void setWebMessageCallback(@NonNull WebMessageCallbackCompat callback);
+
+    /**
+     * Sets a callback to receive message events on the handler that is provided
+     * by the application. If the handler is null the message events are received on the main
+     * thread.
+     *
+     * @param handler   the handler to receive the message events.
+     * @param callback  the message callback.
+     */
+    public abstract void setWebMessageCallback(@Nullable Handler handler,
+            @NonNull WebMessageCallbackCompat callback);
+
+    /**
+     * Internal getter returning the private {@link WebMessagePort} implementing this class.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public abstract WebMessagePort getFrameworkPort();
+}
diff --git a/webkit/src/main/java/androidx/webkit/WebResourceErrorCompat.java b/webkit/src/main/java/androidx/webkit/WebResourceErrorCompat.java
new file mode 100644
index 0000000..3d8b99a
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/WebResourceErrorCompat.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.webkit.WebResourceError;
+import android.webkit.WebViewClient;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresFeature;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Compatibility version of {@link WebResourceError}.
+ */
+public abstract class WebResourceErrorCompat {
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @IntDef(value = {
+            WebViewClient.ERROR_UNKNOWN,
+            WebViewClient.ERROR_HOST_LOOKUP,
+            WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME,
+            WebViewClient.ERROR_AUTHENTICATION,
+            WebViewClient.ERROR_PROXY_AUTHENTICATION,
+            WebViewClient.ERROR_CONNECT,
+            WebViewClient.ERROR_IO,
+            WebViewClient.ERROR_TIMEOUT,
+            WebViewClient.ERROR_REDIRECT_LOOP,
+            WebViewClient.ERROR_UNSUPPORTED_SCHEME,
+            WebViewClient.ERROR_FAILED_SSL_HANDSHAKE,
+            WebViewClient.ERROR_BAD_URL,
+            WebViewClient.ERROR_FILE,
+            WebViewClient.ERROR_FILE_NOT_FOUND,
+            WebViewClient.ERROR_TOO_MANY_REQUESTS,
+            WebViewClient.ERROR_UNSAFE_RESOURCE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface NetErrorCode {}
+
+    /**
+     * Gets the error code of the error. The code corresponds to one
+     * of the {@code ERROR_*} constants in {@link WebViewClient}.
+     *
+     * @return The error code of the error
+     */
+    @RequiresFeature(name = WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public abstract @NetErrorCode int getErrorCode();
+
+    /**
+     * Gets the string describing the error. Descriptions are localized,
+     * and thus can be used for communicating the problem to the user.
+     *
+     * @return The description of the error
+     */
+    @NonNull
+    @RequiresFeature(name = WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public abstract CharSequence getDescription();
+
+    /**
+     * This class cannot be created by applications.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public WebResourceErrorCompat() {
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/WebResourceRequestCompat.java b/webkit/src/main/java/androidx/webkit/WebResourceRequestCompat.java
new file mode 100644
index 0000000..783efca
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/WebResourceRequestCompat.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.annotation.SuppressLint;
+import android.webkit.WebResourceRequest;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresFeature;
+import androidx.webkit.internal.WebResourceRequestAdapter;
+import androidx.webkit.internal.WebViewFeatureInternal;
+import androidx.webkit.internal.WebViewGlueCommunicator;
+
+// TODO(gsennton) add a test for this class
+
+/**
+ * Compatibility version of {@link WebResourceRequest}.
+ */
+public class WebResourceRequestCompat {
+
+    // Developers should not be able to instantiate this class.
+    private WebResourceRequestCompat() {}
+
+    /**
+     * Gets whether the request was a result of a server-side redirect.
+     *
+     * @return whether the request was a result of a server-side redirect.
+     */
+    @SuppressLint("NewApi")
+    @RequiresFeature(name = WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public static boolean isRedirect(@NonNull WebResourceRequest request) {
+        WebViewFeatureInternal feature = WebViewFeatureInternal.WEB_RESOURCE_REQUEST_IS_REDIRECT;
+        if (feature.isSupportedByFramework()) {
+            return request.isRedirect();
+        } else if (feature.isSupportedByWebView()) {
+            return getAdapter(request).isRedirect();
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    private static WebResourceRequestAdapter getAdapter(WebResourceRequest request) {
+        return WebViewGlueCommunicator.getCompatConverter().convertWebResourceRequest(request);
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java b/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
index c73cda6..bff6170 100644
--- a/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
+++ b/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
@@ -16,20 +16,27 @@
 
 package androidx.webkit;
 
-import android.os.Build;
+import android.annotation.SuppressLint;
 import android.webkit.WebSettings;
 
+import androidx.annotation.IntDef;
+import androidx.annotation.RequiresFeature;
+import androidx.annotation.RestrictTo;
 import androidx.webkit.internal.WebSettingsAdapter;
+import androidx.webkit.internal.WebViewFeatureInternal;
 import androidx.webkit.internal.WebViewGlueCommunicator;
 
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
 /**
  * Compatibility version of {@link android.webkit.WebSettings}
  */
 public class WebSettingsCompat {
     private WebSettingsCompat() {}
 
-    // TODO(gsennton): add feature detection
-
     /**
      * Sets whether this WebView should raster tiles when it is
      * offscreen but attached to a window. Turning this on can avoid
@@ -43,11 +50,18 @@
      *   visible WebViews and WebViews about to be animated to visible.
      * </ul>
      */
+    @SuppressLint("NewApi")
+    @RequiresFeature(name = WebViewFeature.OFF_SCREEN_PRERASTER,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
     public static void setOffscreenPreRaster(WebSettings webSettings, boolean enabled) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+        WebViewFeatureInternal webviewFeature =
+                WebViewFeatureInternal.getFeature(WebViewFeature.OFF_SCREEN_PRERASTER);
+        if (webviewFeature.isSupportedByFramework()) {
             webSettings.setOffscreenPreRaster(enabled);
-        } else {
+        } else if (webviewFeature.isSupportedByWebView()) {
             getAdapter(webSettings).setOffscreenPreRaster(enabled);
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
         }
     }
 
@@ -57,11 +71,18 @@
      * @return {@code true} if this WebView will raster tiles when it is
      * offscreen but attached to a window.
      */
+    @SuppressLint("NewApi")
+    @RequiresFeature(name = WebViewFeature.OFF_SCREEN_PRERASTER,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
     public static boolean getOffscreenPreRaster(WebSettings webSettings) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+        WebViewFeatureInternal webviewFeature =
+                WebViewFeatureInternal.getFeature(WebViewFeature.OFF_SCREEN_PRERASTER);
+        if (webviewFeature.isSupportedByFramework()) {
             return webSettings.getOffscreenPreRaster();
-        } else {
+        } else if (webviewFeature.isSupportedByWebView()) {
             return getAdapter(webSettings).getOffscreenPreRaster();
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
         }
     }
 
@@ -79,11 +100,18 @@
      *
      * @param enabled Whether Safe Browsing is enabled.
      */
+    @SuppressLint("NewApi")
+    @RequiresFeature(name = WebViewFeature.SAFE_BROWSING_ENABLE,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
     public static void setSafeBrowsingEnabled(WebSettings webSettings, boolean enabled) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        WebViewFeatureInternal webviewFeature =
+                WebViewFeatureInternal.getFeature(WebViewFeature.SAFE_BROWSING_ENABLE);
+        if (webviewFeature.isSupportedByFramework()) {
             webSettings.setSafeBrowsingEnabled(enabled);
-        } else {
+        } else if (webviewFeature.isSupportedByWebView()) {
             getAdapter(webSettings).setSafeBrowsingEnabled(enabled);
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
         }
     }
 
@@ -93,23 +121,52 @@
      *
      * @return {@code true} if Safe Browsing is enabled and {@code false} otherwise.
      */
+    @SuppressLint("NewApi")
+    @RequiresFeature(name = WebViewFeature.SAFE_BROWSING_ENABLE,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
     public static boolean getSafeBrowsingEnabled(WebSettings webSettings) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        WebViewFeatureInternal webviewFeature =
+                WebViewFeatureInternal.getFeature(WebViewFeature.SAFE_BROWSING_ENABLE);
+        if (webviewFeature.isSupportedByFramework()) {
             return webSettings.getSafeBrowsingEnabled();
-        } else {
+        } else if (webviewFeature.isSupportedByWebView()) {
             return getAdapter(webSettings).getSafeBrowsingEnabled();
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
         }
     }
 
     /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @IntDef(flag = true, value = {
+            WebSettings.MENU_ITEM_NONE,
+            WebSettings.MENU_ITEM_SHARE,
+            WebSettings.MENU_ITEM_WEB_SEARCH,
+            WebSettings.MENU_ITEM_PROCESS_TEXT
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @Target({ElementType.PARAMETER, ElementType.METHOD})
+    public @interface MenuItemFlags {}
+
+    /**
      * Disables the action mode menu items according to {@code menuItems} flag.
      * @param menuItems an integer field flag for the menu items to be disabled.
      */
-    public static void setDisabledActionModeMenuItems(WebSettings webSettings, int menuItems) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+    @SuppressLint("NewApi")
+    @RequiresFeature(name = WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public static void setDisabledActionModeMenuItems(WebSettings webSettings,
+            @MenuItemFlags int menuItems) {
+        WebViewFeatureInternal webviewFeature =
+                WebViewFeatureInternal.getFeature(WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS);
+        if (webviewFeature.isSupportedByFramework()) {
             webSettings.setDisabledActionModeMenuItems(menuItems);
-        } else {
+        } else if (webviewFeature.isSupportedByWebView()) {
             getAdapter(webSettings).setDisabledActionModeMenuItems(menuItems);
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
         }
     }
 
@@ -119,11 +176,18 @@
      *
      * @return all the disabled menu item flags combined with bitwise OR.
      */
-    public static int getDisabledActionModeMenuItems(WebSettings webSettings) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+    @SuppressLint("NewApi")
+    @RequiresFeature(name = WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public static @MenuItemFlags int getDisabledActionModeMenuItems(WebSettings webSettings) {
+        WebViewFeatureInternal webviewFeature =
+                WebViewFeatureInternal.getFeature(WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS);
+        if (webviewFeature.isSupportedByFramework()) {
             return webSettings.getDisabledActionModeMenuItems();
-        } else {
+        } else if (webviewFeature.isSupportedByWebView()) {
             return getAdapter(webSettings).getDisabledActionModeMenuItems();
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
         }
     }
 
diff --git a/webkit/src/main/java/androidx/webkit/WebViewClientCompat.java b/webkit/src/main/java/androidx/webkit/WebViewClientCompat.java
new file mode 100644
index 0000000..3dcfcc5
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/WebViewClientCompat.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.os.Build;
+import android.webkit.SafeBrowsingResponse;
+import android.webkit.WebResourceError;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.webkit.internal.WebResourceErrorImpl;
+import androidx.webkit.internal.WebViewFeatureInternal;
+
+import org.chromium.support_lib_boundary.WebViewClientBoundaryInterface;
+import org.chromium.support_lib_boundary.util.Features;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.InvocationHandler;
+
+/**
+ * Compatibility version of {@link android.webkit.WebViewClient}.
+ */
+// Note: some methods are marked as RequiresApi 21, because only an up-to-date WebView APK would
+// ever invoke these methods (and WebView can only be updated on Lollipop and above). The app can
+// still construct a WebViewClientCompat on a pre-Lollipop devices, and explicitly invoke these
+// methods, so each of these methods must also handle this case.
+public class WebViewClientCompat extends WebViewClient implements WebViewClientBoundaryInterface {
+    private static final String[] sSupportedFeatures = new String[] {
+        Features.VISUAL_STATE_CALLBACK,
+        Features.RECEIVE_WEB_RESOURCE_ERROR,
+        Features.RECEIVE_HTTP_ERROR,
+        Features.SHOULD_OVERRIDE_WITH_REDIRECTS,
+        Features.SAFE_BROWSING_HIT,
+    };
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @IntDef(value = {
+            WebViewClient.SAFE_BROWSING_THREAT_UNKNOWN,
+            WebViewClient.SAFE_BROWSING_THREAT_MALWARE,
+            WebViewClient.SAFE_BROWSING_THREAT_PHISHING,
+            WebViewClient.SAFE_BROWSING_THREAT_UNWANTED_SOFTWARE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SafeBrowsingThreat {}
+
+    /**
+     * Returns the list of features this client supports. This feature list should always be a
+     * subset of the Features declared in WebViewFeature.
+     *
+     * @hide
+     */
+    @Override
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public final String[] getSupportedFeatures() {
+        return sSupportedFeatures;
+    }
+
+    /**
+     * Notify the host application that {@link android.webkit.WebView} content left over from
+     * previous page navigations will no longer be drawn.
+     *
+     * <p>This callback can be used to determine the point at which it is safe to make a recycled
+     * {@link android.webkit.WebView} visible, ensuring that no stale content is shown. It is called
+     * at the earliest point at which it can be guaranteed that {@link WebView#onDraw} will no
+     * longer draw any content from previous navigations. The next draw will display either the
+     * {@link WebView#setBackgroundColor background color} of the {@link WebView}, or some of the
+     * contents of the newly loaded page.
+     *
+     * <p>This method is called when the body of the HTTP response has started loading, is reflected
+     * in the DOM, and will be visible in subsequent draws. This callback occurs early in the
+     * document loading process, and as such you should expect that linked resources (for example,
+     * CSS and images) may not be available.
+     *
+     * <p>For more fine-grained notification of visual state updates, see {@link
+     * WebViewCompat#postVisualStateCallback}.
+     *
+     * <p>Please note that all the conditions and recommendations applicable to
+     * {@link WebViewCompat#postVisualStateCallback} also apply to this API.
+     *
+     * <p>This callback is only called for main frame navigations.
+     *
+     * <p>This method is called only if {@link WebViewFeature#VISUAL_STATE_CALLBACK} is supported.
+     * You can check whether that flag is supported using {@link
+     * WebViewFeature#isFeatureSupported(String)}.
+     *
+     * @param view The {@link android.webkit.WebView} for which the navigation occurred.
+     * @param url  The URL corresponding to the page navigation that triggered this callback.
+     */
+    @Override
+    public void onPageCommitVisible(@NonNull WebView view, @NonNull String url) {
+    }
+
+    /**
+     * Invoked by chromium (for WebView APks 67+) for the {@code onReceivedError} event.
+     * Applications are not meant to override this, and should instead override the non-final {@link
+     * onReceivedError(WebView, WebResourceRequest, WebResourceErrorCompat)} method.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    @RequiresApi(21)
+    public final void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest request,
+            /* WebResourceError */ @NonNull InvocationHandler handler) {
+        onReceivedError(view, request, new WebResourceErrorImpl(handler));
+    }
+
+    /**
+     * Invoked by chromium (in legacy WebView APKs) for the {@code onReceivedError} event on {@link
+     * Build.VERSION_CODES.M} and above. Applications are not meant to override this, and should
+     * instead override the non-final {@link onReceivedError(WebView, WebResourceRequest,
+     * WebResourceErrorCompat)} method.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    @RequiresApi(23)
+    public final void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest request,
+            @NonNull WebResourceError error) {
+        if (Build.VERSION.SDK_INT < 23) return;
+        onReceivedError(view, request, new WebResourceErrorImpl(error));
+    }
+
+    /**
+     * Report web resource loading error to the host application. These errors usually indicate
+     * inability to connect to the server. Note that unlike the deprecated version of the callback,
+     * the new version will be called for any resource (iframe, image, etc.), not just for the main
+     * page. Thus, it is recommended to perform minimum required work in this callback.
+     *
+     * <p>This method is called only if {@link WebViewFeature#RECEIVE_WEB_RESOURCE_ERROR} is
+     * supported. You can check whether that flag is supported using {@link
+     * WebViewFeature#isFeatureSupported(String)}.
+     *
+     * @param view The WebView that is initiating the callback.
+     * @param request The originating request.
+     * @param error Information about the error occurred.
+     */
+    @SuppressWarnings("deprecation") // for invoking the old onReceivedError.
+    @RequiresApi(21)
+    public void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest request,
+            @NonNull WebResourceErrorCompat error) {
+        if (Build.VERSION.SDK_INT < 21) return;
+        if (!WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)
+                || !WebViewFeature.isFeatureSupported(
+                        WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION)) {
+            // If the WebView APK drops supports for these APIs in the future, simply do nothing.
+            return;
+        }
+        if (request.isForMainFrame()) {
+            onReceivedError(view,
+                    error.getErrorCode(), error.getDescription().toString(),
+                    request.getUrl().toString());
+        }
+    }
+
+    /**
+     * Notify the host application that an HTTP error has been received from the server while
+     * loading a resource.  HTTP errors have status codes &gt;= 400.  This callback will be called
+     * for any resource (iframe, image, etc.), not just for the main page. Thus, it is recommended
+     * to perform minimum required work in this callback. Note that the content of the server
+     * response may not be provided within the {@code errorResponse} parameter.
+     *
+     * <p>This method is called only if {@link WebViewFeature#RECEIVE_HTTP_ERROR} is supported. You
+     * can check whether that flag is supported using {@link
+     * WebViewFeature#isFeatureSupported(String)}.
+     *
+     * @param view The WebView that is initiating the callback.
+     * @param request The originating request.
+     * @param errorResponse Information about the error occurred.
+     */
+    @Override
+    public void onReceivedHttpError(@NonNull WebView view, @NonNull WebResourceRequest request,
+            @NonNull WebResourceResponse errorResponse) {
+    }
+
+    /**
+     * Invoked by chromium (for WebView APks 67+) for the {@code onSafeBrowsingHit} event.
+     * Applications are not meant to override this, and should instead override the non-final {@link
+     * onSafeBrowsingHit(WebView, WebResourceRequest, int, SafeBrowsingResponseCompat)} method.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    public final void onSafeBrowsingHit(@NonNull WebView view, @NonNull WebResourceRequest request,
+            @SafeBrowsingThreat int threatType,
+            /* SafeBrowsingResponse */ @NonNull InvocationHandler handler) {
+        onSafeBrowsingHit(view, request, threatType,
+                SafeBrowsingResponseCompat.fromInvocationHandler(handler));
+    }
+
+    /**
+     * Invoked by chromium (in legacy WebView APKs) for the {@code onSafeBrowsingHit} event on
+     * {@link Build.VERSION_CODES.O_MR1} and above. Applications are not meant to override this, and
+     * should instead override the non-final {@link onSafeBrowsingHit(WebView, WebResourceRequest,
+     * int, SafeBrowsingResponseCompat)} method.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    @RequiresApi(27)
+    public final void onSafeBrowsingHit(@NonNull WebView view, @NonNull WebResourceRequest request,
+            @SafeBrowsingThreat int threatType, @NonNull SafeBrowsingResponse response) {
+        onSafeBrowsingHit(view, request, threatType,
+                SafeBrowsingResponseCompat.fromSafeBrowsingResponse(response));
+    }
+
+    /**
+     * Notify the host application that a loading URL has been flagged by Safe Browsing.
+     *
+     * The application must invoke the callback to indicate the preferred response. The default
+     * behavior is to show an interstitial to the user, with the reporting checkbox visible.
+     *
+     * If the application needs to show its own custom interstitial UI, the callback can be invoked
+     * asynchronously with {@link SafeBrowsingResponseCompat#backToSafety} or {@link
+     * SafeBrowsingResponseCompat#proceed}, depending on user response.
+     *
+     * @param view The WebView that hit the malicious resource.
+     * @param request Object containing the details of the request.
+     * @param threatType The reason the resource was caught by Safe Browsing, corresponding to a
+     *                   {@code SAFE_BROWSING_THREAT_*} value.
+     * @param callback Applications must invoke one of the callback methods.
+     */
+    public void onSafeBrowsingHit(@NonNull WebView view, @NonNull WebResourceRequest request,
+            @SafeBrowsingThreat int threatType, @NonNull SafeBrowsingResponseCompat callback) {
+        if (WebViewFeature.isFeatureSupported(
+                WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL)) {
+            callback.showInterstitial(true);
+        } else {
+            // This should not happen, but in case the WebView APK eventually drops support for
+            // showInterstitial(), raise a runtime exception and require the WebView APK to handle
+            // this.
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    /**
+     * Give the host application a chance to take over the control when a new
+     * url is about to be loaded in the current WebView. If WebViewClient is not
+     * provided, by default WebView will ask Activity Manager to choose the
+     * proper handler for the url. If WebViewClient is provided, return {@code true}
+     * means the host application handles the url, while return {@code false} means the
+     * current WebView handles the url.
+     *
+     * <p>Notes:
+     * <ul>
+     * <li>This method is not called for requests using the POST &quot;method&quot;.</li>
+     * <li>This method is also called for subframes with non-http schemes, thus it is
+     * strongly disadvised to unconditionally call {@link WebView#loadUrl(String)}
+     * with the request's url from inside the method and then return {@code true},
+     * as this will make WebView to attempt loading a non-http url, and thus fail.</li>
+     * </ul>
+     *
+     * <p>This method is called only if {@link WebViewFeature#SHOULD_OVERRIDE_WITH_REDIRECTS} is
+     * supported. You can check whether that flag is supported using {@link
+     * WebViewFeature#isFeatureSupported(String)}.
+     *
+     * @param view The WebView that is initiating the callback.
+     * @param request Object containing the details of the request.
+     * @return {@code true} if the host application wants to leave the current WebView
+     *         and handle the url itself, otherwise return {@code false}.
+     */
+    @Override
+    @SuppressWarnings("deprecation") // for invoking the old shouldOverrideUrlLoading.
+    @RequiresApi(21)
+    public boolean shouldOverrideUrlLoading(@NonNull WebView view,
+            @NonNull WebResourceRequest request) {
+        if (Build.VERSION.SDK_INT < 21) return false;
+        return shouldOverrideUrlLoading(view, request.getUrl().toString());
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/WebViewCompat.java b/webkit/src/main/java/androidx/webkit/WebViewCompat.java
index 6a48e35..1172997 100644
--- a/webkit/src/main/java/androidx/webkit/WebViewCompat.java
+++ b/webkit/src/main/java/androidx/webkit/WebViewCompat.java
@@ -16,7 +16,10 @@
 
 package androidx.webkit;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Looper;
@@ -25,10 +28,13 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresFeature;
 import androidx.core.os.BuildCompat;
+import androidx.webkit.internal.WebMessagePortImpl;
+import androidx.webkit.internal.WebViewFeatureInternal;
 import androidx.webkit.internal.WebViewGlueCommunicator;
 import androidx.webkit.internal.WebViewProviderAdapter;
-import androidx.webkit.internal.WebViewProviderFactoryAdapter;
+import androidx.webkit.internal.WebViewProviderFactory;
 
 import org.chromium.support_lib_boundary.WebViewProviderBoundaryInterface;
 
@@ -40,6 +46,9 @@
  * Compatibility version of {@link android.webkit.WebView}
  */
 public class WebViewCompat {
+    private static final Uri WILDCARD_URI = Uri.parse("*");
+    private static final Uri EMPTY_URI = Uri.parse("");
+
     private WebViewCompat() {} // Don't allow instances of this class to be constructed.
 
     /**
@@ -104,13 +113,22 @@
      * {@link android.webkit.WebSettings#setOffscreenPreRaster} for more details and do consider its
      * caveats.
      *
+     * This method should only be called if
+     * {@link WebViewFeature#isFeatureSupported(String)}
+     * returns true for {@link WebViewFeature#VISUAL_STATE_CALLBACK}.
+     *
      * @param requestId An id that will be returned in the callback to allow callers to match
      *                  requests with callbacks.
      * @param callback  The callback to be invoked.
      */
+    @SuppressWarnings("NewApi")
+    @RequiresFeature(name = WebViewFeature.VISUAL_STATE_CALLBACK,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
     public static void postVisualStateCallback(@NonNull WebView webview, long requestId,
             @NonNull final VisualStateCallback callback) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+        WebViewFeatureInternal webViewFeature =
+                WebViewFeatureInternal.getFeature(WebViewFeature.VISUAL_STATE_CALLBACK);
+        if (webViewFeature.isSupportedByFramework()) {
             webview.postVisualStateCallback(requestId,
                     new android.webkit.WebView.VisualStateCallback() {
                         @Override
@@ -118,10 +136,11 @@
                             callback.onComplete(l);
                         }
                     });
-        } else {
-            // TODO(gsennton): guard with if WebViewApk.hasFeature(POSTVISUALSTATECALLBACK)
+        } else if (webViewFeature.isSupportedByWebView()) {
             checkThread(webview);
             getProvider(webview).insertVisualStateCallback(requestId, callback);
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
         }
     }
 
@@ -143,12 +162,19 @@
      * @param callback will be called on the UI thread with {@code true} if initialization is
      * successful, {@code false} otherwise.
      */
+    @SuppressLint("NewApi")
+    @RequiresFeature(name = WebViewFeature.START_SAFE_BROWSING,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
     public static void startSafeBrowsing(@NonNull Context context,
             @Nullable ValueCallback<Boolean> callback) {
-        if (Build.VERSION.SDK_INT >= 27) {
+        WebViewFeatureInternal webviewFeature =
+                WebViewFeatureInternal.getFeature(WebViewFeature.START_SAFE_BROWSING);
+        if (webviewFeature.isSupportedByFramework()) {
             WebView.startSafeBrowsing(context, callback);
-        } else { // TODO(gsennton): guard with WebViewApk.hasFeature(SafeBrowsing)
+        } else if (webviewFeature.isSupportedByWebView()) {
             getFactory().getStatics().initSafeBrowsing(context, callback);
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
         }
     }
 
@@ -175,12 +201,19 @@
      * whitelist. It will be called with {@code false} if any hosts are malformed. The callback
      * will be run on the UI thread
      */
+    @SuppressLint("NewApi")
+    @RequiresFeature(name = WebViewFeature.SAFE_BROWSING_WHITELIST,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
     public static void setSafeBrowsingWhitelist(@NonNull List<String> hosts,
             @Nullable ValueCallback<Boolean> callback) {
-        if (Build.VERSION.SDK_INT >= 27) {
+        WebViewFeatureInternal webviewFeature =
+                WebViewFeatureInternal.getFeature(WebViewFeature.SAFE_BROWSING_WHITELIST);
+        if (webviewFeature.isSupportedByFramework()) {
             WebView.setSafeBrowsingWhitelist(hosts, callback);
-        } else { // TODO(gsennton): guard with WebViewApk.hasFeature(SafeBrowsing)
+        } else if (webviewFeature.isSupportedByWebView()) {
             getFactory().getStatics().setSafeBrowsingWhitelist(hosts, callback);
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
         }
     }
 
@@ -189,12 +222,110 @@
      *
      * @return the url pointing to a privacy policy document which can be displayed to users.
      */
+    @SuppressLint("NewApi")
     @NonNull
+    @RequiresFeature(name = WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
     public static Uri getSafeBrowsingPrivacyPolicyUrl() {
-        if (Build.VERSION.SDK_INT >= 27) {
+        WebViewFeatureInternal webviewFeature =
+                WebViewFeatureInternal.getFeature(WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL);
+        if (webviewFeature.isSupportedByFramework()) {
             return WebView.getSafeBrowsingPrivacyPolicyUrl();
-        } else { // TODO(gsennton): guard with WebViewApk.hasFeature(SafeBrowsing)
+        } else if (webviewFeature.isSupportedByWebView()) {
             return getFactory().getStatics().getSafeBrowsingPrivacyPolicyUrl();
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    /**
+     * If WebView has already been loaded into the current process this method will return the
+     * package that was used to load it. Otherwise, the package that would be used if the WebView
+     * was loaded right now will be returned; this does not cause WebView to be loaded, so this
+     * information may become outdated at any time.
+     * The WebView package changes either when the current WebView package is updated, disabled, or
+     * uninstalled. It can also be changed through a Developer Setting.
+     * If the WebView package changes, any app process that has loaded WebView will be killed. The
+     * next time the app starts and loads WebView it will use the new WebView package instead.
+     * @return the current WebView package, or {@code null} if there is none.
+     */
+    // Note that this API is not protected by a {@link androidx.webkit.WebViewFeature} since
+    // this feature is not dependent on the WebView APK.
+    @Nullable
+    public static PackageInfo getCurrentWebViewPackage(@NonNull Context context) {
+        // There was no WebView Package before Lollipop, the WebView code was part of the framework
+        // back then.
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+            return null;
+        }
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            return WebView.getCurrentWebViewPackage();
+        } else { // L-N
+            try {
+                PackageInfo loadedWebViewPackageInfo = getLoadedWebViewPackageInfo();
+                if (loadedWebViewPackageInfo != null) return loadedWebViewPackageInfo;
+            } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException
+                | NoSuchMethodException  e) {
+                return null;
+            }
+
+            // If WebViewFactory.getLoadedPackageInfo() returns null then WebView hasn't been loaded
+            // yet, in that case we need to fetch the name of the WebView package, and fetch the
+            // corresponding PackageInfo through the PackageManager
+            return getNotYetLoadedWebViewPackageInfo(context);
+        }
+    }
+
+    /**
+     * Return the PackageInfo of the currently loaded WebView APK. This method uses reflection and
+     * propagates any exceptions thrown, to the caller.
+     */
+    private static PackageInfo getLoadedWebViewPackageInfo()
+            throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
+            IllegalAccessException {
+        Class<?> webViewFactoryClass = Class.forName("android.webkit.WebViewFactory");
+        PackageInfo webviewPackageInfo =
+                (PackageInfo) webViewFactoryClass.getMethod(
+                        "getLoadedPackageInfo").invoke(null);
+        return webviewPackageInfo;
+    }
+
+    /**
+     * Return the PackageInfo of the WebView APK that would have been used as WebView implementation
+     * if WebView was to be loaded right now.
+     */
+    private static PackageInfo getNotYetLoadedWebViewPackageInfo(Context context) {
+        String webviewPackageName = null;
+        try {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
+                    && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
+                Class<?> webViewFactoryClass = null;
+                webViewFactoryClass = Class.forName("android.webkit.WebViewFactory");
+
+                webviewPackageName = (String) webViewFactoryClass.getMethod(
+                        "getWebViewPackageName").invoke(null);
+            } else {
+                Class<?> webviewUpdateServiceClass =
+                        Class.forName("android.webkit.WebViewUpdateService");
+                webviewPackageName = (String) webviewUpdateServiceClass.getMethod(
+                        "getCurrentWebViewPackageName").invoke(null);
+            }
+        } catch (ClassNotFoundException e) {
+            return null;
+        } catch (IllegalAccessException e) {
+            return null;
+        } catch (InvocationTargetException e) {
+            return null;
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+        if (webviewPackageName == null) return null;
+        PackageManager pm = context.getPackageManager();
+        try {
+            return pm.getPackageInfo(webviewPackageName, 0);
+        } catch (PackageManager.NameNotFoundException e) {
+            return null;
         }
     }
 
@@ -202,7 +333,58 @@
         return new WebViewProviderAdapter(createProvider(webview));
     }
 
-    private static WebViewProviderFactoryAdapter getFactory() {
+    /**
+     * Creates a message channel to communicate with JS and returns the message
+     * ports that represent the endpoints of this message channel. The HTML5 message
+     * channel functionality is described
+     * <a href="https://html.spec.whatwg.org/multipage/comms.html#messagechannel">here
+     * </a>
+     *
+     * <p>The returned message channels are entangled and already in started state.
+     *
+     * @return an array of size two, containing the two message ports that form the message channel.
+     */
+    public static @NonNull WebMessagePortCompat[] createWebMessageChannel(
+            @NonNull WebView webview) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            return WebMessagePortImpl.portsToCompat(webview.createWebMessageChannel());
+        } else { // TODO(gsennton) add reflection-based implementation
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    /**
+     * Post a message to main frame. The embedded application can restrict the
+     * messages to a certain target origin. See
+     * <a href="https://html.spec.whatwg.org/multipage/comms.html#posting-messages">
+     * HTML5 spec</a> for how target origin can be used.
+     * <p>
+     * A target origin can be set as a wildcard ("*"). However this is not recommended.
+     * See the page above for security issues.
+     *
+     * @param message the WebMessage
+     * @param targetOrigin the target origin.
+     */
+    public static void postWebMessage(@NonNull WebView webview, @NonNull WebMessageCompat message,
+            @NonNull Uri targetOrigin) {
+        // The wildcard ("*") Uri was first supported in WebView 60, see
+        // crrev/5ec5b67cbab33cea51b0ee11a286c885c2de4d5d, so on some Android versions using "*"
+        // won't work. WebView has always supported using an empty Uri "" as a wildcard - so convert
+        // "*" into "" here.
+        if (WILDCARD_URI.equals(targetOrigin)) {
+            targetOrigin = EMPTY_URI;
+        }
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            webview.postWebMessage(
+                    WebMessagePortImpl.compatToFrameworkMessage(message),
+                    targetOrigin);
+        } else { // TODO(gsennton) add reflection-based implementation
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    private static WebViewProviderFactory getFactory() {
         return WebViewGlueCommunicator.getFactory();
     }
 
@@ -213,11 +395,11 @@
     @SuppressWarnings("NewApi")
     private static void checkThread(WebView webview) {
         if (BuildCompat.isAtLeastP()) {
-            if (webview.getLooper() != Looper.myLooper()) {
+            if (webview.getWebViewLooper() != Looper.myLooper()) {
                 throw new RuntimeException("A WebView method was called on thread '"
                         + Thread.currentThread().getName() + "'. "
                         + "All WebView methods must be called on the same thread. "
-                        + "(Expected Looper " + webview.getLooper() + " called on "
+                        + "(Expected Looper " + webview.getWebViewLooper() + " called on "
                         + Looper.myLooper() + ", FYI main Looper is " + Looper.getMainLooper()
                         + ")");
             }
diff --git a/webkit/src/main/java/androidx/webkit/WebViewFeature.java b/webkit/src/main/java/androidx/webkit/WebViewFeature.java
new file mode 100644
index 0000000..e750295
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/WebViewFeature.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.content.Context;
+import android.webkit.ValueCallback;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebSettings;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.StringDef;
+import androidx.webkit.internal.WebViewFeatureInternal;
+
+import org.chromium.support_lib_boundary.util.Features;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+
+/**
+ * Utility class for checking which WebView Support Library features are supported on the device.
+ */
+public class WebViewFeature {
+
+    private WebViewFeature() {}
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @StringDef(value = {
+            VISUAL_STATE_CALLBACK,
+            OFF_SCREEN_PRERASTER,
+            SAFE_BROWSING_ENABLE,
+            DISABLED_ACTION_MODE_MENU_ITEMS,
+            START_SAFE_BROWSING,
+            SAFE_BROWSING_WHITELIST,
+            SAFE_BROWSING_PRIVACY_POLICY_URL,
+            SERVICE_WORKER_BASIC_USAGE,
+            SERVICE_WORKER_CACHE_MODE,
+            SERVICE_WORKER_CONTENT_ACCESS,
+            SERVICE_WORKER_FILE_ACCESS,
+            SERVICE_WORKER_BLOCK_NETWORK_LOADS,
+            SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST,
+            RECEIVE_WEB_RESOURCE_ERROR,
+            RECEIVE_HTTP_ERROR,
+            SHOULD_OVERRIDE_WITH_REDIRECTS,
+            SAFE_BROWSING_HIT,
+            WEB_RESOURCE_REQUEST_IS_REDIRECT,
+            WEB_RESOURCE_ERROR_GET_DESCRIPTION,
+            WEB_RESOURCE_ERROR_GET_CODE,
+            SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY,
+            SAFE_BROWSING_RESPONSE_PROCEED,
+            SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @Target({ElementType.PARAMETER, ElementType.METHOD})
+    public @interface WebViewSupportFeature {}
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link androidx.webkit.WebViewCompat#postVisualStateCallback(android.webkit.WebView, long,
+     * WebViewCompat.VisualStateCallback)}, and {@link
+     * WebViewClientCompat#onPageCommitVisible(
+     * android.webkit.WebView, String)}.
+     */
+    public static final String VISUAL_STATE_CALLBACK = Features.VISUAL_STATE_CALLBACK;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link androidx.webkit.WebSettingsCompat#getOffscreenPreRaster(WebSettings)}, and
+     * {@link androidx.webkit.WebSettingsCompat#setOffscreenPreRaster(WebSettings, boolean)}.
+     */
+    public static final String OFF_SCREEN_PRERASTER = Features.OFF_SCREEN_PRERASTER;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link androidx.webkit.WebSettingsCompat#getSafeBrowsingEnabled(WebSettings)}, and
+     * {@link androidx.webkit.WebSettingsCompat#setSafeBrowsingEnabled(WebSettings, boolean)}.
+     */
+    public static final String SAFE_BROWSING_ENABLE = Features.SAFE_BROWSING_ENABLE;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link androidx.webkit.WebSettingsCompat#getDisabledActionModeMenuItems(WebSettings)}, and
+     * {@link androidx.webkit.WebSettingsCompat#setDisabledActionModeMenuItems(WebSettings, int)}.
+     */
+    public static final String DISABLED_ACTION_MODE_MENU_ITEMS =
+            Features.DISABLED_ACTION_MODE_MENU_ITEMS;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link androidx.webkit.WebViewCompat#startSafeBrowsing(Context, ValueCallback)}.
+     */
+    public static final String START_SAFE_BROWSING = Features.START_SAFE_BROWSING;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link androidx.webkit.WebViewCompat#setSafeBrowsingWhitelist(List, ValueCallback)}.
+     */
+    public static final String SAFE_BROWSING_WHITELIST = Features.SAFE_BROWSING_WHITELIST;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link WebViewCompat#getSafeBrowsingPrivacyPolicyUrl()}.
+     */
+    public static final String SAFE_BROWSING_PRIVACY_POLICY_URL =
+            Features.SAFE_BROWSING_PRIVACY_POLICY_URL;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link ServiceWorkerControllerCompat#getInstance()}.
+     */
+    public static final String SERVICE_WORKER_BASIC_USAGE = Features.SERVICE_WORKER_BASIC_USAGE;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link ServiceWorkerWebSettingsCompat#getCacheMode()}, and
+     * {@link ServiceWorkerWebSettingsCompat#setCacheMode(int)}.
+     */
+    public static final String SERVICE_WORKER_CACHE_MODE = Features.SERVICE_WORKER_CACHE_MODE;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link ServiceWorkerWebSettingsCompat#getAllowContentAccess()}, and
+     * {@link ServiceWorkerWebSettingsCompat#setAllowContentAccess(boolean)}.
+     */
+    public static final String SERVICE_WORKER_CONTENT_ACCESS =
+            Features.SERVICE_WORKER_CONTENT_ACCESS;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link ServiceWorkerWebSettingsCompat#getAllowFileAccess()}, and
+     * {@link ServiceWorkerWebSettingsCompat#setAllowFileAccess(boolean)}.
+     */
+    public static final String SERVICE_WORKER_FILE_ACCESS = Features.SERVICE_WORKER_FILE_ACCESS;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link ServiceWorkerWebSettingsCompat#getBlockNetworkLoads()}, and
+     * {@link ServiceWorkerWebSettingsCompat#setBlockNetworkLoads(boolean)}.
+     */
+    public static final String SERVICE_WORKER_BLOCK_NETWORK_LOADS =
+            Features.SERVICE_WORKER_BLOCK_NETWORK_LOADS;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link ServiceWorkerClientCompat#shouldInterceptRequest(WebResourceRequest)}.
+     */
+    public static final String SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST =
+            Features.SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link WebViewClientCompat#onReceivedError(android.webkit.WebView, WebResourceRequest,
+     * WebResourceErrorCompat)}.
+     */
+    public static final String RECEIVE_WEB_RESOURCE_ERROR = Features.RECEIVE_WEB_RESOURCE_ERROR;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link WebViewClientCompat#onReceivedHttpError(android.webkit.WebView, WebResourceRequest,
+     * WebResourceResponse)}.
+     */
+    public static final String RECEIVE_HTTP_ERROR = Features.RECEIVE_HTTP_ERROR;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link WebViewClientCompat#shouldOverrideUrlLoading(android.webkit.WebView,
+     * WebResourceRequest)}.
+     */
+    public static final String SHOULD_OVERRIDE_WITH_REDIRECTS =
+            Features.SHOULD_OVERRIDE_WITH_REDIRECTS;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link WebViewClientCompat#onSafeBrowsingHit(android.webkit.WebView,
+     * WebResourceRequest, int, SafeBrowsingResponseCompat)}.
+     */
+    public static final String SAFE_BROWSING_HIT = Features.SAFE_BROWSING_HIT;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link WebResourceRequestCompat#isRedirect(WebResourceRequest)}.
+     */
+    public static final String WEB_RESOURCE_REQUEST_IS_REDIRECT =
+            Features.WEB_RESOURCE_REQUEST_IS_REDIRECT;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link WebResourceErrorCompat#getDescription()}.
+     */
+    public static final String WEB_RESOURCE_ERROR_GET_DESCRIPTION =
+            Features.WEB_RESOURCE_ERROR_GET_DESCRIPTION;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link WebResourceErrorCompat#getErrorCode()}.
+     */
+    public static final String WEB_RESOURCE_ERROR_GET_CODE =
+            Features.WEB_RESOURCE_ERROR_GET_CODE;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link SafeBrowsingResponseCompat#backToSafety(boolean)}.
+     */
+    public static final String SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY =
+            Features.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link SafeBrowsingResponseCompat#proceed(boolean)}.
+     */
+    public static final String SAFE_BROWSING_RESPONSE_PROCEED =
+            Features.SAFE_BROWSING_RESPONSE_PROCEED;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link SafeBrowsingResponseCompat#showInterstitial(boolean)}.
+     */
+    public static final String SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL =
+            Features.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL;
+
+    /**
+     * Return whether a feature is supported at run-time. This depends on the Android version of the
+     * device and the WebView APK on the device.
+     */
+    public static boolean isFeatureSupported(@NonNull @WebViewSupportFeature String feature) {
+        WebViewFeatureInternal webviewFeature = WebViewFeatureInternal.getFeature(feature);
+        return webviewFeature.isSupportedByFramework() || webviewFeature.isSupportedByWebView();
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/FrameworkServiceWorkerClient.java b/webkit/src/main/java/androidx/webkit/internal/FrameworkServiceWorkerClient.java
new file mode 100644
index 0000000..c28346e
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/FrameworkServiceWorkerClient.java
@@ -0,0 +1,44 @@
+/*
+ * 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.ServiceWorkerClient;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+
+import androidx.annotation.RequiresApi;
+import androidx.webkit.ServiceWorkerClientCompat;
+
+/**
+ * A shim class that implements {@link ServiceWorkerClient} by delegating to a
+ * {@link ServiceWorkerClientCompat}.
+ * This class is used on up-to-date devices to avoid using reflection to call into WebView APK code.
+ */
+@RequiresApi(Build.VERSION_CODES.N)
+public class FrameworkServiceWorkerClient extends ServiceWorkerClient {
+    private final ServiceWorkerClientCompat mImpl;
+
+    public FrameworkServiceWorkerClient(ServiceWorkerClientCompat impl) {
+        mImpl = impl;
+    }
+
+    @Override
+    public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
+        return mImpl.shouldInterceptRequest(request);
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/IncompatibleApkWebViewProviderFactory.java b/webkit/src/main/java/androidx/webkit/internal/IncompatibleApkWebViewProviderFactory.java
new file mode 100644
index 0000000..71d5768
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/IncompatibleApkWebViewProviderFactory.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit.internal;
+
+import android.webkit.WebView;
+
+import org.chromium.support_lib_boundary.ServiceWorkerControllerBoundaryInterface;
+import org.chromium.support_lib_boundary.StaticsBoundaryInterface;
+import org.chromium.support_lib_boundary.WebViewProviderBoundaryInterface;
+import org.chromium.support_lib_boundary.WebkitToCompatConverterBoundaryInterface;
+
+/**
+ * This is a stub class used when the WebView Support Library is invoked on a device incompatible
+ * with the library (either a pre-L device or a device without a compatible WebView APK).
+ * The only method in this class that should be called is {@link #getWebViewFeatures()}.
+ */
+public class IncompatibleApkWebViewProviderFactory implements WebViewProviderFactory {
+    private static final String[] EMPTY_STRING_ARRAY = new String[0];
+    private static final String UNSUPPORTED_EXCEPTION_EXPLANATION =
+            "This should never happen, if this method was called it means we're trying to reach "
+            + "into WebView APK code on an incompatible device. This most likely means the current "
+            + "method is being called too early, or is being called on start-up rather than lazily";
+
+    @Override
+    public WebViewProviderBoundaryInterface createWebView(WebView webview) {
+        throw new UnsupportedOperationException(UNSUPPORTED_EXCEPTION_EXPLANATION);
+    }
+
+    @Override
+    public WebkitToCompatConverterBoundaryInterface getWebkitToCompatConverter() {
+        throw new UnsupportedOperationException(UNSUPPORTED_EXCEPTION_EXPLANATION);
+    }
+
+    @Override
+    public StaticsBoundaryInterface getStatics() {
+        throw new UnsupportedOperationException(UNSUPPORTED_EXCEPTION_EXPLANATION);
+    }
+
+    @Override
+    public String[] getWebViewFeatures() {
+        return EMPTY_STRING_ARRAY;
+    }
+
+    @Override
+    public ServiceWorkerControllerBoundaryInterface getServiceWorkerController() {
+        throw new UnsupportedOperationException(UNSUPPORTED_EXCEPTION_EXPLANATION);
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerClientAdapter.java b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerClientAdapter.java
new file mode 100644
index 0000000..f96bbd1
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerClientAdapter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit.internal;
+
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+
+import androidx.webkit.ServiceWorkerClientCompat;
+
+import org.chromium.support_lib_boundary.ServiceWorkerClientBoundaryInterface;
+import org.chromium.support_lib_boundary.util.Features;
+
+/**
+ * Adapter between {@link ServiceWorkerClientCompat} and
+ * {@link ServiceWorkerClientBoundaryInterface} (the corresponding interface shared with the support
+ * library glue in the WebView APK).
+ */
+public class ServiceWorkerClientAdapter implements ServiceWorkerClientBoundaryInterface {
+    private final ServiceWorkerClientCompat mClient;
+
+    public ServiceWorkerClientAdapter(ServiceWorkerClientCompat client) {
+        mClient = client;
+    }
+
+    @Override
+    public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
+        return mClient.shouldInterceptRequest(request);
+    }
+
+    @Override
+    public String[] getSupportedFeatures() {
+        return new String[] { Features.SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST };
+    }
+}
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..3787a92
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerControllerImpl.java
@@ -0,0 +1,94 @@
+/*
+ * 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.annotation.RequiresApi;
+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.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 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());
+        } else if (feature.isSupportedByWebView()) {
+            mFrameworksImpl = null;
+            mBoundaryInterface = WebViewGlueCommunicator.getFactory().getServiceWorkerController();
+            mWebSettings = new ServiceWorkerWebSettingsImpl(
+                    mBoundaryInterface.getServiceWorkerWebSettings());
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    @RequiresApi(24)
+    private ServiceWorkerController getFrameworksImpl() {
+        if (mFrameworksImpl == null) {
+            mFrameworksImpl = ServiceWorkerController.getInstance();
+        }
+        return mFrameworksImpl;
+    }
+
+    private ServiceWorkerControllerBoundaryInterface getBoundaryInterface() {
+        if (mBoundaryInterface == null) {
+            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()) {
+            getFrameworksImpl().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/ServiceWorkerWebSettingsImpl.java b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerWebSettingsImpl.java
new file mode 100644
index 0000000..cad3ccb
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerWebSettingsImpl.java
@@ -0,0 +1,195 @@
+/*
+ * 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.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.webkit.ServiceWorkerWebSettingsCompat;
+
+import org.chromium.support_lib_boundary.ServiceWorkerWebSettingsBoundaryInterface;
+import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Proxy;
+
+/**
+ * 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 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(@NonNull ServiceWorkerWebSettings settings) {
+        mFrameworksImpl = settings;
+    }
+
+    public ServiceWorkerWebSettingsImpl(@NonNull InvocationHandler invocationHandler) {
+        mBoundaryInterface = BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+                ServiceWorkerWebSettingsBoundaryInterface.class, invocationHandler);
+    }
+
+    @RequiresApi(24)
+    private ServiceWorkerWebSettings getFrameworksImpl() {
+        if (mFrameworksImpl == null) {
+            mFrameworksImpl =
+                    WebViewGlueCommunicator.getCompatConverter().convertServiceWorkerSettings(
+                            Proxy.getInvocationHandler(mBoundaryInterface));
+        }
+        return mFrameworksImpl;
+    }
+
+    private ServiceWorkerWebSettingsBoundaryInterface getBoundaryInterface() {
+        if (mBoundaryInterface == null) {
+            // If the boundary interface is null we must have a working frameworks implementation to
+            // convert into a boundary interface.
+            // The case of the boundary interface being null here only occurs if we created the
+            // ServiceWorkerWebSettingsImpl using a frameworks API, but now want to call an API on
+            // the ServiceWorkerWebSettingsImpl that is only supported by the support library glue.
+            // This could happen for example if we introduce a new ServiceWorkerWebSettings API in
+            // level 30 and we run the support library on an N device (whose framework supports
+            // ServiceWorkerWebSettings).
+            mBoundaryInterface = BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+                    ServiceWorkerWebSettingsBoundaryInterface.class,
+                    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()) {
+            getFrameworksImpl().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 getFrameworksImpl().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()) {
+            getFrameworksImpl().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 getFrameworksImpl().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()) {
+            getFrameworksImpl().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 getFrameworksImpl().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()) {
+            getFrameworksImpl().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 getFrameworksImpl().getBlockNetworkLoads();
+        } else if (feature.isSupportedByWebView()) {
+            return getBoundaryInterface().getBlockNetworkLoads();
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebMessagePortImpl.java b/webkit/src/main/java/androidx/webkit/internal/WebMessagePortImpl.java
new file mode 100644
index 0000000..2de5105
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/WebMessagePortImpl.java
@@ -0,0 +1,145 @@
+/*
+ * 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.os.Build;
+import android.os.Handler;
+import android.webkit.WebMessage;
+import android.webkit.WebMessagePort;
+
+import androidx.annotation.RequiresApi;
+import androidx.webkit.WebMessageCompat;
+import androidx.webkit.WebMessagePortCompat;
+
+/**
+ * Implementation of {@link WebMessagePortCompat}.
+ * This class uses either the framework, the WebView APK, or both, to implement
+ * {@link WebMessagePortCompat} functionality.
+ */
+public class WebMessagePortImpl extends WebMessagePortCompat {
+    private final WebMessagePort mFrameworksImpl;
+    // TODO(gsennton) add WebMessagePortBoundaryInterface variable
+
+    public WebMessagePortImpl(WebMessagePort frameworksImpl) {
+        mFrameworksImpl = frameworksImpl;
+    }
+
+    @SuppressLint("NewApi")
+    @Override
+    public void postMessage(WebMessageCompat message) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            mFrameworksImpl.postMessage(compatToFrameworkMessage(message));
+        } else { // TODO(gsennton) add reflection-based implementation
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    @SuppressLint("NewApi")
+    @Override
+    public void close() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            mFrameworksImpl.close();
+        } else { // TODO(gsennton) add reflection-based implementation
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    @Override
+    public void setWebMessageCallback(final WebMessageCallbackCompat callback) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            mFrameworksImpl.setWebMessageCallback(new WebMessagePort.WebMessageCallback() {
+                @Override
+                @SuppressWarnings("NewApi")
+                public void onMessage(WebMessagePort port, WebMessage message) {
+                    callback.onMessage(new WebMessagePortImpl(port),
+                            frameworkMessageToCompat(message));
+                }
+            });
+        } else { // TODO(gsennton) add reflection-based implementation
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    @Override
+    public void setWebMessageCallback(Handler handler, final WebMessageCallbackCompat callback) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            mFrameworksImpl.setWebMessageCallback(new WebMessagePort.WebMessageCallback() {
+                @Override
+                @SuppressWarnings("NewApi")
+                public void onMessage(WebMessagePort port, WebMessage message) {
+                    callback.onMessage(new WebMessagePortImpl(port),
+                            frameworkMessageToCompat(message));
+                }
+            }, handler);
+        } else { // TODO(gsennton) add reflection-based implementation
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    @Override
+    public WebMessagePort getFrameworkPort() {
+        return mFrameworksImpl;
+    }
+
+    /**
+     * Convert an array of {@link WebMessagePort} objects into an array containing objects of the
+     * corresponding support library class {@link WebMessagePortCompat}.
+     */
+    public static WebMessagePortCompat[] portsToCompat(WebMessagePort[] ports) {
+        if (ports == null) return null;
+        WebMessagePortCompat[] compatPorts = new WebMessagePortCompat[ports.length];
+        for (int n = 0; n < ports.length; n++) {
+            compatPorts[n] = new WebMessagePortImpl(ports[n]);
+        }
+        return compatPorts;
+    }
+
+    /**
+     * Convert an array of {@link WebMessagePortCompat} objects into an array containing objects of
+     * the corresponding framework class {@link WebMessagePort}.
+     */
+    public static WebMessagePort[] compatToPorts(WebMessagePortCompat[] compatPorts) {
+        if (compatPorts == null) return null;
+        WebMessagePort[] ports = new WebMessagePort[compatPorts.length];
+        for (int n = 0; n < ports.length; n++) {
+            ports[n] = compatPorts[n].getFrameworkPort();
+        }
+        return ports;
+    }
+
+    /**
+     * Convert a {@link WebMessageCompat} into the corresponding framework class {@link WebMessage}.
+     */
+    @RequiresApi(23)
+    public static WebMessage compatToFrameworkMessage(WebMessageCompat message) {
+        return new WebMessage(
+                message.getData(),
+                compatToPorts(message.getPorts()));
+    }
+
+    /**
+     * Convert a {@link WebMessage} into the corresponding support library class
+     * {@link WebMessageCompat}.
+     */
+    @RequiresApi(23)
+    public static WebMessageCompat frameworkMessageToCompat(WebMessage message) {
+        return new WebMessageCompat(
+                message.getData(),
+                portsToCompat(message.getPorts()));
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebResourceErrorImpl.java b/webkit/src/main/java/androidx/webkit/internal/WebResourceErrorImpl.java
new file mode 100644
index 0000000..a27f61c
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/WebResourceErrorImpl.java
@@ -0,0 +1,106 @@
+/*
+ * 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.WebResourceError;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.webkit.WebResourceErrorCompat;
+import androidx.webkit.WebViewFeature;
+
+import org.chromium.support_lib_boundary.WebResourceErrorBoundaryInterface;
+import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Proxy;
+
+/**
+ * Implementation of {@link WebResourceErrorCompat}.
+ * This class uses either the framework, the WebView APK, or both, to implement
+ * {@link WebResourceErrorCompat} functionality.
+ *
+ */
+public class WebResourceErrorImpl extends WebResourceErrorCompat {
+    /**
+     * Frameworks implementation - do not use this directly, instead use
+     * {@link #getFrameworksImpl()} to ensure this variable has been instantiated correctly.
+     */
+    private WebResourceError mFrameworksImpl;
+
+    /**
+     * Support library glue implementation - do not use this directly, instead use
+     * {@link #getBoundaryInterface()} to ensure this variable has been instantiated correctly.
+     */
+    private WebResourceErrorBoundaryInterface mBoundaryInterface;
+
+    public WebResourceErrorImpl(@NonNull InvocationHandler invocationHandler) {
+        mBoundaryInterface = BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+                WebResourceErrorBoundaryInterface.class, invocationHandler);
+    }
+
+    public WebResourceErrorImpl(@NonNull WebResourceError error) {
+        mFrameworksImpl = error;
+    }
+
+    @RequiresApi(23)
+    private WebResourceError getFrameworksImpl() {
+        if (mFrameworksImpl == null) {
+            mFrameworksImpl = WebViewGlueCommunicator.getCompatConverter().convertWebResourceError(
+                    Proxy.getInvocationHandler(mBoundaryInterface));
+        }
+        return mFrameworksImpl;
+    }
+
+    private WebResourceErrorBoundaryInterface getBoundaryInterface() {
+        if (mBoundaryInterface == null) {
+            mBoundaryInterface = BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+                    WebResourceErrorBoundaryInterface.class,
+                    WebViewGlueCommunicator.getCompatConverter().convertWebResourceError(
+                            mFrameworksImpl));
+        }
+        return mBoundaryInterface;
+    }
+
+    @SuppressLint("NewApi")
+    @Override
+    public int getErrorCode() {
+        final WebViewFeatureInternal feature =
+                WebViewFeatureInternal.getFeature(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE);
+        if (feature.isSupportedByFramework()) {
+            return getFrameworksImpl().getErrorCode();
+        } else if (feature.isSupportedByWebView()) {
+            return getBoundaryInterface().getErrorCode();
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    @SuppressLint("NewApi")
+    @Override
+    public CharSequence getDescription() {
+        final WebViewFeatureInternal feature = WebViewFeatureInternal.getFeature(
+                        WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION);
+        if (feature.isSupportedByFramework()) {
+            return getFrameworksImpl().getDescription();
+        } else if (feature.isSupportedByWebView()) {
+            return getBoundaryInterface().getDescription();
+        }
+        throw WebViewFeatureInternal.getUnsupportedOperationException();
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebResourceRequestAdapter.java b/webkit/src/main/java/androidx/webkit/internal/WebResourceRequestAdapter.java
new file mode 100644
index 0000000..0d6a05d
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/WebResourceRequestAdapter.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit.internal;
+
+import android.webkit.WebResourceRequest;
+
+import org.chromium.support_lib_boundary.WebResourceRequestBoundaryInterface;
+
+/**
+ * Adapter between {@link androidx.webkit.WebResourceRequestCompat} and
+ * {@link org.chromium.support_lib_boundary.WebResourceRequestBoundaryInterface}.
+ */
+public class WebResourceRequestAdapter {
+    private final WebResourceRequestBoundaryInterface mBoundaryInterface;
+
+    public WebResourceRequestAdapter(WebResourceRequestBoundaryInterface boundaryInterface) {
+        mBoundaryInterface = boundaryInterface;
+    }
+
+    /**
+     * Adapter method for
+     * {@link androidx.webkit.WebResourceRequestCompat#isRedirect(WebResourceRequest)}.
+     */
+    public boolean isRedirect() {
+        return mBoundaryInterface.isRedirect();
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java b/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
new file mode 100644
index 0000000..916da9d
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
@@ -0,0 +1,258 @@
+/*
+ * 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.content.Context;
+import android.os.Build;
+import android.webkit.ValueCallback;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebSettings;
+
+import androidx.webkit.ServiceWorkerClientCompat;
+import androidx.webkit.WebResourceRequestCompat;
+import androidx.webkit.WebViewClientCompat;
+import androidx.webkit.WebViewCompat;
+import androidx.webkit.WebViewFeature;
+import androidx.webkit.WebViewFeature.WebViewSupportFeature;
+
+import java.util.List;
+
+/**
+ * Enum representing a WebView feature, this provides functionality for determining whether a
+ * feature is supported by the current framework and/or WebView APK.
+ */
+public enum WebViewFeatureInternal {
+    /**
+     * This feature covers
+     * {@link androidx.webkit.WebViewCompat#postVisualStateCallback(android.webkit.WebView, long,
+     * androidx.webkit.WebViewCompat.VisualStateCallback)}, and
+     * {@link WebViewClientCompat#onPageCommitVisible(android.webkit.WebView, String)}.
+     */
+    VISUAL_STATE_CALLBACK_FEATURE(WebViewFeature.VISUAL_STATE_CALLBACK, Build.VERSION_CODES.M),
+
+    /**
+     * This feature covers
+     * {@link androidx.webkit.WebSettingsCompat#getOffscreenPreRaster(WebSettings)}, and
+     * {@link androidx.webkit.WebSettingsCompat#setOffscreenPreRaster(WebSettings, boolean)}.
+     */
+    OFF_SCREEN_PRERASTER(WebViewFeature.OFF_SCREEN_PRERASTER, Build.VERSION_CODES.M),
+
+    /**
+     * This feature covers
+     * {@link androidx.webkit.WebSettingsCompat#getSafeBrowsingEnabled(WebSettings)}, and
+     * {@link androidx.webkit.WebSettingsCompat#setSafeBrowsingEnabled(WebSettings, boolean)}.
+     */
+    SAFE_BROWSING_ENABLE(WebViewFeature.SAFE_BROWSING_ENABLE, Build.VERSION_CODES.O),
+
+    /**
+     * This feature covers
+     * {@link androidx.webkit.WebSettingsCompat#getDisabledActionModeMenuItems(WebSettings)}, and
+     * {@link androidx.webkit.WebSettingsCompat#setDisabledActionModeMenuItems(WebSettings, int)}.
+     */
+    DISABLED_ACTION_MODE_MENU_ITEMS(WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS,
+                                    Build.VERSION_CODES.N),
+
+    /**
+     * This feature covers
+     * {@link androidx.webkit.WebViewCompat#startSafeBrowsing(Context, ValueCallback)}.
+     */
+    START_SAFE_BROWSING(WebViewFeature.START_SAFE_BROWSING, Build.VERSION_CODES.O_MR1),
+
+    /**
+     * This feature covers
+     * {@link androidx.webkit.WebViewCompat#setSafeBrowsingWhitelist(List, ValueCallback)}.
+     */
+    SAFE_BROWSING_WHITELIST(WebViewFeature.SAFE_BROWSING_WHITELIST, Build.VERSION_CODES.O_MR1),
+
+    /**
+     * This feature covers
+     * {@link WebViewCompat#getSafeBrowsingPrivacyPolicyUrl()}.
+     */
+    SAFE_BROWSING_PRIVACY_POLICY_URL(WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL,
+            Build.VERSION_CODES.O_MR1),
+
+    /**
+     * This feature covers
+     * {@link androidx.webkit.ServiceWorkerControllerCompat#getInstance()}.
+     */
+    SERVICE_WORKER_BASIC_USAGE(WebViewFeature.SERVICE_WORKER_BASIC_USAGE, Build.VERSION_CODES.N),
+
+    /**
+     * This feature covers
+     * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#getCacheMode()}, and
+     * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#setCacheMode(int)}.
+     */
+    SERVICE_WORKER_CACHE_MODE(WebViewFeature.SERVICE_WORKER_CACHE_MODE, Build.VERSION_CODES.N),
+
+    /**
+     * This feature covers
+     * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#getAllowContentAccess()}, and
+     * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#setAllowContentAccess(boolean)}.
+     */
+    SERVICE_WORKER_CONTENT_ACCESS(WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS,
+            Build.VERSION_CODES.N),
+
+    /**
+     * This feature covers
+     * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#getAllowFileAccess()}, and
+     * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#setAllowFileAccess(boolean)}.
+     */
+    SERVICE_WORKER_FILE_ACCESS(WebViewFeature.SERVICE_WORKER_FILE_ACCESS, Build.VERSION_CODES.N),
+
+    /**
+     * This feature covers
+     * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#getBlockNetworkLoads()}, and
+     * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#setBlockNetworkLoads(boolean)}.
+     */
+    SERVICE_WORKER_BLOCK_NETWORK_LOADS(WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS,
+            Build.VERSION_CODES.N),
+
+    /**
+     * This feature covers
+     * {@link ServiceWorkerClientCompat#shouldInterceptRequest(WebResourceRequest)}.
+     */
+    SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST(WebViewFeature.SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST,
+            Build.VERSION_CODES.N),
+
+    /**
+     * This feature covers
+     * {@link WebViewClientCompat#onReceivedError(android.webkit.WebView, WebResourceRequest,
+     * WebResourceErrorCompat)}.
+     */
+    RECEIVE_WEB_RESOURCE_ERROR(WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR, Build.VERSION_CODES.M),
+
+    /**
+     * This feature covers
+     * {@link WebViewClientCompat#onReceivedHttpError(android.webkit.WebView, WebResourceRequest,
+     * WebResourceResponse)}.
+     */
+    RECEIVE_HTTP_ERROR(WebViewFeature.RECEIVE_HTTP_ERROR, Build.VERSION_CODES.M),
+
+    /**
+     * This feature covers
+     * {@link WebViewClientCompat#shouldOverrideUrlLoading(android.webkit.WebView,
+     * WebResourceRequest)}.
+     */
+    SHOULD_OVERRIDE_WITH_REDIRECTS(WebViewFeature.SHOULD_OVERRIDE_WITH_REDIRECTS,
+            Build.VERSION_CODES.N),
+
+    /**
+     * This feature covers
+     * {@link WebViewClientCompat#onSafeBrowsingHit(android.webkit.WebView,
+     * WebResourceRequest, int, SafeBrowsingResponseCompat)}.
+     */
+    SAFE_BROWSING_HIT(WebViewFeature.SAFE_BROWSING_HIT, Build.VERSION_CODES.O_MR1),
+
+    /**
+     * This feature covers
+     * {@link WebResourceRequestCompat#isRedirect(WebResourceRequest)}.
+     */
+    WEB_RESOURCE_REQUEST_IS_REDIRECT(WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT,
+            Build.VERSION_CODES.N),
+
+    /**
+     * This feature covers
+     * {@link WebResourceErrorCompat#getDescription()}.
+     */
+    WEB_RESOURCE_ERROR_GET_DESCRIPTION(WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION,
+            Build.VERSION_CODES.M),
+
+    /**
+     * This feature covers
+     * {@link WebResourceErrorCompat#getErrorCode()}.
+     */
+    WEB_RESOURCE_ERROR_GET_CODE(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE,
+            Build.VERSION_CODES.M),
+
+    /**
+     * This feature covers
+     * {@link SafeBrowsingResponseCompat#backToSafety(boolean)}.
+     */
+    SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY(WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY,
+            Build.VERSION_CODES.O_MR1),
+
+    /**
+     * This feature covers
+     * {@link SafeBrowsingResponseCompat#proceed(boolean)}.
+     */
+    SAFE_BROWSING_RESPONSE_PROCEED(WebViewFeature.SAFE_BROWSING_RESPONSE_PROCEED,
+            Build.VERSION_CODES.O_MR1),
+
+    /**
+     * This feature covers
+     * {@link SafeBrowsingResponseCompat#showInterstitial(boolean)}.
+     */
+    SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL(
+            WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL,
+            Build.VERSION_CODES.O_MR1);
+
+    private final String mFeatureValue;
+    private final int mOsVersion;
+
+    WebViewFeatureInternal(@WebViewSupportFeature String featureValue, int osVersion) {
+        mFeatureValue = featureValue;
+        mOsVersion = osVersion;
+    }
+
+    /**
+     * Return the {@link WebViewFeatureInternal} corresponding to {@param feature}.
+     */
+    public static WebViewFeatureInternal getFeature(@WebViewSupportFeature String feature) {
+        for (WebViewFeatureInternal internalFeature : WebViewFeatureInternal.values()) {
+            if (internalFeature.mFeatureValue.equals(feature)) return internalFeature;
+        }
+        throw new RuntimeException("Unknown feature " + feature);
+    }
+
+    /**
+     * Return whether this {@link WebViewFeature} is supported by the framework of the current
+     * device.
+     */
+    public boolean isSupportedByFramework() {
+        return Build.VERSION.SDK_INT >= mOsVersion;
+    }
+
+    /**
+     * Return whether this {@link WebViewFeature} is supported by the current WebView APK.
+     */
+    public boolean isSupportedByWebView() {
+        String[] webviewFeatures = LAZY_HOLDER.WEBVIEW_APK_FEATURES;
+        for (String webviewFeature : webviewFeatures) {
+            if (webviewFeature.equals(mFeatureValue)) return true;
+        }
+        return false;
+    }
+
+    private static class LAZY_HOLDER {
+        static final String[] WEBVIEW_APK_FEATURES =
+                WebViewGlueCommunicator.getFactory().getWebViewFeatures();
+    }
+
+
+    public static String[] getWebViewApkFeaturesForTesting() {
+        return LAZY_HOLDER.WEBVIEW_APK_FEATURES;
+    }
+
+    /**
+     * Utility method for throwing an exception explaining that the feature the app trying to use
+     * isn't supported.
+     */
+    public static UnsupportedOperationException getUnsupportedOperationException() {
+        return new UnsupportedOperationException("This method is not supported by the current "
+                + "version of the framework and the current WebView APK");
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebViewGlueCommunicator.java b/webkit/src/main/java/androidx/webkit/internal/WebViewGlueCommunicator.java
index 1325ed0..ea4460f 100644
--- a/webkit/src/main/java/androidx/webkit/internal/WebViewGlueCommunicator.java
+++ b/webkit/src/main/java/androidx/webkit/internal/WebViewGlueCommunicator.java
@@ -16,6 +16,7 @@
 
 package androidx.webkit.internal;
 
+import android.os.Build;
 import android.webkit.WebView;
 
 import androidx.core.os.BuildCompat;
@@ -39,40 +40,57 @@
     /**
      * Fetch the one global support library WebViewProviderFactory from the WebView glue layer.
      */
-    public static WebViewProviderFactoryAdapter getFactory() {
+    public static WebViewProviderFactory getFactory() {
         return LAZY_FACTORY_HOLDER.INSTANCE;
     }
 
     public static WebkitToCompatConverter getCompatConverter() {
-        return LAZY_FACTORY_HOLDER.COMPAT_CONVERTER;
+        return LAZY_COMPAT_CONVERTER_HOLDER.INSTANCE;
     }
 
     private static class LAZY_FACTORY_HOLDER {
-        static final WebViewProviderFactoryAdapter INSTANCE =
-                new WebViewProviderFactoryAdapter(
-                        WebViewGlueCommunicator.createGlueProviderFactory());
-        static final WebkitToCompatConverter COMPAT_CONVERTER =
-                new WebkitToCompatConverter(
-                        INSTANCE.getWebkitToCompatConverter());
+        private static final WebViewProviderFactory INSTANCE =
+                        WebViewGlueCommunicator.createGlueProviderFactory();
     }
 
-    private static InvocationHandler fetchGlueProviderFactoryImpl() {
+    private static class LAZY_COMPAT_CONVERTER_HOLDER {
+        static final WebkitToCompatConverter INSTANCE = new WebkitToCompatConverter(
+                WebViewGlueCommunicator.getFactory().getWebkitToCompatConverter());
+    }
+
+    private static InvocationHandler fetchGlueProviderFactoryImpl() throws IllegalAccessException,
+            InvocationTargetException, ClassNotFoundException, NoSuchMethodException {
+        Class<?> glueFactoryProviderFetcherClass = Class.forName(
+                GLUE_FACTORY_PROVIDER_FETCHER_CLASS, false, getWebViewClassLoader());
+        Method createProviderFactoryMethod = glueFactoryProviderFetcherClass.getDeclaredMethod(
+                GLUE_FACTORY_PROVIDER_FETCHER_METHOD);
+        return (InvocationHandler) createProviderFactoryMethod.invoke(null);
+    }
+
+    private static WebViewProviderFactory createGlueProviderFactory() {
+        // We do not support pre-L devices since their WebView APKs cannot be updated.
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+            return new IncompatibleApkWebViewProviderFactory();
+        }
+        InvocationHandler invocationHandler;
         try {
-            Class<?> glueFactoryProviderFetcherClass = Class.forName(
-                    GLUE_FACTORY_PROVIDER_FETCHER_CLASS, false, getWebViewClassLoader());
-            Method createProviderFactoryMethod = glueFactoryProviderFetcherClass.getDeclaredMethod(
-                    GLUE_FACTORY_PROVIDER_FETCHER_METHOD);
-            return (InvocationHandler) createProviderFactoryMethod.invoke(null);
-        } catch (IllegalAccessException | InvocationTargetException | ClassNotFoundException
-                | NoSuchMethodException e) {
+            invocationHandler = fetchGlueProviderFactoryImpl();
+            // The only way we should fail to fetch the provider-factory is if the class we are
+            // calling into doesn't exist - any other kind of failure is unexpected and should cause
+            // a run-time exception.
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        } catch (InvocationTargetException e) {
+            throw new RuntimeException(e);
+        } catch (ClassNotFoundException e) {
+            // If WebView APK support library glue entry point doesn't exist then return a Provider
+            // factory that declares that there are no features available.
+            return new IncompatibleApkWebViewProviderFactory();
+        } catch (NoSuchMethodException e) {
             throw new RuntimeException(e);
         }
-    }
-
-    private static WebViewProviderFactoryBoundaryInterface createGlueProviderFactory() {
-        InvocationHandler invocationHandler = fetchGlueProviderFactoryImpl();
-        return BoundaryInterfaceReflectionUtil.castToSuppLibClass(
-                WebViewProviderFactoryBoundaryInterface.class, invocationHandler);
+        return new WebViewProviderFactoryAdapter(BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+                WebViewProviderFactoryBoundaryInterface.class, invocationHandler));
     }
 
     /**
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactory.java b/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactory.java
new file mode 100644
index 0000000..5e4669a
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactory.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit.internal;
+
+import android.webkit.WebView;
+
+import org.chromium.support_lib_boundary.ServiceWorkerControllerBoundaryInterface;
+import org.chromium.support_lib_boundary.StaticsBoundaryInterface;
+import org.chromium.support_lib_boundary.WebViewProviderBoundaryInterface;
+import org.chromium.support_lib_boundary.WebkitToCompatConverterBoundaryInterface;
+
+/**
+ * Interface representing {@link android.webkit.WebViewProviderFactory}.
+ * On device with a compatible WebView APK this interface is implemented by a class defined in the
+ * WebView APK itself.
+ * On devices without a compatible WebView APK this interface is implemented by a stub class
+ * {@link androidx.webkit.internal.IncompatibleWebViewProviderFactory}.
+ */
+public interface WebViewProviderFactory {
+    /**
+     * Create a support library version of {@link android.webkit.WebViewProvider}.
+     */
+    WebViewProviderBoundaryInterface createWebView(WebView webview);
+
+    /**
+     * Create the boundary interface for {@link androidx.webkit.internal.WebkitToCompatConverter}
+     * which converts android.webkit classes into their corresponding support library classes.
+     */
+    WebkitToCompatConverterBoundaryInterface getWebkitToCompatConverter();
+
+    /**
+     * Fetch the boundary interface representing
+     * {@link android.webkit.WebViewFactoryProvider#Statics}.
+     */
+    StaticsBoundaryInterface getStatics();
+
+    /**
+     * Fetch the features supported by the current WebView APK.
+     */
+    String[] getWebViewFeatures();
+
+    /**
+     * Fetch the boundary interface representing {@link android.webkit.ServiceWorkerController}.
+     */
+    ServiceWorkerControllerBoundaryInterface getServiceWorkerController();
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactoryAdapter.java b/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactoryAdapter.java
index 62ce41e..43e5eae 100644
--- a/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactoryAdapter.java
+++ b/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactoryAdapter.java
@@ -18,6 +18,7 @@
 
 import android.webkit.WebView;
 
+import org.chromium.support_lib_boundary.ServiceWorkerControllerBoundaryInterface;
 import org.chromium.support_lib_boundary.StaticsBoundaryInterface;
 import org.chromium.support_lib_boundary.WebViewProviderBoundaryInterface;
 import org.chromium.support_lib_boundary.WebViewProviderFactoryBoundaryInterface;
@@ -28,7 +29,7 @@
  * Adapter for WebViewProviderFactoryBoundaryInterface providing static WebView functionality
  * similar to that provided by {@link android.webkit.WebViewFactoryProvider}.
  */
-public class WebViewProviderFactoryAdapter {
+public class WebViewProviderFactoryAdapter implements WebViewProviderFactory {
     WebViewProviderFactoryBoundaryInterface mImpl;
 
     public WebViewProviderFactoryAdapter(WebViewProviderFactoryBoundaryInterface impl) {
@@ -40,6 +41,7 @@
      * {@link android.webkit.WebViewProvider} - the class used to implement
      * {@link androidx.webkit.WebViewCompat}.
      */
+    @Override
     public WebViewProviderBoundaryInterface createWebView(WebView webview) {
         return BoundaryInterfaceReflectionUtil.castToSuppLibClass(
                 WebViewProviderBoundaryInterface.class, mImpl.createWebView(webview));
@@ -50,6 +52,7 @@
      * {@link androidx.webkit.internal.WebkitToCompatConverter}, which converts android.webkit
      * classes into their corresponding support library classes.
      */
+    @Override
     public WebkitToCompatConverterBoundaryInterface getWebkitToCompatConverter() {
         return BoundaryInterfaceReflectionUtil.castToSuppLibClass(
                 WebkitToCompatConverterBoundaryInterface.class, mImpl.getWebkitToCompatConverter());
@@ -59,8 +62,27 @@
      * Adapter method for fetching the support library class representing
      * {@link android.webkit.WebViewFactoryProvider#Statics}.
      */
+    @Override
     public StaticsBoundaryInterface getStatics() {
         return BoundaryInterfaceReflectionUtil.castToSuppLibClass(
                 StaticsBoundaryInterface.class, mImpl.getStatics());
     }
+
+    /**
+     * Adapter method for fetching the features supported by the current WebView APK.
+     */
+    @Override
+    public String[] getWebViewFeatures() {
+        return mImpl.getSupportedFeatures();
+    }
+
+    /**
+     * Adapter method for fetching the support library class representing
+     * {@link android.webkit.ServiceWorkerController}.
+     */
+    @Override
+    public ServiceWorkerControllerBoundaryInterface getServiceWorkerController() {
+        return BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+                ServiceWorkerControllerBoundaryInterface.class, mImpl.getServiceWorkerController());
+    }
 }
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java b/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java
index a07cf07..2e18bb9 100644
--- a/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java
+++ b/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java
@@ -16,12 +16,22 @@
 
 package androidx.webkit.internal;
 
+import android.webkit.ServiceWorkerWebSettings;
+import android.webkit.WebResourceError;
+import android.webkit.WebResourceRequest;
 import android.webkit.WebSettings;
 
+import androidx.annotation.RequiresApi;
+import androidx.webkit.WebResourceErrorCompat;
+
+import org.chromium.support_lib_boundary.ServiceWorkerWebSettingsBoundaryInterface;
+import org.chromium.support_lib_boundary.WebResourceRequestBoundaryInterface;
 import org.chromium.support_lib_boundary.WebSettingsBoundaryInterface;
 import org.chromium.support_lib_boundary.WebkitToCompatConverterBoundaryInterface;
 import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
 
+import java.lang.reflect.InvocationHandler;
+
 /**
  * A class providing functionality for converting android.webkit classes into support library
  * classes.
@@ -42,4 +52,55 @@
         return new WebSettingsAdapter(BoundaryInterfaceReflectionUtil.castToSuppLibClass(
                 WebSettingsBoundaryInterface.class, mImpl.convertSettings(webSettings)));
     }
+
+    /**
+     * Return a {@link WebResourceRequestAdapter} linked to the given {@link WebResourceRequest} so
+     * that calls on either of those objects affect the other object.
+     */
+    public WebResourceRequestAdapter convertWebResourceRequest(WebResourceRequest request) {
+        return new WebResourceRequestAdapter(BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+                WebResourceRequestBoundaryInterface.class,
+                mImpl.convertWebResourceRequest(request)));
+    }
+
+    /**
+     * Return a {@link ServiceWorkerWebSettingsBoundaryInterface} linked to the given
+     * {@link ServiceWorkerWebSettings} such that calls on either of those objects affect the other
+     * object.
+     */
+    public InvocationHandler convertServiceWorkerSettings(
+            ServiceWorkerWebSettings settings) {
+        return mImpl.convertServiceWorkerSettings(settings);
+    }
+
+    /**
+     * Convert from an {@link InvocationHandler} representing an
+     * {@link androidx.webkit.ServiceWorkerWebSettingsCompat} into a
+     * {@link ServiceWorkerWebSettings}.
+     */
+    @RequiresApi(24)
+    public ServiceWorkerWebSettings convertServiceWorkerSettings(
+            /* SupportLibServiceWorkerSettings */ InvocationHandler serviceWorkerSettings) {
+        return (ServiceWorkerWebSettings) mImpl.convertServiceWorkerSettings(serviceWorkerSettings);
+    }
+
+    /**
+     * Return a {@link InvocationHandler} linked to the given
+     * {@link WebResourceError}such that calls on either of those objects affect the other
+     * object.
+     */
+    InvocationHandler convertWebResourceError(WebResourceError webResourceError) {
+        return mImpl.convertWebResourceError(webResourceError);
+    }
+
+
+    /**
+     * Convert from an {@link InvocationHandler} representing a {@link WebResourceErrorCompat} into
+     * a {@link WebResourceError}.
+     */
+    @RequiresApi(23)
+    WebResourceError convertWebResourceError(
+            /* SupportLibWebResourceError */ InvocationHandler webResourceError) {
+        return (WebResourceError) mImpl.convertWebResourceError(webResourceError);
+    }
 }