Merge tag 'android-security-10.0.0_r53' into int/10/fp2

Android security 10.0.0 release 53

* tag 'android-security-10.0.0_r53':

Change-Id: Ieaef137658368e8b381785f022f3a9fa3466cb3f
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bd59d21
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+*.iml
+.gradle
+/local.properties
+/.idea
+.DS_Store
+build/
+
diff --git a/TestMediaApp/Android.mk b/TestMediaApp/Android.mk
index b97a07e..67127e0 100644
--- a/TestMediaApp/Android.mk
+++ b/TestMediaApp/Android.mk
@@ -34,11 +34,12 @@
 LOCAL_MODULE_TAGS := optional
 
 # car_car is ok here because this is meant to simulate a third party media app
+# Do NOT add dependencies preventing the app from being unbundled (compiled with gradle in Studio).
 LOCAL_STATIC_ANDROID_LIBRARIES := \
         androidx.car_car \
         androidx.appcompat_appcompat \
         androidx.preference_preference \
-        car-media-common
+        androidx.legacy_legacy-support-v4
 
 LOCAL_USE_AAPT2 := true
 
diff --git a/TestMediaApp/AndroidManifest.xml b/TestMediaApp/AndroidManifest.xml
index 3639e9b..911145e 100644
--- a/TestMediaApp/AndroidManifest.xml
+++ b/TestMediaApp/AndroidManifest.xml
@@ -17,25 +17,26 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.car.media.testmediaapp" >
 
-    <uses-sdk
-        android:minSdkVersion="21"
-        android:targetSdkVersion="28"/>
+    <uses-feature
+        android:name="android.hardware.type.automotive"
+        android:required="true"/>
+
 
     <application
         android:allowBackup="true"
-        android:icon="@mipmap/ic_launcher"
-        android:roundIcon="@mipmap/ic_launcher_round"
         android:label="@string/app_name"
         android:supportsRtl="true"
         android:theme="@style/TestMediaAppTheme" >
 
+        <!-- This provider is read-only, only returns album art, and is not a security risk -->
         <provider
-            android:name=".TmaAssetProvider"
+            android:name=".TmaPublicProvider"
             android:exported="true"
-            android:authorities="com.android.car.media.testmediaapp.assets"/>
+            android:authorities="com.android.car.media.testmediaapp.public"/>
 
         <service
             android:name=".TmaBrowser"
+            android:icon="@drawable/ic_app_icon"
             android:exported="true"
             android:label="@string/app_name">
             <intent-filter>
@@ -44,6 +45,17 @@
             </intent-filter>
         </service>
 
+        <service
+            android:name=".TmaBrowser2"
+            android:icon="@mipmap/ic_launcher"
+            android:exported="true"
+            android:label="@string/broken_service">
+            <intent-filter>
+                <action android:name="android.media.browse.MediaBrowserService" />
+                <action android:name="android.intent.action.MEDIA_BUTTON"/>
+            </intent-filter>
+        </service>
+
         <activity
             android:name=".prefs.TmaPrefsActivity"
             android:label="@string/app_name">
@@ -55,6 +67,8 @@
 
 
         <!--                To use the app on a phone.                      -->
+        <meta-data android:name="com.google.android.gms.car.application"
+            android:resource="@xml/automotive_app_desc"/>
 
         <activity android:name=".phone.TmaLauncherActivity" >
             <intent-filter>
diff --git a/TestMediaApp/assets/media_items/advanced.json b/TestMediaApp/assets/media_items/advanced.json
index 4cf34ea..45a18ef 100644
--- a/TestMediaApp/assets/media_items/advanced.json
+++ b/TestMediaApp/assets/media_items/advanced.json
@@ -11,19 +11,49 @@
       "FLAGS": "playable",
       "METADATA": {
         "MEDIA_ID": "advanced custom_action",
-        "DISPLAY_TITLE": "Custom Action",
-        "DURATION": 10000
+        "DISPLAY_TITLE": "Custom Action Item 1",
+        "DURATION": 10000,
+        "ART_URI": "drawable/ic_heart_plus_plus"
+      },
+      "CUSTOM_ACTIONS": ["HEART_LESS_LESS", "HEART_PLUS_PLUS"]
+    },
+    {
+      "FLAGS": "playable",
+      "METADATA": {
+        "MEDIA_ID": "advanced custom_action 2",
+        "DISPLAY_TITLE": "Custom Actions overflow",
+        "DURATION": 10000,
+        "ART_URI": "drawable/ic_heart_less_less"
+      },
+      "CUSTOM_ACTIONS": ["HEART_LESS_LESS", "HEART_PLUS_PLUS", "HEART_PLUS_PLUS", "HEART_PLUS_PLUS", "HEART_PLUS_PLUS"]
+    },
+    {
+      "FLAGS": "playable",
+      "METADATA": {
+        "MEDIA_ID": "advanced custom_action 3",
+        "DISPLAY_TITLE": "Custom Action Item 3",
+        "DURATION": 10000,
+        "ART_URI": "drawable/ic_rectangle_horiz"
       },
       "CUSTOM_ACTIONS": ["HEART_LESS_LESS", "HEART_PLUS_PLUS"]
     },
     {
       "FLAGS": "browsable",
-      "PLAYABLE_HINT": "GRID",
+      "BROWSABLE_HINT": "GRID_CATEGORY",
       "METADATA": {
         "MEDIA_ID": "advanced art nodes",
         "DISPLAY_TITLE": "Album Art"
       },
       "INCLUDE":"media_items/album_art/art_nodes.json"
+    },
+    {
+      "FLAGS": "browsable",
+      "PLAYABLE_HINT": "LIST",
+      "METADATA": {
+        "MEDIA_ID": "advanced exceptions",
+        "DISPLAY_TITLE": "Exceptions"
+      },
+      "INCLUDE":"media_items/exceptions.json"
     }
   ]
 }
\ No newline at end of file
diff --git a/TestMediaApp/assets/media_items/album_art/art_nodes.json b/TestMediaApp/assets/media_items/album_art/art_nodes.json
index 434ed23..692809f 100644
--- a/TestMediaApp/assets/media_items/album_art/art_nodes.json
+++ b/TestMediaApp/assets/media_items/album_art/art_nodes.json
@@ -1,6 +1,6 @@
 {
   "FLAGS": "browsable",
-  "BROWSABLE_HINT": "LIST",
+  "BROWSABLE_HINT": "GRID_CATEGORY",
 
   "METADATA": {
     "MEDIA_ID": "album_art/art_nodes",
@@ -52,6 +52,15 @@
         "DISPLAY_TITLE": "Nature 1024"
       },
       "INCLUDE":"media_items/album_art/nature/art_nature_1024.json"
+    },
+    {
+      "FLAGS": "browsable",
+      "PLAYABLE_HINT": "GRID",
+      "METADATA": {
+        "MEDIA_ID": "album_art/art_nodes nature files",
+        "DISPLAY_TITLE": "Nature files"
+      },
+      "INCLUDE":"media_items/album_art/nature/art_nature_files.json"
     }
   ]
 }
\ No newline at end of file
diff --git a/TestMediaApp/assets/media_items/album_art/nature/art_nature_1024.json b/TestMediaApp/assets/media_items/album_art/nature/art_nature_1024.json
index b1759f2..c34ed80 100644
--- a/TestMediaApp/assets/media_items/album_art/nature/art_nature_1024.json
+++ b/TestMediaApp/assets/media_items/album_art/nature/art_nature_1024.json
@@ -13,7 +13,7 @@
         "MEDIA_ID": "art_nature_1024_leaves bee",
         "DISPLAY_TITLE": "Bee",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-1024/bee.jpg"
+        "ART_URI": "assets/bitmaps/nature-1024/bee.jpg"
       }
     },
     {
@@ -22,7 +22,7 @@
         "MEDIA_ID": "art_nature_1024_leaves clouds",
         "DISPLAY_TITLE": "Clouds",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-1024/clouds.jpg"
+        "ART_URI": "assets/bitmaps/nature-1024/clouds.jpg"
       }
     },
     {
@@ -31,7 +31,7 @@
         "MEDIA_ID": "art_nature_1024_leaves flower1",
         "DISPLAY_TITLE": "Flower 1",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-1024/flower1.jpg"
+        "ART_URI": "assets/bitmaps/nature-1024/flower1.jpg"
       }
     },
     {
@@ -40,7 +40,7 @@
         "MEDIA_ID": "art_nature_1024_leaves flower2",
         "DISPLAY_TITLE": "Flower 2",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-1024/flower2.jpg"
+        "ART_URI": "assets/bitmaps/nature-1024/flower2.jpg"
       }
     },
     {
@@ -49,7 +49,7 @@
         "MEDIA_ID": "art_nature_1024_leaves flower3",
         "DISPLAY_TITLE": "Flower3 ",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-1024/flower3.jpg"
+        "ART_URI": "assets/bitmaps/nature-1024/flower3.jpg"
       }
     },
     {
@@ -58,7 +58,7 @@
         "MEDIA_ID": "art_nature_1024_leaves flowers",
         "DISPLAY_TITLE": "Flowers",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-1024/flowers.jpg"
+        "ART_URI": "assets/bitmaps/nature-1024/flowers.jpg"
       }
     },
     {
@@ -67,7 +67,7 @@
         "MEDIA_ID": "art_nature_1024_leaves leaves",
         "DISPLAY_TITLE": "Leaves",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-1024/leaves.jpg"
+        "ART_URI": "assets/bitmaps/nature-1024/leaves.jpg"
       }
     },
     {
@@ -76,7 +76,7 @@
         "MEDIA_ID": "art_nature_1024_leaves sage",
         "DISPLAY_TITLE": "Sage",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-1024/sage.jpg"
+        "ART_URI": "assets/bitmaps/nature-1024/sage.jpg"
       }
     },
     {
@@ -85,7 +85,7 @@
         "MEDIA_ID": "art_nature_1024_leaves tree",
         "DISPLAY_TITLE": "Tree",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-1024/tree.jpg"
+        "ART_URI": "assets/bitmaps/nature-1024/tree.jpg"
       }
     }
   ]
diff --git a/TestMediaApp/assets/media_items/album_art/nature/art_nature_128.json b/TestMediaApp/assets/media_items/album_art/nature/art_nature_128.json
index 0fdd629..67200e8 100644
--- a/TestMediaApp/assets/media_items/album_art/nature/art_nature_128.json
+++ b/TestMediaApp/assets/media_items/album_art/nature/art_nature_128.json
@@ -13,7 +13,7 @@
         "MEDIA_ID": "art_nature_128 bee",
         "DISPLAY_TITLE": "Bee",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-128/bee.jpg"
+        "ART_URI": "assets/bitmaps/nature-128/bee.jpg"
       }
     },
     {
@@ -22,7 +22,7 @@
         "MEDIA_ID": "art_nature_128 clouds",
         "DISPLAY_TITLE": "Clouds",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-128/clouds.jpg"
+        "ART_URI": "assets/bitmaps/nature-128/clouds.jpg"
       }
     },
     {
@@ -31,7 +31,7 @@
         "MEDIA_ID": "art_nature_128 flower1",
         "DISPLAY_TITLE": "Flower 1",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-128/flower1.jpg"
+        "ART_URI": "assets/bitmaps/nature-128/flower1.jpg"
       }
     },
     {
@@ -40,7 +40,7 @@
         "MEDIA_ID": "art_nature_128 flower2",
         "DISPLAY_TITLE": "Flower 2",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-128/flower2.jpg"
+        "ART_URI": "assets/bitmaps/nature-128/flower2.jpg"
       }
     },
     {
@@ -49,7 +49,7 @@
         "MEDIA_ID": "art_nature_128 flower3",
         "DISPLAY_TITLE": "Flower3 ",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-128/flower3.jpg"
+        "ART_URI": "assets/bitmaps/nature-128/flower3.jpg"
       }
     },
     {
@@ -58,7 +58,7 @@
         "MEDIA_ID": "art_nature_128 flowers",
         "DISPLAY_TITLE": "Flowers",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-128/flowers.jpg"
+        "ART_URI": "assets/bitmaps/nature-128/flowers.jpg"
       }
     },
     {
@@ -67,7 +67,7 @@
         "MEDIA_ID": "art_nature_128 leaves",
         "DISPLAY_TITLE": "Leaves",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-128/leaves.jpg"
+        "ART_URI": "assets/bitmaps/nature-128/leaves.jpg"
       }
     },
     {
@@ -76,7 +76,7 @@
         "MEDIA_ID": "art_nature_128 sage",
         "DISPLAY_TITLE": "Sage",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-128/sage.jpg"
+        "ART_URI": "assets/bitmaps/nature-128/sage.jpg"
       }
     },
     {
@@ -85,7 +85,7 @@
         "MEDIA_ID": "art_nature_128 tree",
         "DISPLAY_TITLE": "Tree",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-128/tree.jpg"
+        "ART_URI": "assets/bitmaps/nature-128/tree.jpg"
       }
     }
   ]
diff --git a/TestMediaApp/assets/media_items/album_art/nature/art_nature_256.json b/TestMediaApp/assets/media_items/album_art/nature/art_nature_256.json
index 200ecc1..719665e 100644
--- a/TestMediaApp/assets/media_items/album_art/nature/art_nature_256.json
+++ b/TestMediaApp/assets/media_items/album_art/nature/art_nature_256.json
@@ -13,7 +13,7 @@
         "MEDIA_ID": "art_nature_256 bee",
         "DISPLAY_TITLE": "Bee",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-256/bee.jpg"
+        "ART_URI": "assets/bitmaps/nature-256/bee.jpg"
       }
     },
     {
@@ -22,7 +22,7 @@
         "MEDIA_ID": "art_nature_256 clouds",
         "DISPLAY_TITLE": "Clouds",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-256/clouds.jpg"
+        "ART_URI": "assets/bitmaps/nature-256/clouds.jpg"
       }
     },
     {
@@ -31,7 +31,7 @@
         "MEDIA_ID": "art_nature_256 flower1",
         "DISPLAY_TITLE": "Flower 1",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-256/flower1.jpg"
+        "ART_URI": "assets/bitmaps/nature-256/flower1.jpg"
       }
     },
     {
@@ -40,7 +40,7 @@
         "MEDIA_ID": "art_nature_256 flower2",
         "DISPLAY_TITLE": "Flower 2",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-256/flower2.jpg"
+        "ART_URI": "assets/bitmaps/nature-256/flower2.jpg"
       }
     },
     {
@@ -49,7 +49,7 @@
         "MEDIA_ID": "art_nature_256 flower3",
         "DISPLAY_TITLE": "Flower3 ",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-256/flower3.jpg"
+        "ART_URI": "assets/bitmaps/nature-256/flower3.jpg"
       }
     },
     {
@@ -58,7 +58,7 @@
         "MEDIA_ID": "art_nature_256 flowers",
         "DISPLAY_TITLE": "Flowers",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-256/flowers.jpg"
+        "ART_URI": "assets/bitmaps/nature-256/flowers.jpg"
       }
     },
     {
@@ -67,7 +67,7 @@
         "MEDIA_ID": "art_nature_256 leaves",
         "DISPLAY_TITLE": "Leaves",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-256/leaves.jpg"
+        "ART_URI": "assets/bitmaps/nature-256/leaves.jpg"
       }
     },
     {
@@ -76,7 +76,7 @@
         "MEDIA_ID": "art_nature_256 sage",
         "DISPLAY_TITLE": "Sage",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-256/sage.jpg"
+        "ART_URI": "assets/bitmaps/nature-256/sage.jpg"
       }
     },
     {
@@ -85,7 +85,7 @@
         "MEDIA_ID": "art_nature_256 tree",
         "DISPLAY_TITLE": "Tree",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-256/tree.jpg"
+        "ART_URI": "assets/bitmaps/nature-256/tree.jpg"
       }
     }
   ]
diff --git a/TestMediaApp/assets/media_items/album_art/nature/art_nature_512.json b/TestMediaApp/assets/media_items/album_art/nature/art_nature_512.json
index 56bb6a4..29cb783 100644
--- a/TestMediaApp/assets/media_items/album_art/nature/art_nature_512.json
+++ b/TestMediaApp/assets/media_items/album_art/nature/art_nature_512.json
@@ -13,7 +13,7 @@
         "MEDIA_ID": "art_nature_512 bee",
         "DISPLAY_TITLE": "Bee",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-512/bee.jpg"
+        "ART_URI": "assets/bitmaps/nature-512/bee.jpg"
       }
     },
     {
@@ -22,7 +22,7 @@
         "MEDIA_ID": "art_nature_512 clouds",
         "DISPLAY_TITLE": "Clouds",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-512/clouds.jpg"
+        "ART_URI": "assets/bitmaps/nature-512/clouds.jpg"
       }
     },
     {
@@ -31,7 +31,7 @@
         "MEDIA_ID": "art_nature_512 flower1",
         "DISPLAY_TITLE": "Flower 1",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-512/flower1.jpg"
+        "ART_URI": "assets/bitmaps/nature-512/flower1.jpg"
       }
     },
     {
@@ -40,7 +40,7 @@
         "MEDIA_ID": "art_nature_512 flower2",
         "DISPLAY_TITLE": "Flower 2",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-512/flower2.jpg"
+        "ART_URI": "assets/bitmaps/nature-512/flower2.jpg"
       }
     },
     {
@@ -49,7 +49,7 @@
         "MEDIA_ID": "art_nature_512 flower3",
         "DISPLAY_TITLE": "Flower3 ",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-512/flower3.jpg"
+        "ART_URI": "assets/bitmaps/nature-512/flower3.jpg"
       }
     },
     {
@@ -58,7 +58,7 @@
         "MEDIA_ID": "art_nature_512 flowers",
         "DISPLAY_TITLE": "Flowers",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-512/flowers.jpg"
+        "ART_URI": "assets/bitmaps/nature-512/flowers.jpg"
       }
     },
     {
@@ -67,7 +67,7 @@
         "MEDIA_ID": "art_nature_512 leaves",
         "DISPLAY_TITLE": "Leaves",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-512/leaves.jpg"
+        "ART_URI": "assets/bitmaps/nature-512/leaves.jpg"
       }
     },
     {
@@ -76,7 +76,7 @@
         "MEDIA_ID": "art_nature_512 sage",
         "DISPLAY_TITLE": "Sage",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-512/sage.jpg"
+        "ART_URI": "assets/bitmaps/nature-512/sage.jpg"
       }
     },
     {
@@ -85,7 +85,7 @@
         "MEDIA_ID": "art_nature_512 tree",
         "DISPLAY_TITLE": "Tree",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-512/tree.jpg"
+        "ART_URI": "assets/bitmaps/nature-512/tree.jpg"
       }
     }
   ]
diff --git a/TestMediaApp/assets/media_items/album_art/nature/art_nature_64.json b/TestMediaApp/assets/media_items/album_art/nature/art_nature_64.json
index 913dd07..72a3f41 100644
--- a/TestMediaApp/assets/media_items/album_art/nature/art_nature_64.json
+++ b/TestMediaApp/assets/media_items/album_art/nature/art_nature_64.json
@@ -13,7 +13,7 @@
         "MEDIA_ID": "art_nature_64 bee",
         "DISPLAY_TITLE": "Bee",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-64/bee.jpg"
+        "ART_URI": "assets/bitmaps/nature-64/bee.jpg"
       }
     },
     {
@@ -22,7 +22,7 @@
         "MEDIA_ID": "art_nature_64 clouds",
         "DISPLAY_TITLE": "Clouds",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-64/clouds.jpg"
+        "ART_URI": "assets/bitmaps/nature-64/clouds.jpg"
       }
     },
     {
@@ -31,7 +31,7 @@
         "MEDIA_ID": "art_nature_64 flower1",
         "DISPLAY_TITLE": "Flower 1",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-64/flower1.jpg"
+        "ART_URI": "assets/bitmaps/nature-64/flower1.jpg"
       }
     },
     {
@@ -40,7 +40,7 @@
         "MEDIA_ID": "art_nature_64 flower2",
         "DISPLAY_TITLE": "Flower 2",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-64/flower2.jpg"
+        "ART_URI": "assets/bitmaps/nature-64/flower2.jpg"
       }
     },
     {
@@ -49,7 +49,7 @@
         "MEDIA_ID": "art_nature_64 flower3",
         "DISPLAY_TITLE": "Flower3 ",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-64/flower3.jpg"
+        "ART_URI": "assets/bitmaps/nature-64/flower3.jpg"
       }
     },
     {
@@ -58,7 +58,7 @@
         "MEDIA_ID": "art_nature_64 flowers",
         "DISPLAY_TITLE": "Flowers",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-64/flowers.jpg"
+        "ART_URI": "assets/bitmaps/nature-64/flowers.jpg"
       }
     },
     {
@@ -67,7 +67,7 @@
         "MEDIA_ID": "art_nature_64 leaves",
         "DISPLAY_TITLE": "Leaves",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-64/leaves.jpg"
+        "ART_URI": "assets/bitmaps/nature-64/leaves.jpg"
       }
     },
     {
@@ -76,7 +76,7 @@
         "MEDIA_ID": "art_nature_64 sage",
         "DISPLAY_TITLE": "Sage",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-64/sage.jpg"
+        "ART_URI": "assets/bitmaps/nature-64/sage.jpg"
       }
     },
     {
@@ -85,7 +85,7 @@
         "MEDIA_ID": "art_nature_64 tree",
         "DISPLAY_TITLE": "Tree",
         "DURATION": 10000,
-        "ART_URI": "bitmaps/nature-64/tree.jpg"
+        "ART_URI": "assets/bitmaps/nature-64/tree.jpg"
       }
     }
   ]
diff --git a/TestMediaApp/assets/media_items/album_art/nature/art_nature_files.json b/TestMediaApp/assets/media_items/album_art/nature/art_nature_files.json
new file mode 100644
index 0000000..2643829
--- /dev/null
+++ b/TestMediaApp/assets/media_items/album_art/nature/art_nature_files.json
@@ -0,0 +1,92 @@
+{
+  "FLAGS": "browsable",
+
+  "METADATA": {
+    "MEDIA_ID": "art_nature_files_leaves",
+    "DISPLAY_TITLE": "Art nature files"
+  },
+
+  "CHILDREN": [
+    {
+      "FLAGS": "playable",
+      "METADATA": {
+        "MEDIA_ID": "art_nature_files_leaves bee",
+        "DISPLAY_TITLE": "Bee",
+        "DURATION": 10000,
+        "ART_URI": "files/bitmaps/nature-1024/bee.jpg"
+      }
+    },
+    {
+      "FLAGS": "playable",
+      "METADATA": {
+        "MEDIA_ID": "art_nature_files_leaves clouds",
+        "DISPLAY_TITLE": "Clouds",
+        "DURATION": 10000,
+        "ART_URI": "files/bitmaps/nature-1024/clouds.jpg"
+      }
+    },
+    {
+      "FLAGS": "playable",
+      "METADATA": {
+        "MEDIA_ID": "art_nature_files_leaves flower1",
+        "DISPLAY_TITLE": "Flower 1",
+        "DURATION": 10000,
+        "ART_URI": "files/bitmaps/nature-1024/flower1.jpg"
+      }
+    },
+    {
+      "FLAGS": "playable",
+      "METADATA": {
+        "MEDIA_ID": "art_nature_files_leaves flower2",
+        "DISPLAY_TITLE": "Flower 2",
+        "DURATION": 10000,
+        "ART_URI": "files/bitmaps/nature-1024/flower2.jpg"
+      }
+    },
+    {
+      "FLAGS": "playable",
+      "METADATA": {
+        "MEDIA_ID": "art_nature_files_leaves flower3",
+        "DISPLAY_TITLE": "Flower3 ",
+        "DURATION": 10000,
+        "ART_URI": "files/bitmaps/nature-1024/flower3.jpg"
+      }
+    },
+    {
+      "FLAGS": "playable",
+      "METADATA": {
+        "MEDIA_ID": "art_nature_files_leaves flowers",
+        "DISPLAY_TITLE": "Flowers",
+        "DURATION": 10000,
+        "ART_URI": "files/bitmaps/nature-1024/flowers.jpg"
+      }
+    },
+    {
+      "FLAGS": "playable",
+      "METADATA": {
+        "MEDIA_ID": "art_nature_files_leaves leaves",
+        "DISPLAY_TITLE": "Leaves",
+        "DURATION": 10000,
+        "ART_URI": "files/bitmaps/nature-1024/leaves.jpg"
+      }
+    },
+    {
+      "FLAGS": "playable",
+      "METADATA": {
+        "MEDIA_ID": "art_nature_files_leaves sage",
+        "DISPLAY_TITLE": "Sage",
+        "DURATION": 10000,
+        "ART_URI": "files/bitmaps/nature-1024/sage.jpg"
+      }
+    },
+    {
+      "FLAGS": "playable",
+      "METADATA": {
+        "MEDIA_ID": "art_nature_files_leaves tree",
+        "DISPLAY_TITLE": "Tree",
+        "DURATION": 10000,
+        "ART_URI": "files/bitmaps/nature-1024/tree.jpg"
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/TestMediaApp/assets/media_items/exceptions.json b/TestMediaApp/assets/media_items/exceptions.json
new file mode 100644
index 0000000..0a48d4d
--- /dev/null
+++ b/TestMediaApp/assets/media_items/exceptions.json
@@ -0,0 +1,22 @@
+{
+  "FLAGS": "browsable",
+
+  "METADATA": {
+    "MEDIA_ID": "exceptions",
+    "DISPLAY_TITLE": "Exceptions"
+  },
+
+  "CHILDREN": [
+    {
+      "FLAGS": "playable",
+      "METADATA": {
+        "MEDIA_ID": "exceptions play NPE",
+        "DISPLAY_TITLE": "Throw NPE on play",
+        "DURATION": 30000
+      },
+      "EVENTS": [
+        { "STATE": "PLAYING", "THROW_EXCEPTION": "java.lang.NullPointerException" }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/TestMediaApp/assets/media_items/mixed.json b/TestMediaApp/assets/media_items/mixed.json
index 7e04e85..34da9c9 100644
--- a/TestMediaApp/assets/media_items/mixed.json
+++ b/TestMediaApp/assets/media_items/mixed.json
@@ -1,7 +1,7 @@
 {
   "FLAGS": "browsable",
   "PLAYABLE_HINT": "GRID",
-  "BROWSABLE_HINT": "LIST",
+  "BROWSABLE_HINT": "LIST_CATEGORY",
 
   "METADATA": {
     "MEDIA_ID": "mixed",
diff --git a/TestMediaApp/assets/media_items/only_nodes.json b/TestMediaApp/assets/media_items/only_nodes.json
index 4858438..26f998e 100644
--- a/TestMediaApp/assets/media_items/only_nodes.json
+++ b/TestMediaApp/assets/media_items/only_nodes.json
@@ -13,7 +13,8 @@
       "FLAGS": "browsable",
       "METADATA": {
         "MEDIA_ID": "only_nodes simple_leaves",
-        "DISPLAY_TITLE": "Basic songs"
+        "DISPLAY_TITLE": "Basic songs",
+        "ART_URI": "drawable/ic_heart_plus_plus"
       },
       "INCLUDE":"media_items/simple_leaves.json"
     },
@@ -23,14 +24,15 @@
       "BROWSABLE_HINT": "LIST",
       "METADATA": {
         "MEDIA_ID": "only_nodes advanced",
-        "DISPLAY_TITLE": "Advanced"
+        "DISPLAY_TITLE": "Advanced",
+        "ART_URI": "drawable/ic_heart_less_less"
       },
       "INCLUDE":"media_items/advanced.json"
     },
     {
       "FLAGS": "browsable",
       "PLAYABLE_HINT": "GRID",
-      "BROWSABLE_HINT": "LIST",
+      "BROWSABLE_HINT": "LIST_CATEGORY",
       "METADATA": {
         "MEDIA_ID": "only_nodes rabbit hole",
         "DISPLAY_TITLE": "Rabbit hole 2"
diff --git a/TestMediaApp/assets/media_items/simple_leaves.json b/TestMediaApp/assets/media_items/simple_leaves.json
index d70a2a4..dc6b0a3 100644
--- a/TestMediaApp/assets/media_items/simple_leaves.json
+++ b/TestMediaApp/assets/media_items/simple_leaves.json
@@ -11,7 +11,7 @@
       "FLAGS": "playable",
       "METADATA": {
         "MEDIA_ID": "simple_leaves normal 10s song",
-        "DISPLAY_TITLE": "A normal 10s song",
+        "DISPLAY_TITLE": "A normal 10s song with a long title. A normal 10s song with a long title. A normal 10s song with a long title. ",
         "DURATION": 10000
       }
     },
@@ -20,6 +20,8 @@
       "METADATA": {
         "MEDIA_ID": "simple_leaves normal 1H song",
         "DISPLAY_TITLE": "A normal 1H song",
+        "ARTIST": "Artist",
+        "ALBUM":"Album",
         "DURATION": 3600000
       }
     },
@@ -28,6 +30,9 @@
       "METADATA": {
         "MEDIA_ID": "simple_leaves slow connection",
         "DISPLAY_TITLE": "Connects and buffers for 4s each",
+        "DISPLAY_SUBTITLE": "A very long subtitle. A very long subtitle. A very long subtitle. A very long subtitle. A very long subtitle. A very long subtitle. ",
+        "ARTIST": "This is a very long artist name. This is a very long artist name. This is a very long artist name.",
+        "ALBUM":"Album",
         "DURATION": 30000
       },
       "EVENTS": [
@@ -41,6 +46,7 @@
       "METADATA": {
         "MEDIA_ID": "simple_leaves poor internet",
         "DISPLAY_TITLE": "Poor internet quality at 2s",
+        "ARTIST": "Artist",
         "DURATION": 30000
       },
       "EVENTS": [
@@ -58,6 +64,8 @@
       "METADATA": {
         "MEDIA_ID": "simple_leaves cache failure",
         "DISPLAY_TITLE": "Caching failure at 2s",
+        "DISPLAY_SUBTITLE": "Show a toast",
+        "ALBUM":"This is a very long album title. This is a very long album title. This is a very long album title.",
         "DURATION": 30000
       },
       "EVENTS": [
@@ -75,6 +83,7 @@
       "METADATA": {
         "MEDIA_ID": "simple_leaves error code",
         "DISPLAY_TITLE": "Parental Control error code at 1s",
+        "DISPLAY_SUBTITLE": "Show a toast",
         "DURATION": 10000
       },
       "EVENTS": [
@@ -91,6 +100,7 @@
       "METADATA": {
         "MEDIA_ID": "simple_leaves premium required",
         "DISPLAY_TITLE": "Paid account required at 1s",
+        "DISPLAY_SUBTITLE": "Show a dialog",
         "DURATION": 50000
       },
       "EVENTS": [
@@ -104,6 +114,30 @@
           "POST_DELAY_MS": 1000
         }
       ]
+    },
+    {
+      "FLAGS": "playable",
+      "METADATA": {
+        "MEDIA_ID": "simple_leaves bluetooth disconnected and reconnected",
+        "DISPLAY_TITLE": "Bluetooth disconnected at 2s and reconnected at 8s",
+        "DURATION": 20000
+      },
+      "EVENTS": [
+        { "STATE": "PLAYING", "POST_DELAY_MS": 0 },
+        {
+          "STATE": "ERROR",
+          "ERROR_MESSAGE": "Bluetooth audio disconnected.",
+          "POST_DELAY_MS": 2000
+        },
+        {
+          "ACTION": "RESET_METADATA",
+          "POST_DELAY_MS": 6000
+        },
+        {
+          "STATE": "PLAYING",
+          "POST_DELAY_MS": 3000
+        }
+      ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/TestMediaApp/assets/media_items/single_node.json b/TestMediaApp/assets/media_items/single_node.json
new file mode 100644
index 0000000..cf93cee
--- /dev/null
+++ b/TestMediaApp/assets/media_items/single_node.json
@@ -0,0 +1,22 @@
+{
+  "FLAGS": "browsable",
+  "PLAYABLE_HINT": "LIST",
+  "BROWSABLE_HINT": "GRID",
+
+  "METADATA": {
+    "MEDIA_ID": "single_node",
+    "DISPLAY_TITLE": "A lonely tab"
+  },
+
+  "CHILDREN": [
+    {
+      "FLAGS": "browsable",
+      "METADATA": {
+        "MEDIA_ID": "single_node simple_leaves",
+        "DISPLAY_TITLE": "Basic songs",
+        "ART_URI": "drawable/ic_heart_plus_plus"
+      },
+      "INCLUDE":"media_items/simple_leaves.json"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/TestMediaApp/assets/media_items/untagged.json b/TestMediaApp/assets/media_items/untagged.json
new file mode 100644
index 0000000..fd8f5f5
--- /dev/null
+++ b/TestMediaApp/assets/media_items/untagged.json
@@ -0,0 +1,27 @@
+{
+  "FLAGS": "browsable",
+
+  "METADATA": {
+    "MEDIA_ID": "untagged",
+    "DISPLAY_TITLE": "Untagged media items"
+  },
+
+  "CHILDREN": [
+    {
+      "METADATA": {
+        "MEDIA_ID": "untagged normal 10s song",
+        "DISPLAY_TITLE": "A normal 10s song with a long title. A normal 10s song with a long title. A normal 10s song with a long title. ",
+        "DURATION": 10000
+      }
+    },
+    {
+      "METADATA": {
+        "MEDIA_ID": "untagged normal 1H song",
+        "DISPLAY_TITLE": "A normal 1H song",
+        "ARTIST": "Artist",
+        "ALBUM":"Album",
+        "DURATION": 3600000
+      }
+    }
+  ]
+}
diff --git a/TestMediaApp/build.gradle b/TestMediaApp/build.gradle
new file mode 100644
index 0000000..79fd66d
--- /dev/null
+++ b/TestMediaApp/build.gradle
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 28
+    defaultConfig {
+        applicationId "com.android.car.media.testmediaapp"
+        minSdkVersion 21
+        targetSdkVersion 28
+        versionCode 1
+        versionName "1.0"
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    lintOptions {
+        abortOnError false
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+        }
+    }
+
+    sourceSets {
+        main {
+            manifest.srcFile 'AndroidManifest.xml'
+            java.srcDirs = ['src']
+            resources.srcDirs = ['src']
+            aidl.srcDirs = ['src']
+            renderscript.srcDirs = ['src']
+            res.srcDirs = ['res']
+            assets.srcDirs = ['assets']
+        }
+    }
+}
+
+dependencies {
+    implementation 'androidx.appcompat:appcompat:1.0.2'
+    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+    implementation 'androidx.media:media:1.0.1'
+    implementation 'androidx.preference:preference:1.0.0'
+}
diff --git a/TestMediaApp/res/drawable/button_ripple_bg.xml b/TestMediaApp/res/drawable/button_ripple_bg.xml
index d012c94..9c99a25 100644
--- a/TestMediaApp/res/drawable/button_ripple_bg.xml
+++ b/TestMediaApp/res/drawable/button_ripple_bg.xml
@@ -17,4 +17,4 @@
 
 <ripple
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:color="@*android:color/car_card_ripple_background" />
+    android:color="@color/ripple_background_color" />
diff --git a/TestMediaApp/res/drawable/ic_app_icon.xml b/TestMediaApp/res/drawable/ic_app_icon.xml
new file mode 100644
index 0000000..0985812
--- /dev/null
+++ b/TestMediaApp/res/drawable/ic_app_icon.xml
@@ -0,0 +1,15 @@
+<vector android:height="24dp" android:viewportHeight="33.86667"
+    android:viewportWidth="33.866665" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillAlpha="1" android:fillColor="#6bfc00"
+        android:pathData="M5.0279,12.7651h23.2377v20.5024h-23.2377z"
+        android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="0.26499999"/>
+    <path android:fillAlpha="1" android:fillColor="#6bfc00"
+        android:pathData="M5.143,12.5437a11.4907,8.1301 0,1 0,22.9814 0a11.4907,8.1301 0,1 0,-22.9814 0z"
+        android:strokeAlpha="1" android:strokeColor="#6bfc00" android:strokeWidth="0.49525586"/>
+    <path android:fillAlpha="1" android:fillColor="#002afc"
+        android:pathData="M10.3424,11.2542a1.0421,1.0942 0,1 0,2.0841 0a1.0421,1.0942 0,1 0,-2.0841 0z"
+        android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="0.26499999"/>
+    <path android:fillAlpha="1" android:fillColor="#002afc"
+        android:pathData="M20.3461,11.3323a1.0421,1.0942 0,1 0,2.0841 0a1.0421,1.0942 0,1 0,-2.0841 0z"
+        android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="0.26499999"/>
+</vector>
diff --git a/TestMediaApp/res/drawable/ic_close.xml b/TestMediaApp/res/drawable/ic_close.xml
new file mode 100644
index 0000000..f4c1e3b
--- /dev/null
+++ b/TestMediaApp/res/drawable/ic_close.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright 2019 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT 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="56dp"
+    android:height="56dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="#FFF"
+        android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+</vector>
\ No newline at end of file
diff --git a/TestMediaApp/res/drawable/ic_rectangle_horiz.xml b/TestMediaApp/res/drawable/ic_rectangle_horiz.xml
new file mode 100644
index 0000000..e791816
--- /dev/null
+++ b/TestMediaApp/res/drawable/ic_rectangle_horiz.xml
@@ -0,0 +1,32 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="80dp"
+    android:height="40dp"
+    android:viewportWidth="21.166668"
+    android:viewportHeight="10.583333">
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="M0.0236,-0.0237h21.1667v10.5833h-21.1667z"
+      android:strokeWidth="0.26458332"/>
+  <path
+      android:pathData="M0.0339,5.2585L0.0339,-0.0214L10.6054,-0.0214 21.177,-0.0214L21.177,5.2585 21.177,10.5383L10.6054,10.5383 0.0339,10.5383Z"
+      android:strokeWidth="0.08928572"
+      android:fillColor="#00ffff"
+      android:fillAlpha="1"/>
+  <path
+      android:pathData="M0.4961,0.4252h19.0169v9.0242h-19.0169z"
+      android:strokeWidth="0.26458332"
+      android:fillColor="#00ffff"
+      android:fillAlpha="1"/>
+  <path
+      android:pathData="M0.4961,0.4252h20.3162v9.7801h-20.3162z"
+      android:strokeWidth="0.26458332"
+      android:fillColor="#ffffff"
+      android:fillAlpha="1"/>
+  <path
+      android:pathData="m14.7699,7.4511a4.6,4.6 0,0 1,-5.07 2.3682,4.6 4.6,0 0,1 -3.6053,-4.2796 4.6,4.6 0,0 1,3.1946 -4.5943,4.6 4.6,0 0,1 5.2671,1.8898l-3.8666,2.4918z"
+      android:strokeAlpha="1"
+      android:strokeWidth="0.265"
+      android:fillColor="#00000000"
+      android:strokeColor="#001dff"
+      android:fillAlpha="1"/>
+</vector>
diff --git a/TestMediaApp/res/mipmap-hdpi/ic_launcher_round.png b/TestMediaApp/res/mipmap-hdpi/ic_launcher_round.png
deleted file mode 100644
index 9a078e3..0000000
--- a/TestMediaApp/res/mipmap-hdpi/ic_launcher_round.png
+++ /dev/null
Binary files differ
diff --git a/TestMediaApp/res/mipmap-mdpi/ic_launcher_round.png b/TestMediaApp/res/mipmap-mdpi/ic_launcher_round.png
deleted file mode 100644
index efc028a..0000000
--- a/TestMediaApp/res/mipmap-mdpi/ic_launcher_round.png
+++ /dev/null
Binary files differ
diff --git a/TestMediaApp/res/mipmap-xhdpi/ic_launcher_round.png b/TestMediaApp/res/mipmap-xhdpi/ic_launcher_round.png
deleted file mode 100644
index 3af2608..0000000
--- a/TestMediaApp/res/mipmap-xhdpi/ic_launcher_round.png
+++ /dev/null
Binary files differ
diff --git a/TestMediaApp/res/mipmap-xxhdpi/ic_launcher_round.png b/TestMediaApp/res/mipmap-xxhdpi/ic_launcher_round.png
deleted file mode 100644
index 9bec2e6..0000000
--- a/TestMediaApp/res/mipmap-xxhdpi/ic_launcher_round.png
+++ /dev/null
Binary files differ
diff --git a/TestMediaApp/res/mipmap-xxxhdpi/ic_launcher_round.png b/TestMediaApp/res/mipmap-xxxhdpi/ic_launcher_round.png
deleted file mode 100644
index 34947cd..0000000
--- a/TestMediaApp/res/mipmap-xxxhdpi/ic_launcher_round.png
+++ /dev/null
Binary files differ
diff --git a/TestMediaApp/res/values/strings.xml b/TestMediaApp/res/values/strings.xml
index 62ec9e1..ac621be 100644
--- a/TestMediaApp/res/values/strings.xml
+++ b/TestMediaApp/res/values/strings.xml
@@ -17,6 +17,8 @@
 <resources>
     <string name="app_name" translatable="false">Test Media App</string>
 
+    <string name="broken_service" translatable="false">Test Empty Media</string>
+
     <string name="select_account" translatable="false">Please select an account type</string>
 
     <string name="no_account" translatable="false"> A test account is required to use this test
diff --git a/TestMediaApp/res/values/styles.xml b/TestMediaApp/res/values/styles.xml
index cf316c5..6a8e8b1 100644
--- a/TestMediaApp/res/values/styles.xml
+++ b/TestMediaApp/res/values/styles.xml
@@ -16,8 +16,11 @@
  */
 -->
 <resources>
-    <style name="TestMediaAppTheme" parent="Theme.Car.Light.NoActionBar">
-        <item name="android:windowBackground">@color/car_card_dark</item>
+    <style name="TestMediaAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
+        <item name="android:windowBackground">@color/window_background </item>
     </style>
 
+    <color name="window_background">#AAA</color>
+    <color name="ripple_background_color">#444</color>
+
 </resources>
diff --git a/TestMediaApp/res/xml/automotive_app_desc.xml b/TestMediaApp/res/xml/automotive_app_desc.xml
new file mode 100644
index 0000000..3daa01a
--- /dev/null
+++ b/TestMediaApp/res/xml/automotive_app_desc.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (c) 2019, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+<automotiveApp xmlns:android="http://schemas.android.com/apk/res/android">
+  <uses name="media"/>
+</automotiveApp>
\ No newline at end of file
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/MediaKeys.java b/TestMediaApp/src/com/android/car/media/testmediaapp/MediaKeys.java
new file mode 100644
index 0000000..9fba1a8
--- /dev/null
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/MediaKeys.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2019, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.android.car.media.testmediaapp;
+
+/**
+ * Copy of constants defined in com.android.car.media.common.MediaConstants until they can be moved
+ * to a shared location available to all media apps. This makes un-bundling TestMediaApp easier.
+ */
+public class MediaKeys {
+
+    /** Integer extra indicating the recommended size (in pixels) for media art bitmaps. */
+    public static final String EXTRA_MEDIA_ART_SIZE_HINT_PIXELS =
+            "android.media.extras.MEDIA_ART_SIZE_HINT_PIXELS";
+
+    /**
+     * Bundle extra holding the Pending Intent to launch to let users resolve the current error.
+     * See {@link #ERROR_RESOLUTION_ACTION_LABEL} for more details.
+     */
+    static final String ERROR_RESOLUTION_ACTION_INTENT =
+            "android.media.extras.ERROR_RESOLUTION_ACTION_INTENT";
+
+
+    /**
+     * Bundle extra indicating the label of the button users can tap to resolve an error state.
+     */
+    static final String ERROR_RESOLUTION_ACTION_LABEL =
+            "android.media.extras.ERROR_RESOLUTION_ACTION_LABEL";
+
+    /**
+     * Bundle extra indicating the presentation hint for playable media items. See {@link
+     * #CONTENT_STYLE_LIST_ITEM_HINT_VALUE} or {@link #CONTENT_STYLE_GRID_ITEM_HINT_VALUE}
+     */
+    static final String CONTENT_STYLE_PLAYABLE_HINT =
+            "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT";
+
+    /**
+     * Bundle extra indicating the presentation hint for browsable media items. See {@link
+     * #CONTENT_STYLE_LIST_ITEM_HINT_VALUE} or {@link #CONTENT_STYLE_GRID_ITEM_HINT_VALUE}
+     */
+    static final String CONTENT_STYLE_BROWSABLE_HINT =
+            "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT";
+
+    /**
+     * Value for {@link #CONTENT_STYLE_PLAYABLE_HINT} and {@link #CONTENT_STYLE_BROWSABLE_HINT} that
+     * hints the corresponding items should be presented as lists.
+     */
+    static final int CONTENT_STYLE_LIST_ITEM_HINT_VALUE = 1;
+
+    /**
+     * Value for {@link #CONTENT_STYLE_PLAYABLE_HINT} and {@link #CONTENT_STYLE_BROWSABLE_HINT} that
+     * hints the corresponding items should be presented as grids.
+     */
+    static final int CONTENT_STYLE_GRID_ITEM_HINT_VALUE = 2;
+
+    /**
+     * Value for {@link #CONTENT_STYLE_BROWSABLE_HINT} that hints the corresponding items should be
+     * presented as a "category" list, where media items are browsable and represented by a
+     * meaningful icon.
+     */
+    public static final int CONTENT_STYLE_CATEGORY_LIST_ITEM_HINT_VALUE = 3;
+
+    /**
+     * Value for {@link #CONTENT_STYLE_BROWSABLE_HINT} that hints the corresponding items should be
+     * presented as a "category" grid, where media items are browsable and represented by a
+     * meaningful icon.
+     */
+    public static final int CONTENT_STYLE_CATEGORY_GRID_ITEM_HINT_VALUE = 4;
+}
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaAssetProvider.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaAssetProvider.java
deleted file mode 100644
index ca72fde..0000000
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaAssetProvider.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (c) 2019, The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.android.car.media.testmediaapp;
-
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.content.res.AssetFileDescriptor;
-import android.database.Cursor;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.io.FileNotFoundException;
-import java.io.IOException;
-
-public class TmaAssetProvider extends ContentProvider {
-
-    private static final String TAG = "TmaAssetProvider";
-
-    private static final String URI_PREFIX = "content://com.android.car.media.testmediaapp.assets/";
-
-
-    public static String buildUriString(String localAssetFilePath) {
-        return URI_PREFIX + localAssetFilePath;
-    }
-
-    @Override
-    public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
-        String file_path = uri.getPath();
-        if (TextUtils.isEmpty(file_path)) throw new FileNotFoundException();
-        try {
-            if (file_path.startsWith("/")) {
-                file_path = file_path.substring(1);
-            }
-            return getContext().getAssets().openFd(file_path);
-        } catch (IOException e) {
-            Log.e(TAG, "openAssetFile failed: " + e);
-            return null;
-        }
-    }
-
-    @Override
-    public boolean onCreate() {
-        return false;
-    }
-
-    @Override
-    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
-            String sortOrder) {
-        return null;
-    }
-
-    @Override
-    public String getType(Uri uri) {
-        return null;
-    }
-
-    @Override
-    public Uri insert(Uri uri, ContentValues values) {
-        return null;
-    }
-
-    @Override
-    public int delete(Uri uri, String selection, String[] selectionArgs) {
-        return 0;
-    }
-
-    @Override
-    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
-        return 0;
-    }
-}
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaBrowser.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaBrowser.java
index 8292064..7a62137 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaBrowser.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaBrowser.java
@@ -15,6 +15,10 @@
  */
 package com.android.car.media.testmediaapp;
 
+import static com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaBrowseNodeType.LEAF_CHILDREN;
+import static com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaBrowseNodeType.QUEUE_ONLY;
+import static com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaLoginEventOrder.PLAYBACK_STATE_UPDATE_FIRST;
+
 import android.content.Context;
 import android.media.AudioManager;
 import android.os.Bundle;
@@ -30,11 +34,13 @@
 
 import com.android.car.media.testmediaapp.loader.TmaLoader;
 import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaAccountType;
-import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaNodeReplyDelay;
+import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaReplyDelay;
 import com.android.car.media.testmediaapp.prefs.TmaPrefs;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 
 /**
@@ -46,10 +52,17 @@
  * {@link TmaPlayer}.
  */
 public class TmaBrowser extends MediaBrowserServiceCompat {
+    private static final String TAG = "TmaBrowser";
 
+    private static final int MAX_SEARCH_DEPTH = 4;
     private static final String MEDIA_SESSION_TAG = "TEST_MEDIA_SESSION";
     private static final String ROOT_ID = "_ROOT_ID_";
     private static final String SEARCH_SUPPORTED = "android.media.browse.SEARCH_SUPPORTED";
+    /**
+     * Extras key to allow Android Auto to identify the browse service from the media session.
+     */
+    private static final String BROWSE_SERVICE_FOR_SESSION_KEY =
+        "android.media.session.BROWSE_SERVICE";
 
     private TmaPrefs mPrefs;
     private Handler mHandler;
@@ -58,7 +71,6 @@
     private TmaPlayer mPlayer;
 
     private BrowserRoot mRoot;
-    private String mLastLoadedNodeId;
 
     @Override
     public void onCreate() {
@@ -75,6 +87,9 @@
         mSession.setCallback(mPlayer);
         mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
                 | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
+        Bundle mediaSessionExtras = new Bundle();
+        mediaSessionExtras.putString(BROWSE_SERVICE_FOR_SESSION_KEY, TmaBrowser.class.getName());
+        mSession.setExtras(mediaSessionExtras);
 
         mPrefs.mAccountType.registerChangeListener(
                 (oldValue, newValue) -> onAccountChanged(newValue));
@@ -85,9 +100,11 @@
         mPrefs.mRootReplyDelay.registerChangeListener(
                 (oldValue, newValue) -> invalidateRoot());
 
-        Bundle extras = new Bundle();
-        extras.putBoolean(SEARCH_SUPPORTED, true);
-        mRoot = new BrowserRoot(ROOT_ID, extras);
+        Bundle browserRootExtras = new Bundle();
+        browserRootExtras.putBoolean(SEARCH_SUPPORTED, true);
+        mRoot = new BrowserRoot(ROOT_ID, browserRootExtras);
+
+        updatePlaybackState(mPrefs.mAccountType.getValue());
     }
 
     @Override
@@ -99,20 +116,35 @@
     }
 
     private void onAccountChanged(TmaAccountType accountType) {
+        if (PLAYBACK_STATE_UPDATE_FIRST.equals(mPrefs.mLoginEventOrder.getValue())) {
+            updatePlaybackState(accountType);
+            invalidateRoot();
+        } else {
+            invalidateRoot();
+            (new Handler()).postDelayed(() -> {
+                updatePlaybackState(accountType);
+            }, 3000);
+        }
+    }
+
+    private void updatePlaybackState(TmaAccountType accountType) {
         if (accountType == TmaAccountType.NONE) {
+            mSession.setMetadata(null);
+            mPlayer.onStop();
             mPlayer.setPlaybackState(
                     new TmaMediaEvent(TmaMediaEvent.EventState.ERROR,
                             TmaMediaEvent.StateErrorCode.AUTHENTICATION_EXPIRED,
                             getResources().getString(R.string.no_account),
                             getResources().getString(R.string.select_account),
-                            TmaMediaEvent.ResolutionIntent.PREFS, 0));
+                            TmaMediaEvent.ResolutionIntent.PREFS,
+                            TmaMediaEvent.Action.NONE, 0, null));
         } else {
             // TODO don't reset error in all cases...
             PlaybackStateCompat.Builder playbackState = new PlaybackStateCompat.Builder();
             playbackState.setState(PlaybackStateCompat.STATE_PAUSED, 0, 0);
+            playbackState.setActions(PlaybackStateCompat.ACTION_PREPARE);
             mSession.setPlaybackState(playbackState.build());
         }
-        invalidateRoot();
     }
 
     private void invalidateRoot() {
@@ -122,24 +154,38 @@
     @Override
     public BrowserRoot onGetRoot(
             @NonNull String clientPackageName, int clientUid, Bundle rootHints) {
+        if (rootHints == null) {
+            Log.e(TAG, "Client " + clientPackageName + " didn't set rootHints.");
+            throw new NullPointerException("rootHints is null");
+        }
+        Log.i(TAG, "onGetroot client: " + clientPackageName + " EXTRA_MEDIA_ART_SIZE_HINT_PIXELS: "
+                + rootHints.getInt(MediaKeys.EXTRA_MEDIA_ART_SIZE_HINT_PIXELS, 0));
         return mRoot;
     }
 
     @Override
     public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaItem>> result) {
-        mLastLoadedNodeId = parentId;
         getMediaItemsWithDelay(parentId, result, null);
+
+        if (QUEUE_ONLY.equals(mPrefs.mRootNodeType.getValue()) && ROOT_ID.equals(parentId)) {
+            TmaMediaItem queue = mLibrary.getRoot(LEAF_CHILDREN);
+            if (queue != null) {
+                mSession.setQueue(queue.buildQueue());
+                mPlayer.prepareMediaItem(queue.getPlayableByIndex(0));
+            }
+        }
     }
 
     @Override
-    public void onSearch(final String query, final Bundle extras, Result<List<MediaItem>> result) {
-        getMediaItemsWithDelay(mLastLoadedNodeId, result, query);
+    public void onSearch(@NonNull String query, Bundle extras,
+            @NonNull Result<List<MediaItem>> result) {
+        getMediaItemsWithDelay(ROOT_ID, result, query);
     }
 
     private void getMediaItemsWithDelay(@NonNull String parentId,
             @NonNull Result<List<MediaItem>> result, @Nullable String filter) {
         // TODO: allow per item override of the delay ?
-        TmaNodeReplyDelay delay = mPrefs.mRootReplyDelay.getValue();
+        TmaReplyDelay delay = mPrefs.mRootReplyDelay.getValue();
         Runnable task = () -> {
             TmaMediaItem node;
             if (TmaAccountType.NONE.equals(mPrefs.mAccountType.getValue())) {
@@ -152,23 +198,46 @@
 
             if (node == null) {
                 result.sendResult(null);
+            } else if (filter != null) {
+                List<MediaItem> hits = new ArrayList<>(50);
+                Pattern pat = Pattern.compile(Pattern.quote(filter), Pattern.CASE_INSENSITIVE);
+                addSearchResults(node, pat.matcher(""), hits, MAX_SEARCH_DEPTH);
+                result.sendResult(hits);
             } else {
                 List<MediaItem> items = new ArrayList<>(node.mChildren.size());
                 for (TmaMediaItem child : node.mChildren) {
-                    MediaItem item = child.toMediaItem();
-                    CharSequence title = item.getDescription().getTitle();
-                    if (filter == null || (title != null && title.toString().contains(filter))) {
-                        items.add(item);
-                    }
+                    items.add(child.toMediaItem());
                 }
                 result.sendResult(items);
             }
         };
-        if (delay == TmaNodeReplyDelay.NONE) {
+        if (delay == TmaReplyDelay.NONE) {
             task.run();
         } else {
             result.detach();
             mHandler.postDelayed(task, delay.mReplyDelayMs);
         }
     }
+
+    private void addSearchResults(@Nullable TmaMediaItem node, Matcher matcher,
+            List<MediaItem> hits, int currentDepth) {
+        if (node == null || currentDepth <= 0) {
+            return;
+        }
+
+        for (TmaMediaItem child : node.mChildren) {
+            MediaItem item = child.toMediaItem();
+            CharSequence title = item.getDescription().getTitle();
+            if (title != null) {
+                matcher.reset(title);
+                if (matcher.find()) {
+                    hits.add(item);
+                }
+            }
+
+            // Ask the library to load the grand children
+            child = mLibrary.getMediaItemById(child.getMediaId());
+            addSearchResults(child, matcher, hits, currentDepth - 1);
+        }
+    }
 }
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaBrowser2.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaBrowser2.java
new file mode 100644
index 0000000..e42bb11
--- /dev/null
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaBrowser2.java
@@ -0,0 +1,26 @@
+package com.android.car.media.testmediaapp;
+
+import android.os.Bundle;
+import android.support.v4.media.MediaBrowserCompat;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.media.MediaBrowserServiceCompat;
+
+import java.util.List;
+
+/** Empty browser to test having two browsers in the apk. */
+public class TmaBrowser2 extends MediaBrowserServiceCompat {
+
+    @Nullable
+    @Override
+    public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid,
+            @Nullable Bundle rootHints) {
+        return null;
+    }
+
+    @Override
+    public void onLoadChildren(@NonNull String parentId,
+            @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
+    }
+}
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaLibrary.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaLibrary.java
index d67f157..327a2b6 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaLibrary.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaLibrary.java
@@ -48,9 +48,12 @@
         mLoader = loader;
         mRootAssetPaths.put(TmaBrowseNodeType.NULL, null);
         mRootAssetPaths.put(TmaBrowseNodeType.EMPTY, "media_items/empty.json");
+        mRootAssetPaths.put(TmaBrowseNodeType.QUEUE_ONLY, "media_items/empty.json");
+        mRootAssetPaths.put(TmaBrowseNodeType.SINGLE_TAB, "media_items/single_node.json");
         mRootAssetPaths.put(TmaBrowseNodeType.NODE_CHILDREN, "media_items/only_nodes.json");
         mRootAssetPaths.put(TmaBrowseNodeType.LEAF_CHILDREN, "media_items/simple_leaves.json");
         mRootAssetPaths.put(TmaBrowseNodeType.MIXED_CHILDREN, "media_items/mixed.json");
+        mRootAssetPaths.put(TmaBrowseNodeType.UNTAGGED, "media_items/untagged.json");
     }
 
     @Nullable
@@ -63,7 +66,7 @@
     TmaMediaItem getMediaItemById(String mediaId) {
         TmaMediaItem result = mMediaItemsByMediaId.get(mediaId);
         // Processing includes only on request allows recursive structures :-)
-        if (!TextUtils.isEmpty(result.mInclude)) {
+        if (result != null && !TextUtils.isEmpty(result.mInclude)) {
             result = result.append(loadAssetFile(result.mInclude).mChildren);
         }
         return result;
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaMediaEvent.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaMediaEvent.java
index 3bec7ac..491f940 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaMediaEvent.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaMediaEvent.java
@@ -42,15 +42,18 @@
 import static android.support.v4.media.session.PlaybackStateCompat.STATE_STOPPED;
 
 import android.support.v4.media.session.PlaybackStateCompat.State;
+import android.util.Log;
 
 /**
  * Contains the info needed to generate a new playback state.
  */
 public class TmaMediaEvent {
 
+    private static final String TAG = "TmaMediaEvent";
+
     public static final TmaMediaEvent INSTANT_PLAYBACK =
             new TmaMediaEvent(EventState.PLAYING, StateErrorCode.UNKNOWN_ERROR, null, null,
-                    ResolutionIntent.NONE, 0);
+                    ResolutionIntent.NONE, Action.NONE, 0, null);
 
     /** The name of each entry is the value used in the json file. */
     public enum EventState {
@@ -102,28 +105,53 @@
         PREFS
     }
 
+    /** The name of each entry is the value used in the json file. */
+    public enum Action {
+        NONE,
+        RESET_METADATA
+    }
+
     final EventState mState;
     final StateErrorCode mErrorCode;
     final String mErrorMessage;
     final String mActionLabel;
     final ResolutionIntent mResolutionIntent;
+    final Action mAction;
     /** How long to wait before sending the event to the app. */
     final int mPostDelayMs;
+    private final String mExceptionClass;
 
     public TmaMediaEvent(EventState state, StateErrorCode errorCode, String errorMessage,
-            String actionLabel, ResolutionIntent resolutionIntent, int postDelayMs) {
+            String actionLabel, ResolutionIntent resolutionIntent, Action action, int postDelayMs,
+            String exceptionClass) {
         mState = state;
         mErrorCode = errorCode;
         mErrorMessage = errorMessage;
         mActionLabel = actionLabel;
         mResolutionIntent = resolutionIntent;
+        mAction = action;
         mPostDelayMs = postDelayMs;
+        mExceptionClass = exceptionClass;
     }
 
     boolean premiumAccountRequired() {
         return mState == EventState.ERROR && mErrorCode == StateErrorCode.PREMIUM_ACCOUNT_REQUIRED;
     }
 
+    void maybeThrow() {
+        if (mExceptionClass != null) {
+            RuntimeException exception = null;
+            try {
+                Class aClass = Class.forName(mExceptionClass);
+                exception = (RuntimeException) aClass.newInstance();
+            } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
+                Log.e(TAG, "Class error for " + mExceptionClass + " : " + e);
+            }
+
+            if (exception != null) throw exception;
+        }
+    }
+
     @Override
     public String toString() {
         return "TmaMediaEvent{" +
@@ -132,7 +160,9 @@
                 ", mErrorMessage='" + mErrorMessage + '\'' +
                 ", mActionLabel='" + mActionLabel + '\'' +
                 ", mResolutionIntent=" + mResolutionIntent +
+                ", mAction=" + mAction +
                 ", mPostDelayMs=" + mPostDelayMs +
+                ", mExceptionClass=" + mExceptionClass +
                 '}';
     }
 }
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaMediaItem.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaMediaItem.java
index 6925588..af1b2e3 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaMediaItem.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaMediaItem.java
@@ -22,14 +22,8 @@
 import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION;
 import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_MEDIA_ID;
 
-import static com.android.car.media.common.MediaConstants.CONTENT_STYLE_BROWSABLE_HINT;
-import static com.android.car.media.common.MediaConstants.CONTENT_STYLE_GRID_ITEM_HINT_VALUE;
-import static com.android.car.media.common.MediaConstants.CONTENT_STYLE_LIST_ITEM_HINT_VALUE;
-import static com.android.car.media.common.MediaConstants.CONTENT_STYLE_PLAYABLE_HINT;
-
 import android.os.Bundle;
 import android.support.v4.media.MediaBrowserCompat.MediaItem;
-import android.support.v4.media.MediaBrowserCompat.MediaItem.Flags;
 import android.support.v4.media.MediaDescriptionCompat;
 import android.support.v4.media.MediaMetadataCompat;
 import android.support.v4.media.session.MediaSessionCompat;
@@ -47,8 +41,10 @@
     /** The name of each entry is the value used in the json file. */
     public enum ContentStyle {
         NONE (0),
-        LIST (CONTENT_STYLE_LIST_ITEM_HINT_VALUE),
-        GRID (CONTENT_STYLE_GRID_ITEM_HINT_VALUE);
+        LIST (MediaKeys.CONTENT_STYLE_LIST_ITEM_HINT_VALUE),
+        GRID (MediaKeys.CONTENT_STYLE_GRID_ITEM_HINT_VALUE),
+        LIST_CATEGORY(MediaKeys.CONTENT_STYLE_CATEGORY_LIST_ITEM_HINT_VALUE),
+        GRID_CATEGORY(MediaKeys.CONTENT_STYLE_CATEGORY_GRID_ITEM_HINT_VALUE);
         final int mBundleValue;
         ContentStyle(int value) {
             mBundleValue = value;
@@ -73,7 +69,7 @@
 
     }
 
-    private final @MediaItem.Flags int mFlags;
+    private final int mFlags;
     private final MediaMetadataCompat mMediaMetadata;
     private final ContentStyle mPlayableStyle;
     private final ContentStyle mBrowsableStyle;
@@ -93,7 +89,7 @@
     int mHearts;
 
 
-    public TmaMediaItem(@Flags int flags, ContentStyle playableStyle, ContentStyle browsableStyle,
+    public TmaMediaItem(int flags, ContentStyle playableStyle, ContentStyle browsableStyle,
             MediaMetadataCompat metadata, List<TmaCustomAction> customActions,
             List<TmaMediaEvent> mediaEvents,
             List<TmaMediaItem> children, String include) {
@@ -124,7 +120,11 @@
         return mParent;
     }
 
+    @Nullable
     TmaMediaItem getPlayableByIndex(long index) {
+        if (index < 0 || index >= mPlayableChildren.size()) {
+            return null;
+        }
         return mPlayableChildren.get((int)index);
     }
 
@@ -209,8 +209,8 @@
             extras.putAll(metadataDescription.getExtras());
         }
 
-        extras.putInt(CONTENT_STYLE_PLAYABLE_HINT, mPlayableStyle.mBundleValue);
-        extras.putInt(CONTENT_STYLE_BROWSABLE_HINT, mBrowsableStyle.mBundleValue);
+        extras.putInt(MediaKeys.CONTENT_STYLE_PLAYABLE_HINT, mPlayableStyle.mBundleValue);
+        extras.putInt(MediaKeys.CONTENT_STYLE_BROWSABLE_HINT, mBrowsableStyle.mBundleValue);
 
         bob.setExtras(extras);
         return bob.build();
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaPlayer.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaPlayer.java
index c6213b2..d8fab6c 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaPlayer.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaPlayer.java
@@ -21,6 +21,7 @@
 import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PAUSE;
 import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY;
 import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID;
+import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PREPARE;
 import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SEEK_TO;
 import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
 import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
@@ -28,9 +29,6 @@
 import static android.support.v4.media.session.PlaybackStateCompat.ERROR_CODE_APP_ERROR;
 import static android.support.v4.media.session.PlaybackStateCompat.STATE_ERROR;
 
-import static com.android.car.media.common.MediaConstants.ERROR_RESOLUTION_ACTION_INTENT;
-import static com.android.car.media.common.MediaConstants.ERROR_RESOLUTION_ACTION_LABEL;
-
 import androidx.annotation.Nullable;
 import android.app.PendingIntent;
 import android.content.Context;
@@ -44,6 +42,7 @@
 import android.util.Log;
 import android.widget.Toast;
 
+import com.android.car.media.testmediaapp.TmaMediaEvent.Action;
 import com.android.car.media.testmediaapp.TmaMediaEvent.EventState;
 import com.android.car.media.testmediaapp.TmaMediaEvent.ResolutionIntent;
 import com.android.car.media.testmediaapp.TmaMediaItem.TmaCustomAction;
@@ -107,8 +106,8 @@
             PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, prefsIntent, 0);
 
             Bundle extras = new Bundle();
-            extras.putString(ERROR_RESOLUTION_ACTION_LABEL, event.mActionLabel);
-            extras.putParcelable(ERROR_RESOLUTION_ACTION_INTENT, pendingIntent);
+            extras.putString(MediaKeys.ERROR_RESOLUTION_ACTION_LABEL, event.mActionLabel);
+            extras.putParcelable(MediaKeys.ERROR_RESOLUTION_ACTION_INTENT, pendingIntent);
             state.setExtras(extras);
         }
 
@@ -145,6 +144,46 @@
     }
 
     @Override
+    public void onPrepareFromMediaId(String mediaId, Bundle extras) {
+        super.onPrepareFromMediaId(mediaId, extras);
+
+        TmaMediaItem item = mLibrary.getMediaItemById(mediaId);
+        prepareMediaItem(item);
+    }
+
+    @Override
+    public void onPrepare() {
+        super.onPrepare();
+        if (!mSession.isActive()) {
+            mSession.setActive(true);
+        }
+        // Prepare the first playable item (at root level) as the active item
+        if (mActiveItem == null) {
+            TmaMediaItem root = mLibrary.getRoot(mPrefs.mRootNodeType.getValue());
+            if (root != null) {
+                prepareMediaItem(root.getPlayableByIndex(0));
+            }
+        }
+    }
+
+    void prepareMediaItem(@Nullable TmaMediaItem item) {
+        if (item != null && item.getParent() != null) {
+            if (mIsPlaying) {
+                stopPlayback();
+            }
+            mActiveItem = item;
+            mActiveItem.updateSessionMetadata(mSession);
+            mSession.setQueue(item.getParent().buildQueue());
+
+            PlaybackStateCompat.Builder state = new PlaybackStateCompat.Builder()
+                    .setState(PlaybackStateCompat.STATE_PAUSED, mCurrentPositionMs, mPlaybackSpeed)
+                    .setActions(addActions(ACTION_PLAY));
+            setActiveItemState(state);
+            mSession.setPlaybackState(state.build());
+        }
+    }
+
+    @Override
     public void onSkipToQueueItem(long id) {
         super.onSkipToQueueItem(id);
         if (mActiveItem != null && mActiveItem.getParent() != null) {
@@ -226,11 +265,14 @@
         if (mActiveItem == null) return;
 
         TmaMediaEvent event = mActiveItem.mMediaEvents.get(mNextEventIndex);
+        event.maybeThrow();
 
         if (event.premiumAccountRequired() &&
                 TmaAccountType.PAID.equals(mPrefs.mAccountType.getValue())) {
             Log.i(TAG, "Ignoring even for paid account");
             return;
+        } else if (Action.RESET_METADATA.equals(event.mAction)) {
+            mSession.setMetadata(mSession.getController().getMetadata());
         } else {
             setPlaybackState(event);
         }
@@ -304,7 +346,8 @@
     }
 
     private long addActions(long actions) {
-        actions |= ACTION_PLAY_FROM_MEDIA_ID | ACTION_SKIP_TO_QUEUE_ITEM | ACTION_SEEK_TO;
+        actions |= ACTION_PLAY_FROM_MEDIA_ID | ACTION_SKIP_TO_QUEUE_ITEM | ACTION_SEEK_TO
+                | ACTION_PREPARE;
 
         if (mActiveItem != null) {
             if (mActiveItem.getNext() != null) {
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaPublicProvider.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaPublicProvider.java
new file mode 100644
index 0000000..e7eb31b
--- /dev/null
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaPublicProvider.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2019, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.android.car.media.testmediaapp;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.car.media.testmediaapp.prefs.TmaPrefs;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class TmaPublicProvider extends ContentProvider {
+
+    private static final String TAG = "TmaAssetProvider";
+
+    private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
+
+    private static final String AUTHORITY = "com.android.car.media.testmediaapp.public";
+
+    private static final String FILES = "/files/";
+    private static final String ASSETS = "/assets/";
+
+    private static final String CONTENT_URI_PREFIX =
+            ContentResolver.SCHEME_CONTENT + "://" + AUTHORITY + "/";
+
+    private static final String RESOURCE_URI_PREFIX =
+            ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + AUTHORITY + "/";
+
+
+    public static String buildUriString(String localArt) {
+        String prefix = localArt.startsWith("drawable") ? RESOURCE_URI_PREFIX : CONTENT_URI_PREFIX;
+        return prefix + localArt;
+    }
+
+    private int mAssetDelay = 0;
+
+
+    @Override
+    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+        String path = uri.getPath();
+
+        if (TextUtils.isEmpty(path) || !path.startsWith(FILES)) {
+            throw new FileNotFoundException(path);
+        }
+
+        Log.i(TAG, "TmaAssetProvider#openFile uri: " + uri + " path: " + path);
+
+        File localFile = new File(getContext().getFilesDir(), path);
+        if (!localFile.exists()) {
+            downloadFile(localFile, path.substring(FILES.length()));
+        }
+
+        return ParcelFileDescriptor.open(localFile,ParcelFileDescriptor.MODE_READ_ONLY);
+    }
+
+    @Override
+    public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
+        String path = uri.getPath();
+        if (TextUtils.isEmpty(path) || !path.startsWith(ASSETS)) {
+            // The ImageDecoder and media center code always try to open as asset first, but
+            // super delegates to openFile...
+            return super.openAssetFile(uri, mode);
+        }
+
+        Log.i(TAG, "TmaAssetProvider#openAssetFile uri: " + uri + " path: " + path);
+
+        try {
+            Thread.sleep(mAssetDelay + (int)(mAssetDelay * (Math.random())));
+        } catch (InterruptedException ignored) {
+        }
+
+        try {
+            return getContext().getAssets().openFd(path.substring(ASSETS.length()));
+        } catch (IOException e) {
+            Log.e(TAG, "openAssetFile failed: " + e);
+            return null;
+        }
+    }
+
+    private void downloadFile(File localFile, String assetsPath) {
+        try {
+            localFile.getParentFile().mkdirs();
+
+            InputStream input = getContext().getAssets().open(assetsPath);
+            OutputStream output = new FileOutputStream(localFile);
+
+            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
+            int n;
+            while (-1 != (n = input.read(buffer))) {
+                output.write(buffer, 0, n);
+            }
+
+        } catch (IOException e) {
+            Log.e(TAG, "downloadFile failed: " + e);
+        }
+    }
+
+    @Override
+    public boolean onCreate() {
+        TmaPrefs.getInstance(getContext()).mAssetReplyDelay.registerChangeListener(
+                (oldValue, newValue) -> mAssetDelay = newValue.mReplyDelayMs);
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        return null;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        return null;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        return 0;
+    }
+}
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/loader/TmaMediaEventReader.java b/TestMediaApp/src/com/android/car/media/testmediaapp/loader/TmaMediaEventReader.java
index b6d1a7a..2222afa 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/loader/TmaMediaEventReader.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/loader/TmaMediaEventReader.java
@@ -21,9 +21,12 @@
 import static com.android.car.media.testmediaapp.loader.TmaLoaderUtils.getInt;
 import static com.android.car.media.testmediaapp.loader.TmaLoaderUtils.getString;
 
+import android.util.Log;
+
 import androidx.annotation.Nullable;
 
 import com.android.car.media.testmediaapp.TmaMediaEvent;
+import com.android.car.media.testmediaapp.TmaMediaEvent.Action;
 import com.android.car.media.testmediaapp.TmaMediaEvent.EventState;
 import com.android.car.media.testmediaapp.TmaMediaEvent.ResolutionIntent;
 import com.android.car.media.testmediaapp.TmaMediaEvent.StateErrorCode;
@@ -51,8 +54,10 @@
         ERROR_MESSAGE,
         ACTION_LABEL,
         INTENT,
+        ACTION,
         /** How long to wait before sending the event to the app. */
-        POST_DELAY_MS
+        POST_DELAY_MS,
+        THROW_EXCEPTION
     }
 
     private static TmaMediaEventReader sInstance;
@@ -67,11 +72,13 @@
     private final Map<String, EventState> mEventStates;
     private final Map<String, StateErrorCode> mErrorCodes;
     private final Map<String, ResolutionIntent> mResolutionIntents;
+    private final Map<String, Action> mActions;
 
     private TmaMediaEventReader() {
         mEventStates = enumNamesToValues(EventState.values());
         mErrorCodes = enumNamesToValues(StateErrorCode.values());
         mResolutionIntents = enumNamesToValues(ResolutionIntent.values());
+        mActions = enumNamesToValues(Action.values());
     }
 
     @Nullable
@@ -83,6 +90,8 @@
                 getString(json, Keys.ERROR_MESSAGE),
                 getString(json, Keys.ACTION_LABEL),
                 getEnum(json, Keys.INTENT, mResolutionIntents, ResolutionIntent.NONE),
-                getInt(json, Keys.POST_DELAY_MS));
+                getEnum(json, Keys.ACTION, mActions, Action.NONE),
+                getInt(json, Keys.POST_DELAY_MS),
+                getString(json, Keys.THROW_EXCEPTION));
     }
 }
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/loader/TmaMediaMetadataReader.java b/TestMediaApp/src/com/android/car/media/testmediaapp/loader/TmaMediaMetadataReader.java
index 95f8f89..8cc4843 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/loader/TmaMediaMetadataReader.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/loader/TmaMediaMetadataReader.java
@@ -53,12 +53,13 @@
 import android.support.v4.media.MediaMetadataCompat;
 import android.util.Log;
 
-import com.android.car.media.testmediaapp.TmaAssetProvider;
+import com.android.car.media.testmediaapp.TmaPublicProvider;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import java.util.EnumSet;
+import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
 
@@ -140,7 +141,9 @@
 
     MediaMetadataCompat fromJson(JSONObject object) throws JSONException {
         MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
-        for (String jsonKey : object.keySet()) {
+        Iterator<String> keys = object.keys();
+        while (keys.hasNext()) {
+            String jsonKey = keys.next();
             MetadataKey key = mMetadataKeys.get(jsonKey);
             if (key != null) {
                 switch (key.mKeyType) {
@@ -150,7 +153,7 @@
                     case TEXT:
                         String value = object.getString(jsonKey);
                         if (mUriKeys.contains(key)) {
-                            value = TmaAssetProvider.buildUriString(value);
+                            value = TmaPublicProvider.buildUriString(value);
                         }
                         builder.putString(key.mLongName, value);
                         break;
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/phone/TmaLauncherActivity.java b/TestMediaApp/src/com/android/car/media/testmediaapp/phone/TmaLauncherActivity.java
index e38defa..2727ae4 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/phone/TmaLauncherActivity.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/phone/TmaLauncherActivity.java
@@ -11,6 +11,7 @@
 
 import androidx.appcompat.app.AppCompatActivity;
 
+import com.android.car.media.testmediaapp.MediaKeys;
 import com.android.car.media.testmediaapp.TmaBrowser;
 import com.android.car.media.testmediaapp.prefs.TmaPrefsActivity;
 import com.android.car.media.testmediaapp.R;
@@ -38,9 +39,11 @@
             startActivity(prefsIntent);
         });
 
-
+        Bundle rootHints = new Bundle();
+        // TODO: 256 is just a placeholder. We'd better find a proper value.
+        rootHints.putInt(MediaKeys.EXTRA_MEDIA_ART_SIZE_HINT_PIXELS, 256);
         mediaBrowser = new MediaBrowserCompat(this, new ComponentName(this, TmaBrowser.class),
-                mConnectionCallbacks, null);
+                mConnectionCallbacks, rootHints);
     }
 
     private final MediaBrowserCompat.ConnectionCallback mConnectionCallbacks =
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/prefs/TmaEnumPrefs.java b/TestMediaApp/src/com/android/car/media/testmediaapp/prefs/TmaEnumPrefs.java
index cbbf92b..3702929 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/prefs/TmaEnumPrefs.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/prefs/TmaEnumPrefs.java
@@ -51,17 +51,19 @@
     }
 
     /** For simulating various reply speeds. */
-    public enum TmaNodeReplyDelay implements EnumPrefValue {
+    public enum TmaReplyDelay implements EnumPrefValue {
         NONE("None", "none", 0),
         SHORT("Short", "short", 50),
+        SHORT_PLUS("Short+", "short+", 150),
         MEDIUM("Medium", "medium", 500),
+        MEDIUM_PLUS("Medium+", "medium+", 2000),
         LONG("Long", "long", 5000),
         EXTRA_LONG("Extra-Long", "extra-long", 10000);
 
         private final PrefValueImpl mPrefValue;
         public final int mReplyDelayMs;
 
-        TmaNodeReplyDelay(String displayTitle, String id, int delayMs) {
+        TmaReplyDelay(String displayTitle, String id, int delayMs) {
             mPrefValue = new PrefValueImpl(displayTitle + "(" + delayMs + ")", id);
             mReplyDelayMs = delayMs;
         }
@@ -81,9 +83,12 @@
     public enum TmaBrowseNodeType implements EnumPrefValue {
         NULL("Null (error)", "null"),
         EMPTY("Empty", "empty"),
+        QUEUE_ONLY("Queue only", "queue-only"),
+        SINGLE_TAB("Single browse-able tab", "single-tab"),
         NODE_CHILDREN("Only browse-able content", "nodes"),
         LEAF_CHILDREN("Only playable content (basic working and error cases)", "leaves"),
-        MIXED_CHILDREN("Mixed content (apps are not supposed to do that)", "mixed");
+        MIXED_CHILDREN("Mixed content (apps are not supposed to do that)", "mixed"),
+        UNTAGGED("Untagged media items (not playable or browsable)", "untagged");
 
         private final PrefValueImpl mPrefValue;
 
@@ -102,6 +107,29 @@
         }
     }
 
+    /* To simulate the events order after login. Media apps should update playback state first, then
+     * load the browse tree. But sometims some apps (e.g., GPB) don't follow this order strictly. */
+    public enum TmaLoginEventOrder implements EnumPrefValue {
+        PLAYBACK_STATE_UPDATE_FIRST("Update playback state first", "state-first"),
+        BROWSE_TREE_LOAD_FRIST("Load browse tree first", "tree-first");
+
+        private final PrefValueImpl mPrefValue;
+
+        TmaLoginEventOrder(String displayTitle, String id) {
+            mPrefValue = new PrefValueImpl(displayTitle, id);
+        }
+
+        @Override
+        public String getTitle() {
+            return mPrefValue.getTitle();
+        }
+
+        @Override
+        public String getId() {
+            return mPrefValue.getId();
+        }
+    }
+
     private static class PrefValueImpl implements EnumPrefValue {
 
         private final String mDisplayTitle;
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/prefs/TmaPrefs.java b/TestMediaApp/src/com/android/car/media/testmediaapp/prefs/TmaPrefs.java
index e3f9417..8e9d89f 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/prefs/TmaPrefs.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/prefs/TmaPrefs.java
@@ -24,7 +24,8 @@
 
 import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaAccountType;
 import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaBrowseNodeType;
-import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaNodeReplyDelay;
+import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaLoginEventOrder;
+import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaReplyDelay;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -40,7 +41,13 @@
     public final PrefEntry<TmaBrowseNodeType> mRootNodeType;
 
     /** Wait time before sending a node reply, unless overridden in json (when supported). */
-    public final PrefEntry<TmaNodeReplyDelay> mRootReplyDelay;
+    public final PrefEntry<TmaReplyDelay> mRootReplyDelay;
+
+    /** Wait time for openAssetFile. */
+    public final PrefEntry<TmaReplyDelay> mAssetReplyDelay;
+
+    /** Media apps event (update playback state, load browse tree) order after login. */
+    public final PrefEntry<TmaLoginEventOrder> mLoginEventOrder;
 
 
     public synchronized static TmaPrefs getInstance(Context context) {
@@ -58,7 +65,9 @@
     private enum TmaPrefKey {
         ACCOUNT_TYPE_KEY,
         ROOT_NODE_TYPE_KEY,
-        ROOT_REPLY_DELAY_KEY
+        ROOT_REPLY_DELAY_KEY,
+        ASSET_REPLY_DELAY_KEY,
+        LOGIN_EVENT_ORDER_KEY
     }
 
     /**
@@ -120,7 +129,13 @@
                 TmaBrowseNodeType.values(), TmaBrowseNodeType.NULL);
 
         mRootReplyDelay = new EnumPrefEntry<>(TmaPrefKey.ROOT_REPLY_DELAY_KEY,
-                TmaNodeReplyDelay.values(), TmaNodeReplyDelay.NONE);
+                TmaReplyDelay.values(), TmaReplyDelay.NONE);
+
+        mAssetReplyDelay = new EnumPrefEntry<>(TmaPrefKey.ASSET_REPLY_DELAY_KEY,
+                TmaReplyDelay.values(), TmaReplyDelay.NONE);
+
+        mLoginEventOrder = new EnumPrefEntry<>(TmaPrefKey.LOGIN_EVENT_ORDER_KEY,
+                TmaLoginEventOrder.values(), TmaLoginEventOrder.PLAYBACK_STATE_UPDATE_FIRST);
     }
 
 
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/prefs/TmaPrefsFragment.java b/TestMediaApp/src/com/android/car/media/testmediaapp/prefs/TmaPrefsFragment.java
index 31cf4ae..066cc9c 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/prefs/TmaPrefsFragment.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/prefs/TmaPrefsFragment.java
@@ -26,7 +26,8 @@
 
 import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaAccountType;
 import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaBrowseNodeType;
-import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaNodeReplyDelay;
+import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaLoginEventOrder;
+import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaReplyDelay;
 import com.android.car.media.testmediaapp.prefs.TmaPrefs.PrefEntry;
 
 public class TmaPrefsFragment extends PreferenceFragmentCompat {
@@ -43,7 +44,11 @@
         screen.addPreference(createEnumPref(context, "Root node type", prefs.mRootNodeType,
                 TmaBrowseNodeType.values()));
         screen.addPreference(createEnumPref(context, "Root reply delay", prefs.mRootReplyDelay,
-                TmaNodeReplyDelay.values()));
+                TmaReplyDelay.values()));
+        screen.addPreference(createEnumPref(context, "Asset delay: random value in [v, 2v]",
+                prefs.mAssetReplyDelay, TmaReplyDelay.values()));
+        screen.addPreference(createEnumPref(context, "Login event order", prefs.mLoginEventOrder,
+                TmaLoginEventOrder.values()));
 
         setPreferenceScreen(screen);
     }
diff --git a/TestMediaApp/svgs/appIcon.svg b/TestMediaApp/svgs/appIcon.svg
new file mode 100644
index 0000000..fb81661
--- /dev/null
+++ b/TestMediaApp/svgs/appIcon.svg
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="128"
+   height="128"
+   viewBox="0 0 33.866666 33.866668"
+   version="1.1"
+   id="svg8"
+   inkscape:version="0.92.3 (2405546, 2018-03-11)"
+   sodipodi:docname="appIcon.svg">
+  <defs
+     id="defs2" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="10.15625"
+     inkscape:cx="63.901538"
+     inkscape:cy="64"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:window-width="2560"
+     inkscape:window-height="1526"
+     inkscape:window-x="0"
+     inkscape:window-y="26"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-263.13332)">
+    <rect
+       style="fill:#6bfc00;fill-opacity:1;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect815"
+       width="23.237743"
+       height="20.502357"
+       x="5.0278974"
+       y="275.89844" />
+    <ellipse
+       style="fill:#6bfc00;fill-opacity:1;stroke:#6bfc00;stroke-width:0.49525586;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path828"
+       ry="8.1301022"
+       rx="11.490721"
+       cy="275.677"
+       cx="16.633743" />
+    <ellipse
+       style="fill:#002afc;fill-opacity:1;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path832"
+       cx="11.38441"
+       cy="274.38748"
+       rx="1.0420513"
+       ry="1.0941538" />
+    <ellipse
+       style="fill:#002afc;fill-opacity:1;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path832-3"
+       cx="21.388103"
+       cy="274.46561"
+       rx="1.0420513"
+       ry="1.0941539" />
+  </g>
+</svg>
diff --git a/TestMediaApp/svgs/rectangle-horiz.svg b/TestMediaApp/svgs/rectangle-horiz.svg
new file mode 100644
index 0000000..606e62a
--- /dev/null
+++ b/TestMediaApp/svgs/rectangle-horiz.svg
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="80"
+   height="40"
+   viewBox="0 0 21.166667 10.583333"
+   version="1.1"
+   id="svg8"
+   inkscape:version="0.92.3 (2405546, 2018-03-11)"
+   sodipodi:docname="rectangle-horiz.svg">
+  <defs
+     id="defs2" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="11.2"
+     inkscape:cx="48.842507"
+     inkscape:cy="4.3665867"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:window-width="2560"
+     inkscape:window-height="1526"
+     inkscape:window-x="0"
+     inkscape:window-y="26"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-286.41668)">
+    <rect
+       id="rect3715"
+       width="21.166666"
+       height="10.583333"
+       x="0.023623543"
+       y="286.39304"
+       style="stroke-width:0.26458332" />
+    <path
+       style="stroke-width:0.08928572;fill:#00ffff;fill-opacity:1"
+       d="M 0.12822069,19.874485 V -0.0808725 H 40.083578 80.038935 V 19.874485 39.829842 H 40.083578 0.12822069 Z"
+       id="path3717"
+       inkscape:connector-curvature="0"
+       transform="matrix(0.26458333,0,0,0.26458333,0,286.41668)" />
+    <rect
+       style="fill:#00ffff;fill-opacity:1;stroke-width:0.26458332"
+       id="rect4524"
+       width="19.016928"
+       height="9.0241814"
+       x="0.49609375"
+       y="286.84189" />
+    <rect
+       style="fill:#ffffff;fill-opacity:1;stroke-width:0.26458332"
+       id="rect4528"
+       width="20.316219"
+       height="9.7801485"
+       x="0.49609375"
+       y="286.84189" />
+    <path
+       style="fill:none;fill-opacity:1;stroke:#001dff;stroke-width:0.265;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+       id="path4536"
+       sodipodi:type="arc"
+       sodipodi:cx="10.689639"
+       sodipodi:cy="291.74377"
+       sodipodi:rx="4.5999999"
+       sodipodi:ry="4.5999999"
+       sodipodi:start="0.47996554"
+       sodipodi:end="5.7107173"
+       d="m 14.769889,293.86782 a 4.5999999,4.5999999 0 0 1 -5.0699926,2.36822 4.5999999,4.5999999 0 0 1 -3.605338,-4.27959 4.5999999,4.5999999 0 0 1 3.1946166,-4.59431 4.5999999,4.5999999 0 0 1 5.26707,1.88978 l -3.866606,2.49185 z" />
+  </g>
+</svg>
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..df49ba3
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+    repositories {
+        google()
+        jcenter()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.5.0'
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..ce751bb
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Sep 26 14:52:51 PDT 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/read-me.txt b/read-me.txt
deleted file mode 100644
index a83c3e5..0000000
--- a/read-me.txt
+++ /dev/null
@@ -1 +0,0 @@
-This repository is only for test applications.
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..f3b7ee8
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,49 @@
+# Car test apps
+
+This repository is only for car test applications.
+
+## Building
+
+If you are not contributing to the repo, you can clone the repo via `git clone sso://googleplex-android/platform/packages/apps/Car/tests --branch pi-car-dev --single-branch`. Otherwise, see [workstation setup](#workstation-setup).
+
+Install [Android Studio](go/install-android-studio). Then import the `tests` Gradle project into Android Studio.
+
+### TestMediaApp
+
+TestMediaApp should be one of the run configurations. The green Run button should build and install the app on your phone.
+
+To see TestMediaApp in Android Auto Projected:
+
+1. Open Android Auto on phone
+2. Click hamburger icon at top left -> Settings
+3. Scroll to Version at bottom and tap ~10 times to unlock Developer Mode
+4. Click kebab icon at top right -> Developer settings
+5. Scroll to bottom and enable "Unknown sources"
+6. Exit and re-open Android Auto
+7. TestMediaApp should now be visible (click headphones icon in phone app to see app picker)
+
+## Contributing
+
+### Workstation setup
+
+Install [repo](https://source.android.com/setup/build/downloading#installing-repo) command line tool. Then run:
+
+```
+sudo apt-get install gitk
+sudo apt-get install git-gui
+mkdir WORKING_DIRECTORY_FOR_GIT_REPO
+cd WORKING_DIRECTORY_FOR_GIT_REPO
+repo init -u persistent-https://googleplex-android.git.corp.google.com/platform/manifest -b pi-car-dev -g name:platform/tools/repohooks,name:platform/packages/apps/Car/tests --depth=1
+repo sync
+```
+
+### Making a change
+
+```
+repo start BRANCH_NAME .
+# Make some changes
+git gui &
+# Use GUI to create a CL. Check amend box to update a work-in-progress CL
+repo upload .
+```
+
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..38f6519
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,17 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+include ':TestMediaApp'