diff options
author | Xin Li <delphij@google.com> | 2021-10-06 22:53:53 +0000 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2021-10-06 22:53:53 +0000 |
commit | 1b77f38f28a94abb246057ad18c028dcdf41d7bc (patch) | |
tree | ad8d4aa72f7f5c24180b32816d7e2e6239977dd3 | |
parent | a7a29bbf7275988f6623550567ed423db1aa855b (diff) | |
parent | 8ba04da2d561ce87c17fb0332401c12f89478b43 (diff) | |
download | tests-1b77f38f28a94abb246057ad18c028dcdf41d7bc.tar.gz |
Merge Android 12android-s-v2-preview-2android-s-v2-preview-1android-s-v2-beta-2android-s-v2-preview-1
Bug: 202323961
Merged-In: Id7a702c6248d86a5018d7a096233bef9f9594fae
Change-Id: Ib069dde5e69fb76fe28ee71909e2deb83cd632e3
21 files changed, 676 insertions, 24 deletions
@@ -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() |