Snap for 10453563 from 713d469f0193a991a8ce5acd0415692d34b9658d to mainline-conscrypt-release Change-Id: Ie95445ef13259281b628a0c21f858c702cfd78b2
diff --git a/car-broadcastradio-support/OWNERS b/car-broadcastradio-support/OWNERS index 01124ba..7d3fbc0 100644 --- a/car-broadcastradio-support/OWNERS +++ b/car-broadcastradio-support/OWNERS
@@ -1,3 +1,6 @@ # People who can approve changes for submission. # Automotive team -twasilczyk@google.com +xuweilin@google.com +oscarazu@google.com +ericjeong@google.com +keunyoung@google.com
diff --git a/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramInfoExt.java b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramInfoExt.java index ce3d014..d377dbd 100644 --- a/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramInfoExt.java +++ b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramInfoExt.java
@@ -21,6 +21,7 @@ import android.annotation.Nullable; import android.graphics.Bitmap; import android.hardware.radio.ProgramSelector; +import android.hardware.radio.RadioManager; import android.hardware.radio.RadioManager.ProgramInfo; import android.hardware.radio.RadioMetadata; import android.media.MediaMetadata; @@ -29,6 +30,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Objects; /** * Proposed extensions to android.hardware.radio.RadioManager.ProgramInfo. @@ -71,13 +73,23 @@ /** * Returns program name suitable to display. * - * If there is no program name, it falls back to channel name. Flags related to + * <p>If there is no program name, it falls back to channel name. Flags related to * the channel name display will be forwarded to the channel name generation method. + * + * @param info {@link ProgramInfo} to get name from + * @param flags Fallback method + * @param programNameOrder {@link RadioMetadata} metadata keys to pull from {@link ProgramInfo} + * for the program name */ - public static @NonNull String getProgramName(@NonNull ProgramInfo info, @NameFlag int flags) { + @NonNull + public static String getProgramName(@NonNull ProgramInfo info, @NameFlag int flags, + @NonNull String[] programNameOrder) { + Objects.requireNonNull(info, "info can not be null."); + Objects.requireNonNull(programNameOrder, "programNameOrder can not be null"); + RadioMetadata meta = info.getMetadata(); if (meta != null) { - for (String key : PROGRAM_NAME_ORDER) { + for (String key : programNameOrder) { String value = meta.getString(key); if (value != null) return value; } @@ -104,6 +116,17 @@ } /** + * Returns program name suitable to display. + * + * <p>If there is no program name, it falls back to channel name. Flags related to + * the channel name display will be forwarded to the channel name generation method. + */ + @NonNull + public static String getProgramName(@NonNull ProgramInfo info, @NameFlag int flags) { + return getProgramName(info, flags, PROGRAM_NAME_ORDER); + } + + /** * Proposed reimplementation of {@link RadioManager#ProgramInfo#getMetadata}. * * As opposed to the original implementation, it never returns null. @@ -118,12 +141,69 @@ } /** - * Converts {@ProgramInfo} to {@MediaMetadata}. + * Converts {@link ProgramInfo} to {@link MediaMetadata} for displaying. * - * This method is meant to be used for currently playing station in {@link MediaSession}. + * <p>This method is meant to be used for displaying the currently playing station in + * {@link MediaSession}, only a subset of keys populated in {@link ProgramInfo#toMediaMetadata} + * will be populated in this method. + * + * <ul> + * The following keys will be populated in the {@link MediaMetadata}: + * <li>{@link MediaMetadata#METADATA_KEY_DISPLAY_TITLE}</li> + * <li>{@link MediaMetadata#METADATA_KEY_DISPLAY_SUBTITLE}</li> + * <li>{@link MediaMetadata#METADATA_KEY_ALBUM_ART}</li> + * <li>{@link MediaMetadata#METADATA_KEY_USER_RATING}</li> + * <ul/> * * @param info {@link ProgramInfo} to convert - * @param isFavorite true, if a given program is a favorite + * @param isFavorite {@code true}, if a given program is a favorite + * @param imageResolver metadata images resolver/cache + * @param programNameOrder order of keys to look for program name in {@link ProgramInfo} + * @return {@link MediaMetadata} object + */ + @NonNull + public static MediaMetadata toMediaDisplayMetadata(@NonNull ProgramInfo info, + boolean isFavorite, @NonNull ImageResolver imageResolver, + @NonNull String[] programNameOrder) { + Objects.requireNonNull(info, "info can not be null."); + Objects.requireNonNull(imageResolver, "imageResolver can not be null."); + Objects.requireNonNull(programNameOrder, "programNameOrder can not be null."); + + MediaMetadata.Builder bld = new MediaMetadata.Builder(); + + ProgramSelector selector = + ProgramSelectorExt.createAmFmSelector(info.getLogicallyTunedTo().getValue()); + String displayTitle = ProgramSelectorExt.getDisplayName(selector, info.getChannel()); + bld.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, displayTitle); + String subtitle = getProgramName(info, /* flags= */ 0, programNameOrder); + bld.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle); + + Bitmap bm = resolveAlbumArtBitmap(info.getMetadata(), imageResolver); + if (bm != null) bld.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bm); + + bld.putRating(MediaMetadata.METADATA_KEY_USER_RATING, Rating.newHeartRating(isFavorite)); + + return bld.build(); + } + + /** + * Converts {@link ProgramInfo} to {@link MediaMetadata}. + * + * <p>This method is meant to be used for currently playing station in {@link MediaSession}. + * + * <ul> + * The following keys will be populated in the {@link MediaMetadata}: + * <li>{@link MediaMetadata#METADATA_KEY_DISPLAY_TITLE}</li> + * <li>{@link MediaMetadata#METADATA_KEY_TITLE}</li> + * <li>{@link MediaMetadata#METADATA_KEY_ARTIST}</li> + * <li>{@link MediaMetadata#METADATA_KEY_ALBUM}</li> + * <li>{@link MediaMetadata#METADATA_KEY_DISPLAY_SUBTITLE}</li> + * <li>{@link MediaMetadata#METADATA_KEY_ALBUM_ART}</li> + * <li>{@link MediaMetadata#METADATA_KEY_USER_RATING}</li> + * <ul/> + * + * @param info {@link ProgramInfo} to convert + * @param isFavorite {@code true}, if a given program is a favorite * @param imageResolver metadata images resolver/cache * @return {@link MediaMetadata} object */ @@ -158,16 +238,22 @@ } bld.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle); } - long albumArtId = RadioMetadataExt.getGlobalBitmapId(meta, - RadioMetadata.METADATA_KEY_ART); - if (albumArtId != 0 && imageResolver != null) { - Bitmap bm = imageResolver.resolve(albumArtId); - if (bm != null) bld.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bm); - } + + Bitmap bm = resolveAlbumArtBitmap(meta, imageResolver); + if (bm != null) bld.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bm); } bld.putRating(MediaMetadata.METADATA_KEY_USER_RATING, Rating.newHeartRating(isFavorite)); return bld.build(); } + + private static Bitmap resolveAlbumArtBitmap(@NonNull RadioMetadata meta, + @NonNull ImageResolver imageResolver) { + long albumArtId = RadioMetadataExt.getGlobalBitmapId(meta, RadioMetadata.METADATA_KEY_ART); + if (albumArtId != 0 && imageResolver != null) { + return imageResolver.resolve(albumArtId); + } + return null; + } }
diff --git a/car-qc-lib/res/color/qc_seekbar_thumb_selector.xml b/car-qc-lib/res/color/qc_seekbar_thumb_selector.xml new file mode 100644 index 0000000..bf94a61 --- /dev/null +++ b/car-qc-lib/res/color/qc_seekbar_thumb_selector.xml
@@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2023 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:color="@color/qc_seekbar_thumb_disabled_on_dark"/> + <item android:color="@color/qc_seekbar_thumb"/> +</selector>
diff --git a/car-qc-lib/res/color/qc_switch_thumb_selector.xml b/car-qc-lib/res/color/qc_switch_thumb_selector.xml new file mode 100644 index 0000000..e0bfc22 --- /dev/null +++ b/car-qc-lib/res/color/qc_switch_thumb_selector.xml
@@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2023 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:color="@color/qc_switch_thumb_color_disabled_on_dark"/> + <item android:color="@color/qc_switch_thumb_color"/> +</selector>
diff --git a/car-qc-lib/res/drawable/qc_toggle_background.xml b/car-qc-lib/res/drawable/qc_toggle_background.xml index 3688175..4e30cf4 100644 --- a/car-qc-lib/res/drawable/qc_toggle_background.xml +++ b/car-qc-lib/res/drawable/qc_toggle_background.xml
@@ -16,8 +16,8 @@ --> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@android:id/background" - android:width="@dimen/qc_toggle_size" - android:height="@dimen/qc_toggle_size" + android:width="@dimen/qc_toggle_background_size" + android:height="@dimen/qc_toggle_background_size" android:start="@dimen/qc_toggle_background_padding" android:top="@dimen/qc_toggle_background_padding" android:drawable="@drawable/qc_toggle_button_background">
diff --git a/car-qc-lib/res/drawable/qc_toggle_unavailable_background.xml b/car-qc-lib/res/drawable/qc_toggle_unavailable_background.xml index e9f2e12..185b801 100644 --- a/car-qc-lib/res/drawable/qc_toggle_unavailable_background.xml +++ b/car-qc-lib/res/drawable/qc_toggle_unavailable_background.xml
@@ -16,8 +16,8 @@ --> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@android:id/background" - android:width="@dimen/qc_toggle_size" - android:height="@dimen/qc_toggle_size" + android:width="@dimen/qc_toggle_background_size" + android:height="@dimen/qc_toggle_background_size" android:start="@dimen/qc_toggle_background_padding" android:top="@dimen/qc_toggle_background_padding"> <shape android:shape="rectangle">
diff --git a/car-qc-lib/res/layout/qc_row_view.xml b/car-qc-lib/res/layout/qc_row_view.xml index 6656b29..a4af032 100644 --- a/car-qc-lib/res/layout/qc_row_view.xml +++ b/car-qc-lib/res/layout/qc_row_view.xml
@@ -69,7 +69,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" app:barrierDirection="end" - app:constraint_referenced_ids="qc_icon" app:barrierAllowsGoneWidgets="false"/> <com.android.car.ui.uxr.DrawableStateTextView @@ -101,8 +100,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" app:barrierDirection="top" - app:constraint_referenced_ids="qc_seekbar_wrapper" - app:barrierAllowsGoneWidgets="false"/> + app:constraint_referenced_ids="qc_seekbar_wrapper"/> <androidx.preference.UnPressableLinearLayout android:id="@+id/qc_seekbar_wrapper" @@ -121,7 +119,7 @@ app:layout_constraintTop_toBottomOf="@+id/barrier2" app:layout_constraintBottom_toBottomOf="parent"> <com.android.car.qc.view.QCSeekBarView - android:id="@+id/seekbar" + android:id="@+id/qc_seekbar" android:layout_width="match_parent" android:layout_height="wrap_content" style="@style/Widget.QC.SeekBar"/>
diff --git a/car-qc-lib/res/values/colors.xml b/car-qc-lib/res/values/colors.xml index 62bcfdc..a2b85ff 100644 --- a/car-qc-lib/res/values/colors.xml +++ b/car-qc-lib/res/values/colors.xml
@@ -20,4 +20,12 @@ <color name="qc_toggle_unavailable_background_color">@android:color/transparent</color> <color name="qc_toggle_unavailable_color">#37FFFFFF</color> <color name="qc_toggle_rotary_shadow_color">#C7000000</color> -</resources> \ No newline at end of file + <!-- The SeekBar thumb color. --> + <color name="qc_seekbar_thumb">#FFFFFF</color> + <!-- The SeekBar thumb color when disabled. Use for the dark theme. --> + <color name="qc_seekbar_thumb_disabled_on_dark">#757575</color> + <!-- The Switch thumb color. --> + <color name="qc_switch_thumb_color">#FFFFFF</color> + <!-- The Switch thumb color when disabled. Use for the dark theme. --> + <color name="qc_switch_thumb_color_disabled_on_dark">#757575</color> +</resources>
diff --git a/car-qc-lib/res/values/dimens.xml b/car-qc-lib/res/values/dimens.xml index b973774..912891f 100644 --- a/car-qc-lib/res/values/dimens.xml +++ b/car-qc-lib/res/values/dimens.xml
@@ -24,7 +24,8 @@ <dimen name="qc_row_content_margin">16dp</dimen> <dimen name="qc_action_items_horizontal_margin">32dp</dimen> - <dimen name="qc_toggle_size">72dp</dimen> + <dimen name="qc_toggle_size">80dp</dimen> + <dimen name="qc_toggle_background_size">72dp</dimen> <dimen name="qc_toggle_margin">12dp</dimen> <dimen name="qc_row_horizontal_margin">16dp</dimen> <dimen name="qc_toggle_background_radius">16dp</dimen>
diff --git a/car-qc-lib/res/values/overlayable.xml b/car-qc-lib/res/values/overlayable.xml new file mode 100644 index 0000000..d5b405a --- /dev/null +++ b/car-qc-lib/res/values/overlayable.xml
@@ -0,0 +1,87 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!-- + ~ Copyright (C) 2022 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. + --> +<!-- +THIS FILE WAS AUTO GENERATED, DO NOT EDIT MANUALLY. +REGENERATE USING packages/apps/Car/tests/tools/rro/generate-overlayable.py +--> +<resources> + <overlayable name="car-qc-lib"> + <policy type="system|product|signature"> + <item type="attr" name="state_toggle_unavailable"/> + <item type="color" name="qc_start_icon_color"/> + <item type="color" name="qc_toggle_background_color"/> + <item type="color" name="qc_toggle_icon_fill_color"/> + <item type="color" name="qc_toggle_off_background_color"/> + <item type="color" name="qc_toggle_rotary_shadow_color"/> + <item type="color" name="qc_toggle_unavailable_background_color"/> + <item type="color" name="qc_toggle_unavailable_color"/> + <item type="dimen" name="qc_action_items_horizontal_margin"/> + <item type="dimen" name="qc_row_content_margin"/> + <item type="dimen" name="qc_row_horizontal_margin"/> + <item type="dimen" name="qc_row_icon_margin_end"/> + <item type="dimen" name="qc_row_icon_size"/> + <item type="dimen" name="qc_row_margin_vertical"/> + <item type="dimen" name="qc_row_min_height"/> + <item type="dimen" name="qc_row_padding_end"/> + <item type="dimen" name="qc_row_padding_start"/> + <item type="dimen" name="qc_seekbar_padding_top"/> + <item type="dimen" name="qc_toggle_background_padding"/> + <item type="dimen" name="qc_toggle_background_radius"/> + <item type="dimen" name="qc_toggle_foreground_icon_inset"/> + <item type="dimen" name="qc_toggle_margin"/> + <item type="dimen" name="qc_toggle_rotary_highlight_radius"/> + <item type="dimen" name="qc_toggle_rotary_highlight_size"/> + <item type="dimen" name="qc_toggle_rotary_shadow_padding"/> + <item type="dimen" name="qc_toggle_rotary_shadow_radius"/> + <item type="dimen" name="qc_toggle_rotary_shadow_size"/> + <item type="dimen" name="qc_toggle_rotary_shadow_width"/> + <item type="dimen" name="qc_toggle_background_size"/> + <item type="dimen" name="qc_toggle_size"/> + <item type="dimen" name="qc_toggle_unavailable_outline_width"/> + <item type="drawable" name="qc_row_action_divider"/> + <item type="drawable" name="qc_seekbar_wrapper_background"/> + <item type="drawable" name="qc_toggle_background"/> + <item type="drawable" name="qc_toggle_button_background"/> + <item type="drawable" name="qc_toggle_rotary_background"/> + <item type="drawable" name="qc_toggle_rotary_highlight"/> + <item type="drawable" name="qc_toggle_rotary_shadow"/> + <item type="drawable" name="qc_toggle_unavailable_background"/> + <item type="id" name="barrier1"/> + <item type="id" name="barrier2"/> + <item type="id" name="qc_icon"/> + <item type="id" name="qc_row_content"/> + <item type="id" name="qc_row_end_items"/> + <item type="id" name="qc_row_start_items"/> + <item type="id" name="qc_seekbar"/> + <item type="id" name="qc_seekbar_wrapper"/> + <item type="id" name="qc_summary"/> + <item type="id" name="qc_tile_toggle_button"/> + <item type="id" name="qc_tile_wrapper"/> + <item type="id" name="qc_title"/> + <item type="id" name="qc_toggle_button"/> + <item type="layout" name="qc_action_switch"/> + <item type="layout" name="qc_action_toggle"/> + <item type="layout" name="qc_row_view"/> + <item type="layout" name="qc_tile_view"/> + <item type="style" name="TextAppearance.QC"/> + <item type="style" name="TextAppearance.QC.Subtitle"/> + <item type="style" name="TextAppearance.QC.Title"/> + <item type="style" name="Widget.QC"/> + <item type="style" name="Widget.QC.SeekBar"/> + </policy> + </overlayable> +</resources> \ No newline at end of file
diff --git a/car-qc-lib/src/com/android/car/qc/QCActionItem.java b/car-qc-lib/src/com/android/car/qc/QCActionItem.java index c476e09..b7b9cd1 100644 --- a/car-qc-lib/src/com/android/car/qc/QCActionItem.java +++ b/car-qc-lib/src/com/android/car/qc/QCActionItem.java
@@ -17,11 +17,13 @@ package com.android.car.qc; import android.app.PendingIntent; +import android.content.Context; import android.graphics.drawable.Icon; import android.os.Parcel; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; /** * Quick Control Action that are includes as either start or end actions in {@link QCRow} @@ -32,14 +34,17 @@ private Icon mIcon; private PendingIntent mAction; private PendingIntent mDisabledClickAction; + private String mContentDescription; public QCActionItem(@NonNull @QCItemType String type, boolean isChecked, boolean isEnabled, boolean isAvailable, boolean isClickableWhileDisabled, @Nullable Icon icon, - @Nullable PendingIntent action, @Nullable PendingIntent disabledClickAction) { + @Nullable String contentDescription, @Nullable PendingIntent action, + @Nullable PendingIntent disabledClickAction) { super(type, isEnabled, isClickableWhileDisabled); mIsChecked = isChecked; mIsAvailable = isAvailable; mIcon = icon; + mContentDescription = contentDescription; mAction = action; mDisabledClickAction = disabledClickAction; } @@ -52,6 +57,10 @@ if (hasIcon) { mIcon = Icon.CREATOR.createFromParcel(in); } + boolean hasContentDescription = in.readBoolean(); + if (hasContentDescription) { + mContentDescription = in.readString(); + } boolean hasAction = in.readBoolean(); if (hasAction) { mAction = PendingIntent.CREATOR.createFromParcel(in); @@ -72,6 +81,11 @@ if (includeIcon) { mIcon.writeToParcel(dest, flags); } + boolean hasContentDescription = mContentDescription != null; + dest.writeBoolean(hasContentDescription); + if (hasContentDescription) { + dest.writeString(mContentDescription); + } boolean hasAction = mAction != null; dest.writeBoolean(hasAction); if (hasAction) { @@ -107,6 +121,11 @@ return mIcon; } + @Nullable + public String getContentDescription() { + return mContentDescription; + } + public static Creator<QCActionItem> CREATOR = new Creator<QCActionItem>() { @Override public QCActionItem createFromParcel(Parcel source) { @@ -131,6 +150,7 @@ private Icon mIcon; private PendingIntent mAction; private PendingIntent mDisabledClickAction; + private String mContentDescription; public Builder(@NonNull @QCItemType String type) { if (!isValidType(type)) { @@ -180,6 +200,23 @@ } /** + * Sets the content description + */ + public Builder setContentDescription(@Nullable String contentDescription) { + mContentDescription = contentDescription; + return this; + } + + /** + * Sets the string resource to use for content description + */ + public Builder setContentDescription(@NonNull Context context, + @StringRes int contentDescriptionResId) { + mContentDescription = context.getString(contentDescriptionResId); + return this; + } + + /** * Sets the PendingIntent to be sent when the action item is clicked. */ public Builder setAction(@Nullable PendingIntent action) { @@ -200,7 +237,8 @@ */ public QCActionItem build() { return new QCActionItem(mType, mIsChecked, mIsEnabled, mIsAvailable, - mIsClickableWhileDisabled, mIcon, mAction, mDisabledClickAction); + mIsClickableWhileDisabled, mIcon, mContentDescription, mAction, + mDisabledClickAction); } private boolean isValidType(String type) {
diff --git a/car-qc-lib/src/com/android/car/qc/provider/BaseQCProvider.java b/car-qc-lib/src/com/android/car/qc/provider/BaseQCProvider.java index 61db361..8e9e550 100644 --- a/car-qc-lib/src/com/android/car/qc/provider/BaseQCProvider.java +++ b/car-qc-lib/src/com/android/car/qc/provider/BaseQCProvider.java
@@ -172,7 +172,11 @@ try { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectAll() - .penaltyDeath() + + // TODO(268275789): Revert back to penaltyDeath and ensure it works in + // presubmit + .penaltyLog() + .build()); return onBind(uri); } finally {
diff --git a/car-qc-lib/src/com/android/car/qc/view/QCRowView.java b/car-qc-lib/src/com/android/car/qc/view/QCRowView.java index 1e10e4b..2b6b7a5 100644 --- a/car-qc-lib/src/com/android/car/qc/view/QCRowView.java +++ b/car-qc-lib/src/com/android/car/qc/view/QCRowView.java
@@ -193,7 +193,7 @@ mStartItemsContainer = findViewById(R.id.qc_row_start_items); mEndItemsContainer = findViewById(R.id.qc_row_end_items); mSeekBarContainer = findViewById(R.id.qc_seekbar_wrapper); - mSeekBar = findViewById(R.id.seekbar); + mSeekBar = findViewById(R.id.qc_seekbar); } void setActionListener(QCActionListener listener) { @@ -311,7 +311,10 @@ (action.isEnabled() || action.isClickableWhileDisabled()) && action.isAvailable(); switchView.setOnCheckedChangeListener(null); switchView.setEnabled(shouldEnableView); + switchView.setThumbTintList(getContext().getColorStateList( + R.color.qc_switch_thumb_selector)); switchView.setChecked(action.isChecked()); + switchView.setContentDescription(action.getContentDescription()); switchView.setOnTouchListener((v, event) -> { if (!action.isEnabled()) { if (event.getActionMasked() == MotionEvent.ACTION_UP) { @@ -345,6 +348,7 @@ toggleButton.setOnCheckedChangeListener(null); Drawable icon = QCViewUtils.getInstance(mContext).getToggleIcon( action.getIcon(), action.isAvailable()); + toggleButton.setContentDescription(action.getContentDescription()); toggleButton.setButtonDrawable(icon); toggleButton.setChecked(action.isChecked()); toggleButton.setEnabled(shouldEnableView); @@ -397,13 +401,15 @@ // remove current action view root.removeView(actionView); } - actionView = mLayoutInflater.inflate(resId, /* root= */ null); + actionView = mLayoutInflater.inflate(resId, root, /* attachToRoot= */ false); root.addView(actionView); return actionView; } private void initSlider(QCSlider slider) { mQCSlider = slider; + CarUiUtils.makeAllViewsEnabled(mSeekBar, slider.isEnabled()); + mSeekBar.setOnSeekBarChangeListener(null); mSeekBar.setMin(slider.getMin()); mSeekBar.setMax(slider.getMax()); @@ -411,6 +417,8 @@ mSeekBar.setEnabled(slider.isEnabled()); mSeekBar.setClickableWhileDisabled(slider.isClickableWhileDisabled()); mSeekBar.setDisabledClickListener(seekBar -> fireAction(slider, new Intent())); + mSeekBar.setThumbTintList(getContext().getColorStateList( + R.color.qc_seekbar_thumb_selector)); if (!slider.isEnabled() && mInDirectManipulationMode) { setInDirectManipulationMode(mSeekBarContainer, mSeekBar, false); }
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/QCActionItemTest.java b/car-qc-lib/tests/unit/src/com/android/car/qc/QCActionItemTest.java index ff70540..c945301 100644 --- a/car-qc-lib/tests/unit/src/com/android/car/qc/QCActionItemTest.java +++ b/car-qc-lib/tests/unit/src/com/android/car/qc/QCActionItemTest.java
@@ -33,32 +33,34 @@ @RunWith(AndroidJUnit4.class) public class QCActionItemTest extends QCItemTestCase<QCActionItem> { + private static final String TEST_CONTENT_DESCRIPTION = "test_content_description"; @Test public void onCreate_invalidType_throwsException() { assertThrows(IllegalArgumentException.class, () -> createAction("INVALID_TYPE", /* action= */ null, - /* disabledAction= */ null, /* icon= */ null)); + /* disabledAction= */ null, /* icon= */ null, /* contentDescription=*/ + null)); } @Test public void onCreateSwitch_hasCorrectType() { QCActionItem action = createAction(QC_TYPE_ACTION_SWITCH, /* action= */ null, - /* disabledAction= */ null, /* icon= */null); + /* disabledAction= */ null, /* icon= */null, /* contentDescription=*/ null); assertThat(action.getType()).isEqualTo(QC_TYPE_ACTION_SWITCH); } @Test public void onCreateToggle_hasCorrectType() { QCActionItem action = createAction(QC_TYPE_ACTION_TOGGLE, /* action= */ null, - /* disabledAction= */ null, /* icon= */ null); + /* disabledAction= */ null, /* icon= */ null, /* contentDescription=*/ null); assertThat(action.getType()).isEqualTo(QC_TYPE_ACTION_TOGGLE); } @Test public void onBundle_nullActions_noCrash() { QCActionItem action = createAction(QC_TYPE_ACTION_TOGGLE, /* action= */ null, - /* disabledAction= */ null, mDefaultIcon); + /* disabledAction= */ null, mDefaultIcon, /* contentDescription=*/ null); writeAndLoadFromBundle(action); // Test passes if this doesn't crash } @@ -66,7 +68,7 @@ @Test public void onBundle_nullIcon_noCrash() { QCActionItem action = createAction(QC_TYPE_ACTION_TOGGLE, mDefaultAction, - mDefaultDisabledAction, /* icon= */ null); + mDefaultDisabledAction, /* icon= */ null, /* contentDescription=*/ null); writeAndLoadFromBundle(action); // Test passes if this doesn't crash } @@ -74,7 +76,7 @@ @Test public void onBundle_switch_accurateData() { QCActionItem action = createAction(QC_TYPE_ACTION_SWITCH, mDefaultAction, - mDefaultDisabledAction, /* icon= */ null); + mDefaultDisabledAction, /* icon= */ null, TEST_CONTENT_DESCRIPTION); QCActionItem newAction = writeAndLoadFromBundle(action); assertThat(newAction.getType()).isEqualTo(QC_TYPE_ACTION_SWITCH); assertThat(newAction.isChecked()).isTrue(); @@ -82,12 +84,13 @@ assertThat(newAction.isClickableWhileDisabled()).isFalse(); assertThat(newAction.getPrimaryAction()).isNotNull(); assertThat(newAction.getIcon()).isNull(); + assertThat(newAction.getContentDescription()).isEqualTo(TEST_CONTENT_DESCRIPTION); } @Test public void onBundle_toggle_accurateDate() { QCActionItem action = createAction(QC_TYPE_ACTION_TOGGLE, mDefaultAction, - mDefaultDisabledAction, mDefaultIcon); + mDefaultDisabledAction, mDefaultIcon, TEST_CONTENT_DESCRIPTION); QCActionItem newAction = writeAndLoadFromBundle(action); assertThat(newAction.getType()).isEqualTo(QC_TYPE_ACTION_TOGGLE); assertThat(newAction.isChecked()).isTrue(); @@ -95,16 +98,18 @@ assertThat(newAction.isClickableWhileDisabled()).isFalse(); assertThat(newAction.getPrimaryAction()).isNotNull(); assertThat(newAction.getIcon()).isNotNull(); + assertThat(newAction.getContentDescription()).isEqualTo(TEST_CONTENT_DESCRIPTION); } private QCActionItem createAction(String type, PendingIntent action, - PendingIntent disabledAction, Icon icon) { + PendingIntent disabledAction, Icon icon, String contentDescription) { return new QCActionItem.Builder(type) .setChecked(true) .setEnabled(true) .setAction(action) .setDisabledClickAction(disabledAction) .setIcon(icon) + .setContentDescription(contentDescription) .build(); } }
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCRowViewTest.java b/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCRowViewTest.java index 647317a..9c1aa79 100644 --- a/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCRowViewTest.java +++ b/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCRowViewTest.java
@@ -30,12 +30,14 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.content.res.ColorStateList; import android.graphics.drawable.Icon; import android.view.MotionEvent; import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.SeekBar; +import android.widget.Switch; import android.widget.TextView; import androidx.test.annotation.UiThreadTest; @@ -203,11 +205,50 @@ .addSlider(new QCSlider.Builder().setInputAction(action).build()) .build(); mView.setRow(row); - SeekBar seekBar = mView.findViewById(R.id.seekbar); + SeekBar seekBar = mView.findViewById(R.id.qc_seekbar); seekBar.setProgress(50); MotionEvent motionEvent = ExtendedMockito.mock(MotionEvent.class); ExtendedMockito.when(motionEvent.getAction()).thenReturn(MotionEvent.ACTION_UP); seekBar.onTouchEvent(motionEvent); verify(action).send(any(Context.class), anyInt(), any(Intent.class)); } + + @Test + @UiThreadTest + public void setRow_switchViewThumbTintList() { + PendingIntent action = mock(PendingIntent.class); + QCRow row = new QCRow.Builder() + .addEndItem( + new QCActionItem.Builder(QC_TYPE_ACTION_SWITCH).setAction(action).build()) + .build(); + mView.setRow(row); + LinearLayout endContainer = mView.findViewById(R.id.qc_row_end_items); + assertThat(endContainer.getChildCount()).isEqualTo(1); + Switch switchView = (Switch) endContainer.getChildAt(0); + assertThat(switchView.getThumbTintList()).isNotNull(); + + ColorStateList switchColorStateList = switchView.getThumbTintList(); + int[] enabledState = {android.R.attr.state_enabled}; + int[] disabledState = {-android.R.attr.state_enabled}; + assertThat(switchColorStateList.getColorForState(enabledState, 0)).isNotEqualTo( + switchColorStateList.getColorForState(disabledState, 0)); + } + + @Test + @UiThreadTest + public void setRow_sliderViewThumbTintList() { + PendingIntent action = mock(PendingIntent.class); + QCRow row = new QCRow.Builder() + .addSlider(new QCSlider.Builder().setInputAction(action).build()) + .build(); + mView.setRow(row); + SeekBar seekBar = mView.findViewById(R.id.qc_seekbar); + assertThat(seekBar.getThumbTintList()).isNotNull(); + + ColorStateList seekBarColorStateList = seekBar.getThumbTintList(); + int[] enabledState = {android.R.attr.state_enabled}; + int[] disabledState = {-android.R.attr.state_enabled}; + assertThat(seekBarColorStateList.getColorForState(enabledState, 0)).isNotEqualTo( + seekBarColorStateList.getColorForState(disabledState, 0)); + } }
diff --git a/tools/rro/generate-overlayable.py b/tools/rro/generate-overlayable.py index 2f76a2d..b814118 100755 --- a/tools/rro/generate-overlayable.py +++ b/tools/rro/generate-overlayable.py
@@ -49,10 +49,12 @@ 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) + required_args.add_argument('-r', '--resourcePath', help='Path to resource directory (absolute or relative to cwd)', required=True, action='append') args = parser.parse_args() - resources = get_all_resources(args.resourcePath, args.excludeFiles) + resources = set() + for path in args.resourcePath: + resources |= get_all_resources(path, args.excludeFiles) generate_overlayable_file(resources, args.targetName, args.policyType, args.outputFile) def generate_overlayable_file(resources, target_name, policy_type, output_file):
diff --git a/tools/rro/resource_utils.py b/tools/rro/resource_utils.py index 5f35e1a..fe4c52c 100644 --- a/tools/rro/resource_utils.py +++ b/tools/rro/resource_utils.py
@@ -15,7 +15,11 @@ import os import re -import lxml.etree as etree +try: + import lxml.etree as etree +except ImportError: + print("Please install 'lxml' python package and retry.") + sys.exit(1) class ResourceLocation: def __init__(self, file, line=None):
diff --git a/tools/rro/verify-overlayable.py b/tools/rro/verify-overlayable.py index 05feb51..ef99aa4 100755 --- a/tools/rro/verify-overlayable.py +++ b/tools/rro/verify-overlayable.py
@@ -28,16 +28,19 @@ 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') + optional_args.add_argument('-m', '--errorMessage', nargs='*', help='Custom error message if resources are added or removed.') 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('-r', '--resourcePath', help='Path to resource directory (absolute or relative to cwd)', required=True, action='append') 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) + resources = set() + for path in args.resourcePath: + resources |= get_all_resources(path, args.excludeFiles) old_mapping = get_resources_from_single_file(args.overlayableFilePath) - compare_resources(old_mapping, resources, args.overlayableFilePath) + compare_resources(old_mapping, resources, args.overlayableFilePath, args.errorMessage) -def compare_resources(old_mapping, new_mapping, res_public_file): +def compare_resources(old_mapping, new_mapping, res_public_file, errorMsg): removed = old_mapping.difference(new_mapping) added = new_mapping.difference(old_mapping) if len(removed) > 0: @@ -45,10 +48,13 @@ 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 'python3 generate-overlayable.py' again and submit the new %s" % res_public_file) - print("Some projects may include $PROJECT_TOP/tools/generate-overlayable.sh which calls " + - "the above command with the appropriate command line arguments") + if errorMsg is not None: + print(errorMsg) + else: + print("Some resource have been modified. If this is intentional please " + + "run 'python3 generate-overlayable.py' again and submit the new %s" % res_public_file) + print("Some projects may include $PROJECT_TOP/tools/generate-overlayable.sh which calls " + + "the above command with the appropriate command line arguments") sys.exit(1) if __name__ == '__main__':