diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-05-10 15:46:41 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-05-10 15:46:41 +0000 |
commit | ee02976069cccb91375d06a2bcfeca3fccfc08bc (patch) | |
tree | be9269d71941eec7871f22db4c1263682b782298 | |
parent | 9e49fbb3afde2aafb36e70df56d0ba98a8c86030 (diff) | |
parent | bcb1dc185374136d2b72d98a89c828ae063919b3 (diff) | |
download | systemlibs-busytown-mac-infra-release.tar.gz |
Snap for 11819167 from bcb1dc185374136d2b72d98a89c828ae063919b3 to busytown-mac-infra-releasebusytown-mac-infra-release
Change-Id: Ia5d306fe4ad636c4b7b665caf3d23487a4de84cb
25 files changed, 494 insertions, 125 deletions
@@ -1,14 +1,6 @@ # People who can approve changes for submission. - -# TLs -ajchen@google.com -rlagos@google.com -stenning@google.com -yizheng@google.com -robertoalexis@google.com farivar@google.com - -# TLMs -johnchoi@google.com -nicksauer@google.com +rampara@google.com +alexstetson@google.com +babakbo@google.com igorr@google.com diff --git a/car-broadcastradio-support/Android.bp b/car-broadcastradio-support/Android.bp index d0e24e1..007a65f 100644 --- a/car-broadcastradio-support/Android.bp +++ b/car-broadcastradio-support/Android.bp @@ -15,6 +15,7 @@ // package { + default_team: "trendy_team_aaos_framework", default_applicable_licenses: ["Android-Apache-2.0"], } diff --git a/car-broadcastradio-support/OWNERS b/car-broadcastradio-support/OWNERS index 7d3fbc0..95b6918 100644 --- a/car-broadcastradio-support/OWNERS +++ b/car-broadcastradio-support/OWNERS @@ -3,4 +3,3 @@ 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..c96521a 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.NonNull; 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,8 @@ import android.util.Log; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Comparator; +import java.util.Objects; /** * Proposed extensions to android.hardware.radio.RadioManager.ProgramInfo. @@ -71,13 +74,23 @@ public class ProgramInfoExt { /** * 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 +117,17 @@ public class ProgramInfoExt { } /** + * 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 +142,75 @@ public class ProgramInfoExt { } /** - * Converts {@ProgramInfo} to {@MediaMetadata}. + * Converts {@link ProgramInfo} to {@link MediaMetadata} for displaying. + * + * <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 {@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; + ProgramSelector.Identifier logicallyTunedTo = info.getLogicallyTunedTo(); + if (logicallyTunedTo != null && logicallyTunedTo.getType() + == ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY) { + selector = ProgramSelectorExt.createAmFmSelector(logicallyTunedTo.getValue()); + } else { + selector = info.getSelector(); + } + 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}. * - * 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 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 * @return {@link MediaMetadata} object */ @@ -158,16 +245,30 @@ public class ProgramInfoExt { } 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; + } + public static class ProgramInfoComparator implements Comparator<RadioManager.ProgramInfo> { + @Override + public int compare(RadioManager.ProgramInfo info1, RadioManager.ProgramInfo info2) { + Comparator<ProgramSelector> selectorComparator = + new ProgramSelectorExt.ProgramSelectorComparator(); + return selectorComparator.compare(info1.getSelector(), info2.getSelector()); + } + } } diff --git a/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramSelectorExt.java b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramSelectorExt.java index 4b3583b..555176d 100644 --- a/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramSelectorExt.java +++ b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramSelectorExt.java @@ -29,6 +29,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.text.DecimalFormat; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -81,6 +82,11 @@ public class ProgramSelectorExt { @Retention(RetentionPolicy.SOURCE) public @interface NameFlag {} + /** + * Invalid value for a {@link ProgramSelector.Identifier} + */ + public static int INVALID_IDENTIFIER_VALUE = 0; + private static final String URI_SCHEME_BROADCASTRADIO = "broadcastradio"; private static final String URI_AUTHORITY_PROGRAM = "program"; private static final String URI_VENDOR_PREFIX = "VENDOR_"; @@ -251,26 +257,81 @@ public class ProgramSelectorExt { } /** + * Get frequency from a {@link ProgramSelector}. + * + * @param selector Program selector + * @return frequency of the first {@link ProgramSelector#IDENTIFIER_TYPE_AMFM_FREQUENCY} id in + * program selector if it exists, otherwise the AM/FM frequency in + * {@link ProgramSelector#IDENTIFIER_TYPE_HD_STATION_ID_EXT} id if it is the primary id, + * {@link #INVALID_IDENTIFIER_VALUE} otherwise. + */ + public static int getFrequency(@NonNull ProgramSelector selector) { + if (ProgramSelectorExt.hasId(selector, ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)) { + return (int) selector.getFirstId(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY); + } else if (selector.getPrimaryId().getType() + == ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT) { + return IdentifierExt.asHdPrimary(selector.getPrimaryId()).getFrequency(); + } else if (selector.getPrimaryId().getType() + == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT + || selector.getPrimaryId().getType() + == ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT) { + try { + return (int) selector.getFirstId(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY); + } catch (IllegalArgumentException e) { + return INVALID_IDENTIFIER_VALUE; + } + } + return INVALID_IDENTIFIER_VALUE; + } + + /** + * Get ensemble value from a DAB-type {@link ProgramSelector}. + * + * @param selector Program selector + * @return Value of the first {@link ProgramSelector#IDENTIFIER_TYPE_DAB_ENSEMBLE} identifier, + * 0 otherwise + */ + public static int getDabEnsemble(@NonNull ProgramSelector selector) { + try { + return (int) selector.getFirstId(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE); + } catch (IllegalArgumentException e) { + return INVALID_IDENTIFIER_VALUE; + } + } + + /** * Returns a channel name that can be displayed to the user. * - * It's implemented only for radio technologies where the channel is meant - * to be presented to the user. + * <p>It's implemented only for radio technologies where the channel is meant + * to be presented to the user, such as FM/AM and HD radio. + * + * <p>For HD radio, the display name is prefix with "-HD[NUMBER]" where the number is the + * sub channel. * * @param sel the program selector - * @return Channel name or null, if radio technology doesn't present channel names to the user. + * @return Channel name or {@code null}, if radio technology doesn't present channel names to + * the user. */ public static @Nullable String getDisplayName(@NonNull ProgramSelector sel, @NameFlag int flags) { boolean noProgramTypeFallback = (flags & NAME_NO_PROGRAM_TYPE_FALLBACK) != 0; if (isAmFmProgram(sel)) { - if (!hasId(sel, ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)) { + long freq; + String hdSuffix = ""; + if (sel.getPrimaryId().getType() + == ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT) { + IdentifierExt.HdPrimary hdIdExt = IdentifierExt.asHdPrimary(sel.getPrimaryId()); + freq = hdIdExt.getFrequency(); + hdSuffix = "-HD" + (hdIdExt.getSubchannel() + 1); + } else if (hasId(sel, ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)) { + freq = sel.getFirstId(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY); + } else { if (noProgramTypeFallback) return null; // if there is no frequency assigned, let's assume it's a malformed RDS selector return "FM"; } - long freq = sel.getFirstId(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY); - return formatAmFmFrequency(freq, flags); + return formatAmFmFrequency(freq, flags) + hdSuffix; } if ((flags & NAME_MODULATION_ONLY) != 0) return null; @@ -285,7 +346,8 @@ public class ProgramSelectorExt { switch (sel.getPrimaryId().getType()) { case ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID: return "SXM"; - case ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC: + case ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT: + case ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT: return "DAB"; case ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID: return "DRMO"; @@ -442,10 +504,10 @@ public class ProgramSelectorExt { */ public static class IdentifierExt { /** - * Decode {@link ProgramSelector#IDENTIFIER_TYPE_HD_STATION_ID_EXT} value. + * Decoder of {@link ProgramSelector#IDENTIFIER_TYPE_HD_STATION_ID_EXT} value. * - * @param id identifier to decode - * @return value decoder + * When pushed to the framework, it will be non-static class referring + * to the original value. */ public static @Nullable HdPrimary asHdPrimary(@NonNull Identifier id) { if (id.getType() == ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT) { @@ -482,5 +544,110 @@ public class ProgramSelectorExt { return (int) ((mValue >>> (32 + 4)) & 0x3FFFF); } } + + /** + * Decoder of {@link ProgramSelector#IDENTIFIER_TYPE_DAB_DMB_SID_EXT} value. + * + * <p>When pushed to the framework, it will be non-static class referring + * to the original value. + * @param id Identifier to be decoded + * @return {@link DabPrimary} object if the identifier is DAB-type, {@code null} otherwise + */ + public static @Nullable DabPrimary asDabPrimary(@NonNull Identifier id) { + if (id.getType() == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT) { + return new DabPrimary(id.getValue()); + } + return null; + } + + /** + * Decoder of {@link ProgramSelector#IDENTIFIER_TYPE_DAB_DMB_SID_EXT} value. + * + * <p>When pushed to the framework, it will be non-static class referring + * to the original value. + */ + public static class DabPrimary { + private final long mValue; + + private DabPrimary(long value) { + mValue = value; + } + + /** + * Get Service Identifier (SId). + * @return SId value + */ + public int getSId() { + return (int) (mValue & 0xFFFFFFFF); + } + + /** + * Get Extended Country Code (ECC) + * @return Extended Country Code + */ + public int getEcc() { + return (int) ((mValue >>> 32) & 0xFF); + } + + /** + * Get SCIdS (Service Component Identifier within the Service) value + * @return SCIdS value + */ + public int getSCIdS() { + return (int) ((mValue >>> (32 + 8)) & 0xF); + } + } + } + + public static class ProgramSelectorComparator implements Comparator<ProgramSelector> { + @Override + public int compare(ProgramSelector selector1, ProgramSelector selector2) { + int type1 = selector1.getPrimaryId().getType(); + int type2 = selector2.getPrimaryId().getType(); + int frequency1 = getFrequency(selector1); + int frequency2 = getFrequency(selector2); + if (isAmFmProgram(selector1) && isAmFmProgram(selector2)) { + if (frequency1 != frequency2) { + return frequency1 > frequency2 ? 1 : -1; + } + int subchannel1 = selector1.getPrimaryId().getType() + == ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT + ? IdentifierExt.asHdPrimary(selector1.getPrimaryId()).getSubchannel() : 0; + int subchannel2 = selector2.getPrimaryId().getType() + == ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT + ? IdentifierExt.asHdPrimary(selector2.getPrimaryId()).getSubchannel() : 0; + if (subchannel1 != subchannel2) { + return subchannel1 > subchannel2 ? 1 : -1; + } + return selector1.getPrimaryId().getType() - selector2.getPrimaryId().getType(); + } else if (type1 == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT + && type2 == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT) { + if (frequency1 != frequency2) { + return frequency1 > frequency2 ? 1 : -1; + } + IdentifierExt.DabPrimary dabPrimary1 = IdentifierExt.asDabPrimary( + selector1.getPrimaryId()); + IdentifierExt.DabPrimary dabPrimary2 = IdentifierExt.asDabPrimary( + selector2.getPrimaryId()); + int ecc1 = dabPrimary1.getEcc(); + int ecc2 = dabPrimary2.getEcc(); + if (ecc1 != ecc2) { + return ecc1 > ecc2 ? 1 : -1; + } + int sId1 = dabPrimary1.getSId(); + int sId2 = dabPrimary2.getSId(); + if (sId1 != sId2) { + return sId1 > sId2 ? 1 : -1; + } + int sCIds1 = dabPrimary1.getSCIdS(); + int sCIds2 = dabPrimary2.getSCIdS(); + if (sCIds1 != sCIds2) { + return sCIds1 > sCIds2 ? 1 : -1; + } + return getDabEnsemble(selector1) > getDabEnsemble(selector2) ? 1 : -1; + } + return type1 > type2 || (type1 == type2 && selector1.getPrimaryId().getValue() + > selector2.getPrimaryId().getValue()) ? 1 : -1; + } } } diff --git a/car-qc-lib/Android.bp b/car-qc-lib/Android.bp index c12fd28..4d3a297 100644 --- a/car-qc-lib/Android.bp +++ b/car-qc-lib/Android.bp @@ -19,13 +19,13 @@ package { android_library { name: "car-qc-lib", - platform_apis: true, srcs: ["src/**/*.java"], optimize: { enabled: false, }, static_libs: [ "androidx.annotation_annotation", - "car-ui-lib" + "car-ui-lib-no-overlayable", + "car-resource-common", ], } diff --git a/car-qc-lib/OWNERS b/car-qc-lib/OWNERS index 7f8081c..9613cf4 100644 --- a/car-qc-lib/OWNERS +++ b/car-qc-lib/OWNERS @@ -4,5 +4,5 @@ alexstetson@google.com # Secondary (only if people in Primary are unreachable) -hseog@google.com -nehah@google.com +babakbo@google.com +igorr@google.com
\ No newline at end of file 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..9976ac6 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 @@ -78,7 +77,7 @@ android:layout_height="wrap_content" android:layout_centerVertical="true" android:singleLine="true" - android:textAppearance="@style/TextAppearance.QC.Title" + style="@style/TextAppearance.QC.Title" app:layout_constraintStart_toEndOf="@+id/barrier1" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@+id/qc_summary" @@ -90,7 +89,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_centerVertical="true" - android:textAppearance="@style/TextAppearance.QC.Subtitle" + style="@style/TextAppearance.QC.Subtitle" app:layout_constraintStart_toEndOf="@+id/barrier1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/qc_title" @@ -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/layout/qc_tile_view.xml b/car-qc-lib/res/layout/qc_tile_view.xml index 7fb0884..c7b7511 100644 --- a/car-qc-lib/res/layout/qc_tile_view.xml +++ b/car-qc-lib/res/layout/qc_tile_view.xml @@ -37,5 +37,5 @@ android:id="@android:id/summary" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textAppearance="@style/TextAppearance.QC.Subtitle"/> + style="@style/TextAppearance.QC.Subtitle"/> </com.android.car.ui.uxr.DrawableStateLinearLayout>
\ No newline at end of file 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/styles.xml b/car-qc-lib/res/values/styles.xml index 587b522..51a7d35 100644 --- a/car-qc-lib/res/values/styles.xml +++ b/car-qc-lib/res/values/styles.xml @@ -16,16 +16,15 @@ <resources> <style name="TextAppearance.QC" parent="android:TextAppearance.DeviceDefault"> - <item name="android:textColor">@color/car_ui_text_color_primary</item> + <item name="android:textColor">@color/car_on_surface</item> </style> - <style name="TextAppearance.QC.Title"> - <item name="android:textSize">@dimen/car_ui_body1_size</item> + <style name="TextAppearance.QC.Title" parent="android:TextAppearance.DeviceDefault.Large"> + <item name="android:textColor">@color/car_on_surface</item> </style> - <style name="TextAppearance.QC.Subtitle"> - <item name="android:textColor">@color/car_ui_text_color_secondary</item> - <item name="android:textSize">@dimen/car_ui_body3_size</item> + <style name="TextAppearance.QC.Subtitle" parent="android:TextAppearance.DeviceDefault.Small"> + <item name="android:textColor">@color/car_on_surface_variant</item> </style> <style name="Widget.QC" parent="android:Widget.DeviceDefault"/> 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..f4e92d1 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} @@ -29,17 +31,22 @@ import androidx.annotation.Nullable; public class QCActionItem extends QCItem { private final boolean mIsChecked; private final boolean mIsAvailable; + private final boolean mIsClickable; 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, + boolean isAvailable, boolean isClickable, boolean isClickableWhileDisabled, + @Nullable Icon icon, @Nullable String contentDescription, @Nullable PendingIntent action, @Nullable PendingIntent disabledClickAction) { super(type, isEnabled, isClickableWhileDisabled); mIsChecked = isChecked; mIsAvailable = isAvailable; + mIsClickable = isClickable; mIcon = icon; + mContentDescription = contentDescription; mAction = action; mDisabledClickAction = disabledClickAction; } @@ -48,10 +55,15 @@ public class QCActionItem extends QCItem { super(in); mIsChecked = in.readBoolean(); mIsAvailable = in.readBoolean(); + mIsClickable = in.readBoolean(); boolean hasIcon = in.readBoolean(); 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); @@ -67,11 +79,17 @@ public class QCActionItem extends QCItem { super.writeToParcel(dest, flags); dest.writeBoolean(mIsChecked); dest.writeBoolean(mIsAvailable); + dest.writeBoolean(mIsClickable); boolean includeIcon = getType().equals(QC_TYPE_ACTION_TOGGLE) && mIcon != null; dest.writeBoolean(includeIcon); 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) { @@ -102,11 +120,20 @@ public class QCActionItem extends QCItem { return mIsAvailable; } + public boolean isClickable() { + return mIsClickable; + } + @Nullable public Icon getIcon() { return mIcon; } + @Nullable + public String getContentDescription() { + return mContentDescription; + } + public static Creator<QCActionItem> CREATOR = new Creator<QCActionItem>() { @Override public QCActionItem createFromParcel(Parcel source) { @@ -127,10 +154,12 @@ public class QCActionItem extends QCItem { private boolean mIsChecked; private boolean mIsEnabled = true; private boolean mIsAvailable = true; + private boolean mIsClickable = true; private boolean mIsClickableWhileDisabled = false; private Icon mIcon; private PendingIntent mAction; private PendingIntent mDisabledClickAction; + private String mContentDescription; public Builder(@NonNull @QCItemType String type) { if (!isValidType(type)) { @@ -164,6 +193,15 @@ public class QCActionItem extends QCItem { } /** + * Sets whether the action is clickable. This differs from available in that the style will + * remain as if it's enabled/available but click actions will not be processed. + */ + public Builder setClickable(boolean clickable) { + mIsClickable = clickable; + return this; + } + + /** * Sets whether or not an action item should be clickable while disabled. */ public Builder setClickableWhileDisabled(boolean clickable) { @@ -180,6 +218,23 @@ public class QCActionItem extends QCItem { } /** + * 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) { @@ -199,8 +254,9 @@ public class QCActionItem extends QCItem { * Builds the final {@link QCActionItem}. */ public QCActionItem build() { - return new QCActionItem(mType, mIsChecked, mIsEnabled, mIsAvailable, - mIsClickableWhileDisabled, mIcon, mAction, mDisabledClickAction); + return new QCActionItem(mType, mIsChecked, mIsEnabled, mIsAvailable, mIsClickable, + 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 @@ public abstract class BaseQCProvider extends ContentProvider { 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..2045fd2 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 @@ public class QCRowView extends FrameLayout { 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) { @@ -308,10 +308,12 @@ public class QCRowView extends FrameLayout { CarUiUtils.makeAllViewsEnabled(switchView, action.isEnabled()); boolean shouldEnableView = - (action.isEnabled() || action.isClickableWhileDisabled()) && action.isAvailable(); + (action.isEnabled() || action.isClickableWhileDisabled()) && action.isAvailable() + && action.isClickable(); switchView.setOnCheckedChangeListener(null); switchView.setEnabled(shouldEnableView); switchView.setChecked(action.isChecked()); + switchView.setContentDescription(action.getContentDescription()); switchView.setOnTouchListener((v, event) -> { if (!action.isEnabled()) { if (event.getActionMasked() == MotionEvent.ACTION_UP) { @@ -338,13 +340,14 @@ public class QCRowView extends FrameLayout { } DrawableStateToggleButton toggleButton = tmpToggleButton; // must be effectively final boolean shouldEnableView = - (action.isEnabled() || action.isClickableWhileDisabled()) && action.isAvailable(); + (action.isEnabled() || action.isClickableWhileDisabled()) && action.isAvailable() + && action.isClickable(); toggleButton.setText(null); toggleButton.setTextOn(null); toggleButton.setTextOff(null); toggleButton.setOnCheckedChangeListener(null); - Drawable icon = QCViewUtils.getInstance(mContext).getToggleIcon( - action.getIcon(), action.isAvailable()); + Drawable icon = QCViewUtils.getToggleIcon(mContext, action.getIcon(), action.isAvailable()); + toggleButton.setContentDescription(action.getContentDescription()); toggleButton.setButtonDrawable(icon); toggleButton.setChecked(action.isChecked()); toggleButton.setEnabled(shouldEnableView); @@ -397,13 +400,15 @@ public class QCRowView extends FrameLayout { // 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()); diff --git a/car-qc-lib/src/com/android/car/qc/view/QCTileView.java b/car-qc-lib/src/com/android/car/qc/view/QCTileView.java index 33c0eff..4173e25 100644 --- a/car-qc-lib/src/com/android/car/qc/view/QCTileView.java +++ b/car-qc-lib/src/com/android/car/qc/view/QCTileView.java @@ -125,8 +125,7 @@ public class QCTileView extends FrameLayout implements Observer<QCItem> { } mToggleButton.toggle(); }); - Drawable icon = QCViewUtils.getInstance(mContext).getToggleIcon( - qcTile.getIcon(), qcTile.isAvailable()); + Drawable icon = QCViewUtils.getToggleIcon(mContext, qcTile.getIcon(), qcTile.isAvailable()); mToggleButton.setButtonDrawable(icon); mToggleButton.setOnCheckedChangeListener( (buttonView, isChecked) -> { diff --git a/car-qc-lib/src/com/android/car/qc/view/QCViewUtils.java b/car-qc-lib/src/com/android/car/qc/view/QCViewUtils.java index 366c724..ca0f877 100644 --- a/car-qc-lib/src/com/android/car/qc/view/QCViewUtils.java +++ b/car-qc-lib/src/com/android/car/qc/view/QCViewUtils.java @@ -16,7 +16,6 @@ package com.android.car.qc.view; -import android.annotation.ColorInt; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; @@ -32,64 +31,44 @@ import com.android.car.qc.R; * Utility class used by {@link QCTileView} and {@link QCRowView} */ public class QCViewUtils { - private static QCViewUtils sInstance; - - private final Context mContext; - private final Drawable mDefaultToggleBackground; - private final Drawable mUnavailableToggleBackground; - private final ColorStateList mDefaultToggleIconTint; - @ColorInt - private final int mUnavailableToggleIconTint; - private final int mToggleForegroundIconInset; - - private QCViewUtils(@NonNull Context context) { - mContext = context.getApplicationContext(); - mDefaultToggleBackground = mContext.getDrawable(R.drawable.qc_toggle_background); - mUnavailableToggleBackground = mContext.getDrawable( - R.drawable.qc_toggle_unavailable_background); - mDefaultToggleIconTint = mContext.getColorStateList(R.color.qc_toggle_icon_fill_color); - mUnavailableToggleIconTint = mContext.getColor(R.color.qc_toggle_unavailable_color); - mToggleForegroundIconInset = mContext.getResources() - .getDimensionPixelSize(R.dimen.qc_toggle_foreground_icon_inset); - } - - /** - * Get an instance of {@link QCViewUtils} - */ - public static QCViewUtils getInstance(@NonNull Context context) { - if (sInstance == null) { - sInstance = new QCViewUtils(context); - } - return sInstance; - } /** * Create a return a Quick Control toggle icon - used for tiles and action toggles. */ - public Drawable getToggleIcon(@Nullable Icon icon, boolean available) { + public static Drawable getToggleIcon(@NonNull Context context, @Nullable Icon icon, + boolean available) { + Drawable defaultToggleBackground = context.getDrawable(R.drawable.qc_toggle_background); + Drawable unavailableToggleBackground = context.getDrawable( + R.drawable.qc_toggle_unavailable_background); + int toggleForegroundIconInset = context.getResources() + .getDimensionPixelSize(R.dimen.qc_toggle_foreground_icon_inset); + Drawable background = available - ? mDefaultToggleBackground.getConstantState().newDrawable().mutate() - : mUnavailableToggleBackground.getConstantState().newDrawable().mutate(); + ? defaultToggleBackground.getConstantState().newDrawable().mutate() + : unavailableToggleBackground.getConstantState().newDrawable().mutate(); if (icon == null) { return background; } - Drawable iconDrawable = icon.loadDrawable(mContext); + Drawable iconDrawable = icon.loadDrawable(context); if (iconDrawable == null) { return background; } if (!available) { - iconDrawable.setTint(mUnavailableToggleIconTint); + int unavailableToggleIconTint = context.getColor(R.color.qc_toggle_unavailable_color); + iconDrawable.setTint(unavailableToggleIconTint); } else { - iconDrawable.setTintList(mDefaultToggleIconTint); + ColorStateList defaultToggleIconTint = context.getColorStateList( + R.color.qc_toggle_icon_fill_color); + iconDrawable.setTintList(defaultToggleIconTint); } Drawable[] layers = {background, iconDrawable}; LayerDrawable drawable = new LayerDrawable(layers); - drawable.setLayerInsetRelative(/* index= */ 1, mToggleForegroundIconInset, - mToggleForegroundIconInset, mToggleForegroundIconInset, - mToggleForegroundIconInset); + drawable.setLayerInsetRelative(/* index= */ 1, toggleForegroundIconInset, + toggleForegroundIconInset, toggleForegroundIconInset, + toggleForegroundIconInset); return drawable; } } diff --git a/car-qc-lib/tests/unit/Android.bp b/car-qc-lib/tests/unit/Android.bp index b1f107a..3af82b4 100644 --- a/car-qc-lib/tests/unit/Android.bp +++ b/car-qc-lib/tests/unit/Android.bp @@ -14,6 +14,7 @@ // limitations under the License. package { + default_team: "trendy_team_system_experience", default_applicable_licenses: ["Android-Apache-2.0"], } @@ -39,9 +40,12 @@ android_test { "androidx.test.ext.truth", "mockito-target-extended-minus-junit4", "platform-test-annotations", - "truth-prebuilt", + "truth", "testng", ], - jni_libs: ["libdexmakerjvmtiagent", "libstaticjvmtiagent"], + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], } 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 @@ import org.junit.runner.RunWith; @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 @@ public class QCActionItemTest extends QCItemTestCase<QCActionItem> { @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 @@ public class QCActionItemTest extends QCItemTestCase<QCActionItem> { @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 @@ public class QCActionItemTest extends QCItemTestCase<QCActionItem> { 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 @@ public class QCActionItemTest extends QCItemTestCase<QCActionItem> { 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 static org.mockito.Mockito.verify; 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 @@ public class QCRowViewTest { .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 @@ def main(): 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..003c69e 100644 --- a/tools/rro/resource_utils.py +++ b/tools/rro/resource_utils.py @@ -15,7 +15,13 @@ import os import re -import lxml.etree as etree +import sys +try: + import lxml.etree as etree +except ImportError: + print("Please install 'lxml' python package and retry. \n" + + "E.g., you can use 'sudo apt-get install python3-lxml'.") + 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 2625310..ef99aa4 100755 --- a/tools/rro/verify-overlayable.py +++ b/tools/rro/verify-overlayable.py @@ -30,11 +30,13 @@ def main(): 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, args.errorMessage) |