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'