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'