aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2021-10-06 22:53:53 +0000
committerXin Li <delphij@google.com>2021-10-06 22:53:53 +0000
commit1b77f38f28a94abb246057ad18c028dcdf41d7bc (patch)
treead8d4aa72f7f5c24180b32816d7e2e6239977dd3
parenta7a29bbf7275988f6623550567ed423db1aa855b (diff)
parent8ba04da2d561ce87c17fb0332401c12f89478b43 (diff)
downloadtests-1b77f38f28a94abb246057ad18c028dcdf41d7bc.tar.gz
Bug: 202323961 Merged-In: Id7a702c6248d86a5018d7a096233bef9f9594fae Change-Id: Ib069dde5e69fb76fe28ee71909e2deb83cd632e3
-rw-r--r--.gitignore2
-rw-r--r--RotaryIME/AndroidManifest.xml1
-rw-r--r--RotaryPlayground/AndroidManifest.xml4
-rw-r--r--RotaryPlayground/res/drawable/button_background.xml21
-rw-r--r--RotaryPlayground/res/drawable/custom_button_background.xml22
-rw-r--r--RotaryPlayground/res/drawable/ic_play_arrow.xml30
-rw-r--r--RotaryPlayground/res/drawable/ic_play_arrow_off.xml35
-rw-r--r--RotaryPlayground/res/drawable/ic_stop.xml28
-rw-r--r--RotaryPlayground/res/layout/custom_focus_areas_fragment.xml3
-rw-r--r--RotaryPlayground/res/layout/rotary_cards.xml85
-rw-r--r--RotaryPlayground/res/values/attrs.xml21
-rw-r--r--RotaryPlayground/src/com/android/car/rotaryplayground/CustomButton.java65
-rw-r--r--RotaryPlayground/src/com/android/car/rotaryplayground/HeadsUpNotificationFragment.java3
-rw-r--r--RotaryPlayground/src/com/android/car/rotaryplayground/RotaryCards.java61
-rw-r--r--TestMediaApp/src/com/android/car/media/testmediaapp/TmaBrowser.java13
-rw-r--r--TestMediaApp/src/com/android/car/media/testmediaapp/TmaForegroundService.java2
-rw-r--r--TestMediaApp/src/com/android/car/media/testmediaapp/TmaPlayer.java3
-rw-r--r--tools/rro/README18
-rwxr-xr-xtools/rro/generate-overlayable.py79
-rw-r--r--tools/rro/resource_utils.py151
-rwxr-xr-xtools/rro/verify-overlayable.py53
21 files changed, 676 insertions, 24 deletions
diff --git a/.gitignore b/.gitignore
index bd59d21..3e2e239 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,4 @@
/.idea
.DS_Store
build/
-
+*.pyc
diff --git a/RotaryIME/AndroidManifest.xml b/RotaryIME/AndroidManifest.xml
index 420175a..95e9320 100644
--- a/RotaryIME/AndroidManifest.xml
+++ b/RotaryIME/AndroidManifest.xml
@@ -21,6 +21,7 @@
<service
android:name=".RotaryIme"
android:label="@string/ime_name"
+ android:exported="true"
android:permission="android.permission.BIND_INPUT_METHOD">
<intent-filter>
<action android:name="android.view.InputMethod" />
diff --git a/RotaryPlayground/AndroidManifest.xml b/RotaryPlayground/AndroidManifest.xml
index be6d847..8877af4 100644
--- a/RotaryPlayground/AndroidManifest.xml
+++ b/RotaryPlayground/AndroidManifest.xml
@@ -23,8 +23,8 @@
<activity
android:name=".RotaryActivity"
android:label="@string/app_name"
- android:allowEmbedded="true"
- android:exported="true">
+ android:exported="true"
+ android:allowEmbedded="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
diff --git a/RotaryPlayground/res/drawable/button_background.xml b/RotaryPlayground/res/drawable/button_background.xml
new file mode 100644
index 0000000..25566c5
--- /dev/null
+++ b/RotaryPlayground/res/drawable/button_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false" android:drawable="@drawable/ic_play_arrow_off" />
+ <item android:drawable="@drawable/ic_play_arrow" />
+</selector>
diff --git a/RotaryPlayground/res/drawable/custom_button_background.xml b/RotaryPlayground/res/drawable/custom_button_background.xml
new file mode 100644
index 0000000..d4716fa
--- /dev/null
+++ b/RotaryPlayground/res/drawable/custom_button_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item app:state_rotary_enabled="true" android:drawable="@drawable/ic_play_arrow" />
+ <item android:drawable="@drawable/ic_play_arrow_off" />
+</selector>
diff --git a/RotaryPlayground/res/drawable/ic_play_arrow.xml b/RotaryPlayground/res/drawable/ic_play_arrow.xml
new file mode 100644
index 0000000..5c0c252
--- /dev/null
+++ b/RotaryPlayground/res/drawable/ic_play_arrow.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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:viewportWidth="48"
+ android:viewportHeight="48">
+
+ <path
+ android:pathData="M-838-2232H562v3600H-838z" />
+ <path
+ android:fillColor="#000000"
+ android:pathData="M16 10v28l22-14z" />
+ <path
+ android:pathData="M0 0h48v48H0z" />
+</vector>
diff --git a/RotaryPlayground/res/drawable/ic_play_arrow_off.xml b/RotaryPlayground/res/drawable/ic_play_arrow_off.xml
new file mode 100644
index 0000000..8ad935f
--- /dev/null
+++ b/RotaryPlayground/res/drawable/ic_play_arrow_off.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <group
+ android:translateX="3.000000"
+ android:translateY="3.000000">
+ <path
+ android:fillColor="#000000"
+ android:strokeWidth="1"
+ android:pathData="M16,9 L7.249,3.431 L14.048,10.242 L16,9 Z" />
+ <path
+ android:fillColor="#000000"
+ android:strokeWidth="1"
+ android:pathData="M18,16.73 L1.27,0 L0,1.27 L5,6.27 L5,16 L10.946,12.216 L16.73,18 L18,16.73 Z" />
+ </group>
+</vector>
diff --git a/RotaryPlayground/res/drawable/ic_stop.xml b/RotaryPlayground/res/drawable/ic_stop.xml
new file mode 100644
index 0000000..fc42b32
--- /dev/null
+++ b/RotaryPlayground/res/drawable/ic_stop.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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:viewportWidth="48"
+ android:viewportHeight="48">
+
+ <path
+ android:pathData="M0 0h48v48H0z" />
+ <path
+ android:fillColor="#000000"
+ android:pathData="M12 12h24v24H12z" />
+</vector>
diff --git a/RotaryPlayground/res/layout/custom_focus_areas_fragment.xml b/RotaryPlayground/res/layout/custom_focus_areas_fragment.xml
index 2750aab..7ae06ea 100644
--- a/RotaryPlayground/res/layout/custom_focus_areas_fragment.xml
+++ b/RotaryPlayground/res/layout/custom_focus_areas_fragment.xml
@@ -167,8 +167,7 @@
android:gravity="center"
android:orientation="vertical"
android:background="@color/card_background_color"
- app:nudgeShortcut="@+id/nudge_shortcut"
- app:nudgeShortcutDirection="up">
+ app:nudgeUpShortcut="@+id/nudge_shortcut">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
diff --git a/RotaryPlayground/res/layout/rotary_cards.xml b/RotaryPlayground/res/layout/rotary_cards.xml
index e3d9e87..2ffdfdc 100644
--- a/RotaryPlayground/res/layout/rotary_cards.xml
+++ b/RotaryPlayground/res/layout/rotary_cards.xml
@@ -149,7 +149,8 @@
android:layout_width="@dimen/card_width"
android:layout_height="match_parent"
android:padding="@dimen/card_padding"
- android:orientation="vertical">
+ android:orientation="vertical"
+ app:defaultFocus="@+id/default_focus">
<TextView
android:layout_height="@dimen/description_height"
android:layout_width="match_parent"
@@ -160,8 +161,8 @@
android:onClick="onRotaryButtonClick"
android:tag="test_button"
android:text="Button" />
- <!-- TODO(b/154180719): Make this button the default focus in this FocusArea -->
<Button
+ android:id="@+id/default_focus"
android:layout_width="match_parent"
android:layout_height="50dp"
android:onClick="onRotaryButtonClick"
@@ -181,18 +182,9 @@
android:text="Button" />
</com.android.car.ui.FocusArea>
- <!-- A FocusArea with buttons in a circle. The default focus should land on A.
+ <!-- A FocusArea with buttons in a circle.
Rotating clockwise moves the focus from A -> B -> C -> D -> E -> F -> G -> H,
- and reverse counterclockwise.
- Adding app:defaultFocus to A to make it the default focus on this card
- Adding android:nextFocusForward is necessary to ensure the expected focus
- order, without it, the focus will move from
- G -> H -> F -> A -> E -> B -> D -> C.
- Lastly, android:nextFocusForward is not added to H -> A, to avoid linking
- the nodes in a circle. app:wrapAround="true" should be used instead.
- -->
- <!-- TODO(agathaman): add app:wrapAround to this card when b/155698037 is fixed -->
- <!-- TODO(agathaman): add app:defaultFocus to this card when b/155698037 is fixed -->
+ and reverse counterclockwise. -->
<com.android.car.ui.FocusArea
android:id="@+id/card_that_wraps_around"
android:background="@color/card_background_color"
@@ -200,7 +192,8 @@
android:layout_width="@dimen/card_width"
android:layout_height="match_parent"
android:padding="@dimen/card_padding"
- android:orientation="vertical">
+ android:orientation="vertical"
+ app:wrapAround="true">
<TextView
android:layout_height="@dimen/description_height"
android:layout_width="match_parent"
@@ -305,6 +298,68 @@
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.android.car.ui.FocusArea>
+ <!-- A FocusArea to demonstrate how to update a view properly.
+ Don't remove the focused view, otherwise Android framework will focus on another
+ view (the default focus view, or the first focusable view in the view tree), and
+ the user will see the focus highlight jump away unexpectedly. Some workarounds:
+ 1. Don't remove the view. You may achieve the desired behavior by updating the
+ source image of focused view.
+ 2. Delegate the view focus to its container, therefore removing the view won't
+ affect the focus
+ 3. Make another view request focus explicitly after Android framework adjusts the
+ focus.
+ Don't disable the focused view, otherwise Android framework will focus on another
+ view. A workaround is to use a custom state to replace android:state_disabled so
+ that it appears disabled but is not disabled actually.
+ -->
+ <com.android.car.ui.FocusArea
+ android:id="@+id/focus_area5"
+ android:background="@color/card_background_color"
+ android:layout_margin="16dp"
+ android:layout_width="@dimen/card_width"
+ android:layout_height="match_parent"
+ android:padding="@dimen/card_padding"
+ android:orientation="vertical">
+ <Button
+ android:id="@+id/button_5a"
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:text="focus jumps" />
+ <Button
+ android:id="@+id/button_5b"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:background="@drawable/ic_play_arrow"/>
+ <FrameLayout
+ android:id="@+id/button_5c_container"
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:focusable="true">
+ <Button
+ android:id="@+id/button_5c"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:text="focus stays" />
+ </FrameLayout>
+ <Button
+ android:id="@+id/button_5d"
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:text="focus returns" />
+ <Button
+ android:id="@+id/button_5e"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:background="@drawable/button_background"/>
+ <com.android.car.rotaryplayground.CustomButton
+ android:id="@+id/button_5f"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:background="@drawable/custom_button_background"/>
+ </com.android.car.ui.FocusArea>
</LinearLayout>
</HorizontalScrollView>
-</LinearLayout> \ No newline at end of file
+</LinearLayout>
diff --git a/RotaryPlayground/res/values/attrs.xml b/RotaryPlayground/res/values/attrs.xml
new file mode 100644
index 0000000..6b5aa4f
--- /dev/null
+++ b/RotaryPlayground/res/values/attrs.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <declare-styleable name="CustomButton">
+ <attr name="state_rotary_enabled" format="boolean" />
+ </declare-styleable>
+</resources>
diff --git a/RotaryPlayground/src/com/android/car/rotaryplayground/CustomButton.java b/RotaryPlayground/src/com/android/car/rotaryplayground/CustomButton.java
new file mode 100644
index 0000000..924908f
--- /dev/null
+++ b/RotaryPlayground/src/com/android/car/rotaryplayground/CustomButton.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.rotaryplayground;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Button;
+
+import androidx.annotation.Nullable;
+
+public class CustomButton extends Button {
+
+ private static final int[] STATE_ROTARY_ENABLED = {R.attr.state_rotary_enabled};
+
+ private boolean mRotaryEnabled = true;
+
+ public CustomButton(Context context) {
+ super(context);
+ }
+
+ public CustomButton(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CustomButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public CustomButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public void setRotaryEnabled(boolean enabled) {
+ mRotaryEnabled = enabled;
+ refreshDrawableState();
+ }
+
+ public boolean isRotaryEnabled() {
+ return mRotaryEnabled;
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ if (mRotaryEnabled) {
+ mergeDrawableStates(drawableState, STATE_ROTARY_ENABLED);
+ }
+ return drawableState;
+ }
+}
diff --git a/RotaryPlayground/src/com/android/car/rotaryplayground/HeadsUpNotificationFragment.java b/RotaryPlayground/src/com/android/car/rotaryplayground/HeadsUpNotificationFragment.java
index 0744ac0..97c676e 100644
--- a/RotaryPlayground/src/com/android/car/rotaryplayground/HeadsUpNotificationFragment.java
+++ b/RotaryPlayground/src/com/android/car/rotaryplayground/HeadsUpNotificationFragment.java
@@ -62,7 +62,8 @@ public class HeadsUpNotificationFragment extends Fragment {
*/
private Notification createNotification() {
Intent intent = new Intent(getContext(), RotaryActivity.class);
- PendingIntent pendingIntent = PendingIntent.getActivity(getContext(), 0, intent, 0);
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE);
return new Notification.Builder(getContext(), NOTIFICATION_CHANNEL_ID)
.setContentTitle("Example heads-up notification")
.setContentText("Try nudging up to HUN")
diff --git a/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryCards.java b/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryCards.java
index 28f34a1..730c7c5 100644
--- a/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryCards.java
+++ b/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryCards.java
@@ -16,13 +16,18 @@
package com.android.car.rotaryplayground;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.Button;
+
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
+import com.android.car.ui.FocusArea;
+
/** Fragment to demo a layout with cards that are FocusArea containers. */
public class RotaryCards extends Fragment {
@@ -30,6 +35,62 @@ public class RotaryCards extends Fragment {
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.rotary_cards, container, false);
+
+ // This button will be removed and immediately added back after click. So the focus
+ // highlight will jump to another view after rotary click.
+ FocusArea focusArea5 = view.findViewById(R.id.focus_area5);
+ Button button5a = view.findViewById(R.id.button_5a);
+ button5a.setOnClickListener(v -> {
+ int index = focusArea5.indexOfChild(button5a);
+ focusArea5.removeView(button5a);
+ focusArea5.addView(button5a, index);
+ });
+
+ // The background of this button will be changed after click. The focus highlight will stay
+ // on this Button after click.
+ Button button5b = view.findViewById(R.id.button_5b);
+ boolean[] stopped = {false};
+ button5b.setOnClickListener(v -> {
+ stopped[0] = !stopped[0];
+ Drawable drawable = view.getContext().getDrawable(
+ stopped[0] ? R.drawable.ic_stop : R.drawable.ic_play_arrow);
+ button5b.setBackground(drawable);
+ });
+
+ // This button will be removed and immediately added back after click. It's not focusable
+ // but its container is focusable. The focus highlight will stay on the container after
+ // click.
+ ViewGroup button5cContainer = view.findViewById(R.id.button_5c_container);
+ Button button5c = view.findViewById(R.id.button_5c);
+ button5c.setFocusable(false);
+ button5cContainer.setOnClickListener(v -> {
+ button5cContainer.removeView(button5c);
+ button5cContainer.addView(button5c, 0);
+ });
+
+ // This button will be removed then added back and request focus explicitly after click.
+ // So the focus highlight will jump to another view then jump back after rotary click.
+ Button button5d = view.findViewById(R.id.button_5d);
+ button5d.setOnClickListener(v -> {
+ boolean needRestoreFocus = button5d.isFocused();
+ int index = focusArea5.indexOfChild(button5d);
+ focusArea5.removeView(button5d);
+ focusArea5.addView(button5d, index);
+ if (needRestoreFocus) {
+ button5d.requestFocus();
+ }
+ });
+
+ // This button will be disabled after click. So the focus highlight will jump to another
+ // view after rotary click.
+ Button button5e = view.findViewById(R.id.button_5e);
+ button5e.setOnClickListener(v -> button5e.setEnabled(!button5e.isEnabled()));
+
+ // This button will appear disabled but is not disabled after click. So the focus
+ // highlight will stay after rotary click.
+ CustomButton button5f = view.findViewById(R.id.button_5f);
+ button5f.setOnClickListener(v -> button5f.setRotaryEnabled(!button5f.isRotaryEnabled()));
+
return view;
}
}
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaBrowser.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaBrowser.java
index c187448..87161fd 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaBrowser.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaBrowser.java
@@ -19,7 +19,10 @@ import static com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaBrowseNod
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.app.PendingIntent;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.Handler;
@@ -31,6 +34,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.media.MediaBrowserServiceCompat;
+import androidx.media.session.MediaButtonReceiver;
import com.android.car.media.testmediaapp.loader.TmaLoader;
import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaAccountType;
@@ -83,7 +87,14 @@ public class TmaBrowser extends MediaBrowserServiceCompat {
super.onCreate();
mPrefs = TmaPrefs.getInstance(this);
mHandler = new Handler();
- mSession = new MediaSessionCompat(this, MEDIA_SESSION_TAG);
+
+ ComponentName mbrComponent = MediaButtonReceiver.getMediaButtonReceiverComponent(this);
+ Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+ mediaButtonIntent.setComponent(mbrComponent);
+ PendingIntent mbrIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent,
+ PendingIntent.FLAG_IMMUTABLE);
+
+ mSession = new MediaSessionCompat(this, MEDIA_SESSION_TAG, mbrComponent, mbrIntent);
setSessionToken(mSession.getSessionToken());
mLibrary = new TmaLibrary(new TmaLoader(this));
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaForegroundService.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaForegroundService.java
index 6241455..594cbab 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaForegroundService.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaForegroundService.java
@@ -58,7 +58,7 @@ public class TmaForegroundService extends Service {
createNotificationChannel();
Intent notificationIntent = new Intent(this, TmaPrefsActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,
- 0, notificationIntent, 0);
+ 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Foreground Service")
.setSmallIcon(R.drawable.ic_app_icon)
diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaPlayer.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaPlayer.java
index 43cf3af..e75d028 100644
--- a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaPlayer.java
+++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaPlayer.java
@@ -103,7 +103,8 @@ public class TmaPlayer extends MediaSessionCompat.Callback {
Intent prefsIntent = new Intent();
prefsIntent.setClass(mBrowser, TmaPrefsActivity.class);
prefsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- PendingIntent pendingIntent = PendingIntent.getActivity(mBrowser, 0, prefsIntent, 0);
+ PendingIntent pendingIntent = PendingIntent.getActivity(mBrowser, 0, prefsIntent,
+ PendingIntent.FLAG_IMMUTABLE);
Bundle extras = new Bundle();
extras.putString(MediaKeys.ERROR_RESOLUTION_ACTION_LABEL, event.mActionLabel);
diff --git a/tools/rro/README b/tools/rro/README
new file mode 100644
index 0000000..dcc91d7
--- /dev/null
+++ b/tools/rro/README
@@ -0,0 +1,18 @@
+These scripts are used to generate and verify overlayable.xml files.
+
+Sample invocations (Media Center as an example).
+
+To generate:
+PROJECT_TOP=$ANDROID_BUILD_TOP/packages/apps/Car/Media
+python $ANDROID_BUILD_TOP/packages/apps/Car/tests/tools/rro/generate-overlayable.py \
+ -n CarMediaApp \
+ -r $PROJECT_TOP/res \
+ -e $PROJECT_TOP/res/values/overlayable.xml $PROJECT_TOP/res/xml/automotive_app_desc.xml \
+ -o $PROJECT_TOP/res/values/overlayable.xml
+
+To verify:
+PROJECT_TOP=$ANDROID_BUILD_TOP/packages/apps/Car/Media
+python $ANDROID_BUILD_TOP/packages/apps/Car/tests/tools/rro/verify-overlayable.py \
+ -r $PROJECT_TOP/res \
+ -e $PROJECT_TOP/res/values/overlayable.xml $PROJECT_TOP/res/xml/automotive_app_desc.xml \
+ -o $PROJECT_TOP/res/values/overlayable.xml \ No newline at end of file
diff --git a/tools/rro/generate-overlayable.py b/tools/rro/generate-overlayable.py
new file mode 100755
index 0000000..1dcb107
--- /dev/null
+++ b/tools/rro/generate-overlayable.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+# Copyright (C) 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import sys
+from resource_utils import get_all_resources, Resource
+from datetime import datetime
+import lxml.etree as etree
+if sys.version_info[0] != 3:
+ print("Must use python 3")
+ sys.exit(1)
+
+COPYRIGHT_STR = """ Copyright (C) %s The Android Open Source Project
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.""" % (datetime.today().strftime("%Y"))
+
+AUTOGENERATION_NOTICE_STR = """
+THIS FILE WAS AUTO GENERATED, DO NOT EDIT MANUALLY.
+REGENERATE USING packages/apps/Car/tests/tools/rro/generate-overlayable.py
+"""
+
+"""
+Script used to update the 'overlayable.xml' file.
+"""
+def main():
+ parser = argparse.ArgumentParser(description='Generate overlayable.xml.')
+ optional_args = parser.add_argument_group('optional arguments')
+ optional_args.add_argument('-t', '--policyType', default='system|product|signature', help='Policy type for the overlay - delimited by |')
+ optional_args.add_argument('-e', '--excludeFiles', nargs='*', help='File paths (absolute or relative to cwd) that should be excluded when generating overlayable.xml')
+ optional_args.add_argument('-o', '--outputFile', default='', help='Output file path (absolute or relative to cwd). If empty, output to stdout')
+ required_args = parser.add_argument_group('required arguments')
+ required_args.add_argument('-n', '--targetName', help='Overlayable name for the overlay.', required=True)
+ required_args.add_argument('-r', '--resourcePath', help='Path to resource directory (absolute or relative to cwd)', required=True)
+ args = parser.parse_args()
+
+ resources = get_all_resources(args.resourcePath, args.excludeFiles)
+ generate_overlayable_file(resources, args.targetName, args.policyType, args.outputFile)
+
+def generate_overlayable_file(resources, target_name, policy_type, output_file):
+ resources = sorted(resources, key=lambda x: x.type + x.name)
+ root = etree.Element('resources')
+ root.addprevious(etree.Comment(COPYRIGHT_STR))
+ root.addprevious(etree.Comment(AUTOGENERATION_NOTICE_STR))
+ overlayable = etree.SubElement(root, 'overlayable')
+ overlayable.set('name', target_name)
+ policy = etree.SubElement(overlayable, 'policy')
+ policy.set('type', policy_type)
+ for resource in resources:
+ item = etree.SubElement(policy, 'item')
+ item.set('type', resource.type)
+ item.set('name', resource.name)
+ data = etree.ElementTree(root)
+ if not output_file:
+ print(etree.tostring(data, pretty_print=True, xml_declaration=True).decode())
+ else:
+ with open(output_file, 'wb') as f:
+ data.write(f, pretty_print=True, xml_declaration=True, encoding='utf-8')
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/rro/resource_utils.py b/tools/rro/resource_utils.py
new file mode 100644
index 0000000..8628abf
--- /dev/null
+++ b/tools/rro/resource_utils.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python3
+# Copyright (C) 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import re
+import lxml.etree as etree
+
+class ResourceLocation:
+ def __init__(self, file, line=None):
+ self.file = file
+ self.line = line
+ def __str__(self):
+ if self.line is not None:
+ return self.file + ':' + str(self.line)
+ else:
+ return self.file
+
+class Resource:
+ def __init__(self, name, type, location=None):
+ self.name = name
+ self.type = type
+ self.locations = []
+ if location is not None:
+ self.locations.append(location)
+ def __eq__(self, other):
+ if isinstance(other, _Grab):
+ return other == self
+ return self.name == other.name and self.type == other.type
+ def __ne__(self, other):
+ if isinstance(other, _Grab):
+ return other != self
+ return self.name != other.name or self.type != other.type
+ def __hash__(self):
+ return hash((self.name, self.type))
+ def __str__(self):
+ result = ''
+ for location in self.locations:
+ result += str(location) + ': '
+ result += '<'+self.type+' name="'+self.name+'"'
+ return result + '>'
+ def __repr__(self):
+ return str(self)
+
+def get_all_resources(resDir, excluded_resource_files=[]):
+ excluded_resource_files = [os.path.abspath(file) for file in excluded_resource_files]
+ allResDirs = [f for f in os.listdir(resDir) if os.path.isdir(os.path.join(resDir, f))]
+ valuesDirs = [f for f in allResDirs if f.startswith('values')]
+ fileDirs = [f for f in allResDirs if not f.startswith('values')]
+ resources = set()
+ # Get the filenames of the all the files in all the fileDirs
+ for dir in fileDirs:
+ type = dir.split('-')[0]
+ for file in os.listdir(os.path.join(resDir, dir)):
+ filePath = os.path.abspath(os.path.join(resDir, dir, file))
+ if file.endswith('.xml') and filePath not in excluded_resource_files:
+ add_resource_to_set(resources,
+ Resource(file[:-4], type,
+ ResourceLocation(os.path.join(resDir, dir, file))))
+ if dir.startswith("layout"):
+ for resource in get_ids_from_layout_file(os.path.join(resDir, dir, file)):
+ add_resource_to_set(resources, resource)
+ for dir in valuesDirs:
+ for file in os.listdir(os.path.join(resDir, dir)):
+ filePath = os.path.abspath(os.path.join(resDir, dir, file))
+ if file.endswith('.xml') and filePath not in excluded_resource_files:
+ for resource in get_resources_from_single_file(os.path.join(resDir, dir, file),
+ dir != "values"):
+ add_resource_to_set(resources, resource)
+ return resources
+
+def get_ids_from_layout_file(filename):
+ result = set()
+ with open(filename, 'r') as file:
+ r = re.compile("@\+id/([a-zA-Z0-9_]+)")
+ for i in r.findall(file.read()):
+ add_resource_to_set(result, Resource(i, 'id', ResourceLocation(filename)))
+ return result
+
+def get_resources_from_single_file(filename, ignore_strings=False):
+ doc = etree.parse(filename)
+ root = doc.getroot()
+ result = set()
+ for resource in root:
+ if resource.tag is etree.Comment:
+ continue
+ if resource.tag == 'declare-styleable':
+ for attr in resource:
+ resName = attr.get('name')
+ # Skip resources beginning with 'android:' as they are part of the framework
+ # resources. This script finds only the app's resources.
+ if resName is None or resName.startswith('android:'):
+ continue
+ resType = "attr"
+ add_resource_to_set(result, Resource(resName, resType, ResourceLocation(filename, attr.sourceline)))
+ continue
+ resName = resource.get('name')
+ resType = resource.tag
+ if resType == "string-array" or resType == "integer-array":
+ resType = "array"
+ if resource.tag == 'item' or resource.tag == 'public':
+ resType = resource.get('type')
+ if resType == 'string' and ignore_strings:
+ continue
+ if resType == 'overlayable':
+ for policy in resource:
+ for overlayable in policy:
+ resName = overlayable.get('name')
+ resType = overlayable.get('type')
+ add_resource_to_set(result, Resource(resName, resType,
+ ResourceLocation(filename, resource.sourceline)))
+ else:
+ add_resource_to_set(result, Resource(resName, resType,
+ ResourceLocation(filename, resource.sourceline)))
+ return result
+
+# Used to get objects out of sets
+class _Grab:
+ def __init__(self, value):
+ self.search_value = value
+ def __hash__(self):
+ return hash(self.search_value)
+ def __eq__(self, other):
+ if self.search_value == other:
+ self.actual_value = other
+ return True
+ return False
+
+def add_resource_to_set(resourceset, resource):
+ if (resource.name == None):
+ return
+ grabber = _Grab(resource)
+ if grabber in resourceset:
+ grabber.actual_value.locations.extend(resource.locations)
+ else:
+ resourceset.update([resource])
+
+def merge_resources(set1, set2):
+ for resource in set2:
+ add_resource_to_set(set1, resource)
diff --git a/tools/rro/verify-overlayable.py b/tools/rro/verify-overlayable.py
new file mode 100755
index 0000000..b469e3c
--- /dev/null
+++ b/tools/rro/verify-overlayable.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+# Copyright (C) 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import sys
+from resource_utils import get_all_resources, get_resources_from_single_file, Resource
+
+if sys.version_info[0] != 3:
+ print("Must use python 3")
+ sys.exit(1)
+
+"""
+Script used to verify the 'overlayable.xml' file.
+"""
+def main():
+ parser = argparse.ArgumentParser(description='Verify overlayable.xml.')
+ optional_args = parser.add_argument_group('optional arguments')
+ optional_args.add_argument('-e', '--excludeFiles', nargs='*', help='File paths (absolute or relative to cwd) that should be excluded when generating overlayable.xml')
+ required_args = parser.add_argument_group('required arguments')
+ required_args.add_argument('-r', '--resourcePath', help='Path to resource directory (absolute or relative to cwd)', required=True)
+ required_args.add_argument('-o', '--overlayableFilePath', help='Filepath to overlayable.xml (absolute or relative to cwd).', required=True)
+ args = parser.parse_args()
+
+ resources = get_all_resources(args.resourcePath, args.excludeFiles)
+ old_mapping = get_resources_from_single_file(args.overlayableFilePath)
+ compare_resources(old_mapping, resources, args.overlayableFilePath)
+
+def compare_resources(old_mapping, new_mapping, res_public_file):
+ removed = old_mapping.difference(new_mapping)
+ added = new_mapping.difference(old_mapping)
+ if len(removed) > 0:
+ print('Resources removed:\n' + '\n'.join(map(lambda x: str(x), removed)))
+ if len(added) > 0:
+ print('Resources added:\n' + '\n'.join(map(lambda x: str(x), added)))
+ if len(added) + len(removed) > 0:
+ print("Some resource have been modified. If this is intentional please " +
+ "run 'python generate-overlayable.py' again and submit the new %s" % res_public_file)
+ sys.exit(1)
+
+if __name__ == '__main__':
+ main()