Remove use of class hidden in master
am: 0a99cb8aa6

Change-Id: I882ec5e941a3213dc22a2b7c9589f796b46e2fae
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 859815b..09e850c 100644
--- a/TestMediaApp/AndroidManifest.xml
+++ b/TestMediaApp/AndroidManifest.xml
@@ -17,9 +17,10 @@
 <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"
@@ -65,6 +66,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/simple_leaves.json b/TestMediaApp/assets/media_items/simple_leaves.json
index d70a2a4..e666c1b 100644
--- a/TestMediaApp/assets/media_items/simple_leaves.json
+++ b/TestMediaApp/assets/media_items/simple_leaves.json
@@ -104,6 +104,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/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_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/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..8506b6b
--- /dev/null
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/MediaKeys.java
@@ -0,0 +1,64 @@
+/*
+ * 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 {
+
+    /**
+     * 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;
+}
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaAssetProvider.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaAssetProvider.java
index 15406a3..fc9fd49 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaAssetProvider.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaAssetProvider.java
@@ -24,6 +24,8 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.car.media.testmediaapp.prefs.TmaPrefs;
+
 import java.io.FileNotFoundException;
 import java.io.IOException;
 
@@ -45,10 +47,17 @@
         return prefix + localArt;
     }
 
+    private int mAssetDelay = 0;
+
     @Override
     public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
         Log.i(TAG, "TmaAssetProvider#openAssetFile " + uri);
 
+        try {
+            Thread.sleep(mAssetDelay + (int)(mAssetDelay * (Math.random())));
+        } catch (InterruptedException ignored) {
+        }
+
         String file_path = uri.getPath();
         if (TextUtils.isEmpty(file_path)) throw new FileNotFoundException();
         try {
@@ -64,7 +73,9 @@
 
     @Override
     public boolean onCreate() {
-        return false;
+        TmaPrefs.getInstance(getContext()).mAssetReplyDelay.registerChangeListener(
+                (oldValue, newValue) -> mAssetDelay = newValue.mReplyDelayMs);
+        return true;
     }
 
     @Override
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaBrowser.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaBrowser.java
index c795610..02e8292 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;
@@ -22,7 +26,6 @@
 import android.support.v4.media.MediaBrowserCompat.MediaItem;
 import android.support.v4.media.session.MediaSessionCompat;
 import android.support.v4.media.session.PlaybackStateCompat;
-import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -30,7 +33,7 @@
 
 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;
@@ -99,20 +102,32 @@
     }
 
     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) {
             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, null));
+                            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);
             mSession.setPlaybackState(playbackState.build());
         }
-        invalidateRoot();
     }
 
     private void invalidateRoot() {
@@ -129,6 +144,14 @@
     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
@@ -139,7 +162,7 @@
     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())) {
@@ -164,7 +187,7 @@
                 result.sendResult(items);
             }
         };
-        if (delay == TmaNodeReplyDelay.NONE) {
+        if (delay == TmaReplyDelay.NONE) {
             task.run();
         } else {
             result.detach();
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaLibrary.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaLibrary.java
index eb77019..27b8a7a 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaLibrary.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaLibrary.java
@@ -48,6 +48,7 @@
         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.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");
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaMediaEvent.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaMediaEvent.java
index f6ec8af..491f940 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaMediaEvent.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaMediaEvent.java
@@ -53,7 +53,7 @@
 
     public static final TmaMediaEvent INSTANT_PLAYBACK =
             new TmaMediaEvent(EventState.PLAYING, StateErrorCode.UNKNOWN_ERROR, null, null,
-                    ResolutionIntent.NONE, 0, null);
+                    ResolutionIntent.NONE, Action.NONE, 0, null);
 
     /** The name of each entry is the value used in the json file. */
     public enum EventState {
@@ -105,23 +105,31 @@
         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;
     }
@@ -152,6 +160,7 @@
                 ", 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 591f4cf..f79e273 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaMediaItem.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaMediaItem.java
@@ -22,11 +22,6 @@
 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.MediaDescriptionCompat;
@@ -46,8 +41,8 @@
     /** 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);
         final int mBundleValue;
         ContentStyle(int value) {
             mBundleValue = value;
@@ -123,7 +118,11 @@
         return mParent;
     }
 
+    @Nullable
     TmaMediaItem getPlayableByIndex(long index) {
+        if (index < 0 || index >= mPlayableChildren.size()) {
+            return null;
+        }
         return mPlayableChildren.get((int)index);
     }
 
@@ -208,8 +207,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 dc368ea..2938a37 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaPlayer.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaPlayer.java
@@ -28,9 +28,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 +41,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 +105,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 +143,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) {
@@ -232,6 +270,8 @@
                 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);
         }
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 c2e573a..2222afa 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/loader/TmaMediaEventReader.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/loader/TmaMediaEventReader.java
@@ -26,6 +26,7 @@
 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;
@@ -53,6 +54,7 @@
         ERROR_MESSAGE,
         ACTION_LABEL,
         INTENT,
+        ACTION,
         /** How long to wait before sending the event to the app. */
         POST_DELAY_MS,
         THROW_EXCEPTION
@@ -70,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
@@ -86,6 +90,7 @@
                 getString(json, Keys.ERROR_MESSAGE),
                 getString(json, Keys.ACTION_LABEL),
                 getEnum(json, Keys.INTENT, mResolutionIntents, ResolutionIntent.NONE),
+                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..5a4a217 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/loader/TmaMediaMetadataReader.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/loader/TmaMediaMetadataReader.java
@@ -59,6 +59,7 @@
 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) {
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..744ef01 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,6 +83,7 @@
     public enum TmaBrowseNodeType implements EnumPrefValue {
         NULL("Null (error)", "null"),
         EMPTY("Empty", "empty"),
+        QUEUE_ONLY("Queue only", "queue-only"),
         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");
@@ -102,6 +105,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/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'