diff options
387 files changed, 12798 insertions, 8223 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 705a4df6c30f..efd85788574e 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -430,10 +430,7 @@ java_aconfig_library { aconfig_declarations { name: "com.android.media.flags.bettertogether-aconfig", package: "com.android.media.flags", - srcs: [ - "media/java/android/media/flags/media_better_together.aconfig", - "media/java/android/media/flags/fade_manager_configuration.aconfig", - ], + srcs: ["media/java/android/media/flags/media_better_together.aconfig"], } java_aconfig_library { diff --git a/Android.bp b/Android.bp index 91f03e0036a3..bb9304819e80 100644 --- a/Android.bp +++ b/Android.bp @@ -97,6 +97,7 @@ filegroup { // AIDL sources from external directories ":android.hardware.biometrics.common-V4-java-source", ":android.hardware.biometrics.fingerprint-V3-java-source", + ":android.hardware.biometrics.face-V4-java-source", ":android.hardware.gnss-V2-java-source", ":android.hardware.graphics.common-V3-java-source", ":android.hardware.keymaster-V4-java-source", diff --git a/Ravenwood.bp b/Ravenwood.bp index 03f3f0f3d61d..f330ad14ea57 100644 --- a/Ravenwood.bp +++ b/Ravenwood.bp @@ -65,7 +65,7 @@ java_genrule { // depend on it. java_genrule { name: "framework-minus-apex.ravenwood", - defaults: ["hoststubgen-for-prototype-only-genrule"], + defaults: ["ravenwood-internal-only-visibility-genrule"], cmd: "cp $(in) $(out)", srcs: [ ":framework-minus-apex.ravenwood-base{ravenwood.jar}", diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index 83db4cbb7e43..900c90203f41 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -1828,7 +1828,9 @@ public class JobSchedulerService extends com.android.server.SystemService /* system_measured_source_download_bytes */0, /* system_measured_source_upload_bytes */ 0, /* system_measured_calling_download_bytes */0, - /* system_measured_calling_upload_bytes */ 0); + /* system_measured_calling_upload_bytes */ 0, + jobStatus.getJob().getIntervalMillis(), + jobStatus.getJob().getFlexMillis()); // If the job is immediately ready to run, then we can just immediately // put it in the pending list and try to schedule it. This is especially @@ -2269,7 +2271,9 @@ public class JobSchedulerService extends com.android.server.SystemService /* system_measured_source_download_bytes */ 0, /* system_measured_source_upload_bytes */ 0, /* system_measured_calling_download_bytes */0, - /* system_measured_calling_upload_bytes */ 0); + /* system_measured_calling_upload_bytes */ 0, + cancelled.getJob().getIntervalMillis(), + cancelled.getJob().getFlexMillis()); } // If this is a replacement, bring in the new version of the job if (incomingJob != null) { diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java index 6449edcd3103..3addf9f98db2 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -534,7 +534,9 @@ public final class JobServiceContext implements ServiceConnection { /* system_measured_source_download_bytes */ 0, /* system_measured_source_upload_bytes */ 0, /* system_measured_calling_download_bytes */ 0, - /* system_measured_calling_upload_bytes */ 0); + /* system_measured_calling_upload_bytes */ 0, + job.getJob().getIntervalMillis(), + job.getJob().getFlexMillis()); sEnqueuedJwiAtJobStart.logSampleWithUid(job.getUid(), job.getWorkCount()); final String sourcePackage = job.getSourcePackageName(); if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { @@ -1616,7 +1618,9 @@ public final class JobServiceContext implements ServiceConnection { TrafficStats.getUidRxBytes(completedJob.getUid()) - mInitialDownloadedBytesFromCalling, TrafficStats.getUidTxBytes(completedJob.getUid()) - - mInitialUploadedBytesFromCalling); + - mInitialUploadedBytesFromCalling, + completedJob.getJob().getIntervalMillis(), + completedJob.getJob().getFlexMillis()); if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_SYSTEM_SERVER, "JobScheduler", getId()); diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java index 913a76a65026..4d4e3407a3c3 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java @@ -591,6 +591,16 @@ public class AppIdleHistory { if (idle) { newBucket = IDLE_BUCKET_CUTOFF; reason = REASON_MAIN_FORCED_BY_USER; + final AppUsageHistory appHistory = getAppUsageHistory(packageName, userId, + elapsedRealtime); + // Wipe all expiry times that could raise the bucket on reevaluation. + if (appHistory.bucketExpiryTimesMs != null) { + for (int i = appHistory.bucketExpiryTimesMs.size() - 1; i >= 0; --i) { + if (appHistory.bucketExpiryTimesMs.keyAt(i) < newBucket) { + appHistory.bucketExpiryTimesMs.removeAt(i); + } + } + } } else { newBucket = STANDBY_BUCKET_ACTIVE; // This is to pretend that the app was just used, don't freeze the state anymore. diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java index bd5c00600044..e589c214801a 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java @@ -59,6 +59,7 @@ import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.usage.AppIdleHistory.STANDBY_BUCKET_UNKNOWN; import android.annotation.CurrentTimeMillisLong; +import android.annotation.DurationMillisLong; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -2146,6 +2147,15 @@ public class AppStandbyController } } + /** + * Flush the handler. + * Returns true if successfully flushed within the timeout, otherwise return false. + */ + @VisibleForTesting + boolean flushHandler(@DurationMillisLong long timeoutMillis) { + return mHandler.runWithScissors(() -> {}, timeoutMillis); + } + @Override public void flushToDisk() { synchronized (mAppIdleLock) { diff --git a/core/api/current.txt b/core/api/current.txt index 12a6f7459bce..008521a0446a 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -35874,19 +35874,19 @@ package android.provider { field public static final String NAMESPACE = "data2"; } - public static final class ContactsContract.CommonDataKinds.Im implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins { - method public static CharSequence getProtocolLabel(android.content.res.Resources, int, CharSequence); - method public static int getProtocolLabelResource(int); - method public static CharSequence getTypeLabel(android.content.res.Resources, int, @Nullable CharSequence); - method public static int getTypeLabelResource(int); - field public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/im"; - field public static final String CUSTOM_PROTOCOL = "data6"; + @Deprecated public static final class ContactsContract.CommonDataKinds.Im implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins { + method @Deprecated public static CharSequence getProtocolLabel(android.content.res.Resources, int, CharSequence); + method @Deprecated public static int getProtocolLabelResource(int); + method @Deprecated public static CharSequence getTypeLabel(android.content.res.Resources, int, @Nullable CharSequence); + method @Deprecated public static int getTypeLabelResource(int); + field @Deprecated public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/im"; + field @Deprecated public static final String CUSTOM_PROTOCOL = "data6"; field public static final String EXTRA_ADDRESS_BOOK_INDEX = "android.provider.extra.ADDRESS_BOOK_INDEX"; field public static final String EXTRA_ADDRESS_BOOK_INDEX_COUNTS = "android.provider.extra.ADDRESS_BOOK_INDEX_COUNTS"; field public static final String EXTRA_ADDRESS_BOOK_INDEX_TITLES = "android.provider.extra.ADDRESS_BOOK_INDEX_TITLES"; - field public static final String PROTOCOL = "data5"; + field @Deprecated public static final String PROTOCOL = "data5"; field @Deprecated public static final int PROTOCOL_AIM = 0; // 0x0 - field public static final int PROTOCOL_CUSTOM = -1; // 0xffffffff + field @Deprecated public static final int PROTOCOL_CUSTOM = -1; // 0xffffffff field @Deprecated public static final int PROTOCOL_GOOGLE_TALK = 5; // 0x5 field @Deprecated public static final int PROTOCOL_ICQ = 6; // 0x6 field @Deprecated public static final int PROTOCOL_JABBER = 7; // 0x7 @@ -35895,9 +35895,9 @@ package android.provider { field @Deprecated public static final int PROTOCOL_QQ = 4; // 0x4 field @Deprecated public static final int PROTOCOL_SKYPE = 3; // 0x3 field @Deprecated public static final int PROTOCOL_YAHOO = 2; // 0x2 - field public static final int TYPE_HOME = 1; // 0x1 - field public static final int TYPE_OTHER = 3; // 0x3 - field public static final int TYPE_WORK = 2; // 0x2 + field @Deprecated public static final int TYPE_HOME = 1; // 0x1 + field @Deprecated public static final int TYPE_OTHER = 3; // 0x3 + field @Deprecated public static final int TYPE_WORK = 2; // 0x2 } public static final class ContactsContract.CommonDataKinds.Nickname implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins { @@ -36012,17 +36012,17 @@ package android.provider { field public static final int TYPE_SPOUSE = 14; // 0xe } - public static final class ContactsContract.CommonDataKinds.SipAddress implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins { - method public static CharSequence getTypeLabel(android.content.res.Resources, int, @Nullable CharSequence); - method public static int getTypeLabelResource(int); - field public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/sip_address"; + @Deprecated public static final class ContactsContract.CommonDataKinds.SipAddress implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins { + method @Deprecated public static CharSequence getTypeLabel(android.content.res.Resources, int, @Nullable CharSequence); + method @Deprecated public static int getTypeLabelResource(int); + field @Deprecated public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/sip_address"; field public static final String EXTRA_ADDRESS_BOOK_INDEX = "android.provider.extra.ADDRESS_BOOK_INDEX"; field public static final String EXTRA_ADDRESS_BOOK_INDEX_COUNTS = "android.provider.extra.ADDRESS_BOOK_INDEX_COUNTS"; field public static final String EXTRA_ADDRESS_BOOK_INDEX_TITLES = "android.provider.extra.ADDRESS_BOOK_INDEX_TITLES"; - field public static final String SIP_ADDRESS = "data1"; - field public static final int TYPE_HOME = 1; // 0x1 - field public static final int TYPE_OTHER = 3; // 0x3 - field public static final int TYPE_WORK = 2; // 0x2 + field @Deprecated public static final String SIP_ADDRESS = "data1"; + field @Deprecated public static final int TYPE_HOME = 1; // 0x1 + field @Deprecated public static final int TYPE_OTHER = 3; // 0x3 + field @Deprecated public static final int TYPE_WORK = 2; // 0x2 } public static final class ContactsContract.CommonDataKinds.StructuredName implements android.provider.ContactsContract.DataColumnsWithJoins { @@ -43403,6 +43403,7 @@ package android.telephony { field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_PARAMETERS_USED_FOR_NTN_LTE_SIGNAL_BAR_INT = "parameters_used_for_ntn_lte_signal_bar_int"; field public static final String KEY_PING_TEST_BEFORE_DATA_SWITCH_BOOL = "ping_test_before_data_switch_bool"; field public static final String KEY_PREFER_2G_BOOL = "prefer_2g_bool"; + field @FlaggedApi("com.android.internal.telephony.flags.hide_prefer_3g_item") public static final String KEY_PREFER_3G_VISIBILITY_BOOL = "prefer_3g_visibility_bool"; field public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_DAILY_NOTIFICATION_COUNT_INT = "premium_capability_maximum_daily_notification_count_int"; field public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_MONTHLY_NOTIFICATION_COUNT_INT = "premium_capability_maximum_monthly_notification_count_int"; field public static final String KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG = "premium_capability_network_setup_time_millis_long"; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 8ce3a8dfb5a2..51e61e6239bb 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -3350,7 +3350,7 @@ package android.companion.virtual.camera { } @FlaggedApi("android.companion.virtual.flags.virtual_camera") public interface VirtualCameraCallback { - method public void onProcessCaptureRequest(int, long, @Nullable android.companion.virtual.camera.VirtualCameraMetadata); + method public default void onProcessCaptureRequest(int, long); method public void onStreamClosed(int); method public void onStreamConfigured(int, @NonNull android.view.Surface, @NonNull android.companion.virtual.camera.VirtualCameraStreamConfig); } @@ -3371,12 +3371,6 @@ package android.companion.virtual.camera { method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setVirtualCameraCallback(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.camera.VirtualCameraCallback); } - @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraMetadata implements android.os.Parcelable { - method public int describeContents(); - method public void writeToParcel(@NonNull android.os.Parcel, int); - field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.camera.VirtualCameraMetadata> CREATOR; - } - @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraStreamConfig implements android.os.Parcelable { ctor public VirtualCameraStreamConfig(@IntRange(from=1) int, @IntRange(from=1) int, int); method public int describeContents(); @@ -6478,6 +6472,7 @@ package android.media { method public void clearAudioServerStateCallback(); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean clearPreferredDevicesForCapturePreset(int); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int dispatchAudioFocusChange(@NonNull android.media.AudioFocusInfo, int, @NonNull android.media.audiopolicy.AudioPolicy); + method @FlaggedApi("android.media.audiopolicy.enable_fade_manager_configuration") @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public int dispatchAudioFocusChangeWithFade(@NonNull android.media.AudioFocusInfo, int, @NonNull android.media.audiopolicy.AudioPolicy, @NonNull java.util.List<android.media.AudioFocusInfo>, @Nullable android.media.FadeManagerConfiguration); method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int[] getActiveAssistantServicesUids(); method @IntRange(from=0) public long getAdditionalOutputDeviceDelay(@NonNull android.media.AudioDeviceInfo); method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int[] getAssistantServicesUids(); @@ -6677,6 +6672,69 @@ package android.media { field public static final int CONTENT_ID_NONE = 0; // 0x0 } + @FlaggedApi("android.media.audiopolicy.enable_fade_manager_configuration") public final class FadeManagerConfiguration implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public java.util.List<android.media.AudioAttributes> getAudioAttributesWithVolumeShaperConfigs(); + method public long getFadeInDelayForOffenders(); + method public long getFadeInDurationForAudioAttributes(@NonNull android.media.AudioAttributes); + method public long getFadeInDurationForUsage(int); + method @Nullable public android.media.VolumeShaper.Configuration getFadeInVolumeShaperConfigForAudioAttributes(@NonNull android.media.AudioAttributes); + method @Nullable public android.media.VolumeShaper.Configuration getFadeInVolumeShaperConfigForUsage(int); + method public long getFadeOutDurationForAudioAttributes(@NonNull android.media.AudioAttributes); + method public long getFadeOutDurationForUsage(int); + method @Nullable public android.media.VolumeShaper.Configuration getFadeOutVolumeShaperConfigForAudioAttributes(@NonNull android.media.AudioAttributes); + method @Nullable public android.media.VolumeShaper.Configuration getFadeOutVolumeShaperConfigForUsage(int); + method public int getFadeState(); + method @NonNull public java.util.List<java.lang.Integer> getFadeableUsages(); + method @NonNull public java.util.List<android.media.AudioAttributes> getUnfadeableAudioAttributes(); + method @NonNull public java.util.List<java.lang.Integer> getUnfadeableContentTypes(); + method @NonNull public java.util.List<java.lang.Integer> getUnfadeablePlayerTypes(); + method @NonNull public java.util.List<java.lang.Integer> getUnfadeableUids(); + method public boolean isAudioAttributesUnfadeable(@NonNull android.media.AudioAttributes); + method public boolean isContentTypeUnfadeable(int); + method public boolean isFadeEnabled(); + method public boolean isPlayerTypeUnfadeable(int); + method public boolean isUidUnfadeable(int); + method public boolean isUsageFadeable(int); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.media.FadeManagerConfiguration> CREATOR; + field public static final long DURATION_NOT_SET = 0L; // 0x0L + field public static final int FADE_STATE_DISABLED = 0; // 0x0 + field public static final int FADE_STATE_ENABLED_AUTO = 2; // 0x2 + field public static final int FADE_STATE_ENABLED_DEFAULT = 1; // 0x1 + field public static final String TAG = "FadeManagerConfiguration"; + field public static final int VOLUME_SHAPER_SYSTEM_FADE_ID = 2; // 0x2 + } + + public static final class FadeManagerConfiguration.Builder { + ctor public FadeManagerConfiguration.Builder(); + ctor public FadeManagerConfiguration.Builder(long, long); + ctor public FadeManagerConfiguration.Builder(@NonNull android.media.FadeManagerConfiguration); + method @NonNull public android.media.FadeManagerConfiguration.Builder addFadeableUsage(int); + method @NonNull public android.media.FadeManagerConfiguration.Builder addUnfadeableAudioAttributes(@NonNull android.media.AudioAttributes); + method @NonNull public android.media.FadeManagerConfiguration.Builder addUnfadeableContentType(int); + method @NonNull public android.media.FadeManagerConfiguration.Builder addUnfadeableUid(int); + method @NonNull public android.media.FadeManagerConfiguration build(); + method @NonNull public android.media.FadeManagerConfiguration.Builder clearFadeableUsage(int); + method @NonNull public android.media.FadeManagerConfiguration.Builder clearUnfadeableAudioAttributes(@NonNull android.media.AudioAttributes); + method @NonNull public android.media.FadeManagerConfiguration.Builder clearUnfadeableContentType(int); + method @NonNull public android.media.FadeManagerConfiguration.Builder clearUnfadeableUid(int); + method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeInDelayForOffenders(long); + method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeInDurationForAudioAttributes(@NonNull android.media.AudioAttributes, long); + method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeInDurationForUsage(int, long); + method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeInVolumeShaperConfigForAudioAttributes(@NonNull android.media.AudioAttributes, @Nullable android.media.VolumeShaper.Configuration); + method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeInVolumeShaperConfigForUsage(int, @Nullable android.media.VolumeShaper.Configuration); + method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeOutDurationForAudioAttributes(@NonNull android.media.AudioAttributes, long); + method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeOutDurationForUsage(int, long); + method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeOutVolumeShaperConfigForAudioAttributes(@NonNull android.media.AudioAttributes, @Nullable android.media.VolumeShaper.Configuration); + method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeOutVolumeShaperConfigForUsage(int, @Nullable android.media.VolumeShaper.Configuration); + method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeState(int); + method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeableUsages(@NonNull java.util.List<java.lang.Integer>); + method @NonNull public android.media.FadeManagerConfiguration.Builder setUnfadeableAudioAttributes(@NonNull java.util.List<android.media.AudioAttributes>); + method @NonNull public android.media.FadeManagerConfiguration.Builder setUnfadeableContentTypes(@NonNull java.util.List<java.lang.Integer>); + method @NonNull public android.media.FadeManagerConfiguration.Builder setUnfadeableUids(@NonNull java.util.List<java.lang.Integer>); + } + public class HwAudioSource { method public boolean isPlaying(); method public void start(); @@ -6895,15 +6953,18 @@ package android.media.audiopolicy { public class AudioPolicy { method public int attachMixes(@NonNull java.util.List<android.media.audiopolicy.AudioMix>); + method @FlaggedApi("android.media.audiopolicy.enable_fade_manager_configuration") @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public int clearFadeManagerConfigurationForFocusLoss(); method public android.media.AudioRecord createAudioRecordSink(android.media.audiopolicy.AudioMix) throws java.lang.IllegalArgumentException; method public android.media.AudioTrack createAudioTrackSource(android.media.audiopolicy.AudioMix) throws java.lang.IllegalArgumentException; method public int detachMixes(@NonNull java.util.List<android.media.audiopolicy.AudioMix>); + method @FlaggedApi("android.media.audiopolicy.enable_fade_manager_configuration") @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public android.media.FadeManagerConfiguration getFadeManagerConfigurationForFocusLoss(); method public int getFocusDuckingBehavior(); method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioFocusInfo> getFocusStack(); method public int getStatus(); method public boolean removeUidDeviceAffinity(int); method public boolean removeUserIdDeviceAffinity(int); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean sendFocusLoss(@NonNull android.media.AudioFocusInfo) throws java.lang.IllegalStateException; + method @FlaggedApi("android.media.audiopolicy.enable_fade_manager_configuration") @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public int setFadeManagerConfigurationForFocusLoss(@NonNull android.media.FadeManagerConfiguration); method public int setFocusDuckingBehavior(int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException; method public void setRegistration(String); method public boolean setUidDeviceAffinity(int, @NonNull java.util.List<android.media.AudioDeviceInfo>); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 42daea24593e..aaeba66e4a40 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -369,11 +369,14 @@ package android.app { } public class NotificationManager { + method @FlaggedApi("android.app.modes_api") @NonNull public String addAutomaticZenRule(@NonNull android.app.AutomaticZenRule, boolean); method public void cleanUpCallersAfter(long); method public android.content.ComponentName getEffectsSuppressor(); method public boolean isNotificationPolicyAccessGrantedForPackage(@NonNull String); + method @FlaggedApi("android.app.modes_api") public boolean removeAutomaticZenRule(@NonNull String, boolean); method @RequiresPermission(android.Manifest.permission.MANAGE_NOTIFICATION_LISTENERS) public void setNotificationListenerAccessGranted(@NonNull android.content.ComponentName, boolean, boolean); method @RequiresPermission(android.Manifest.permission.MANAGE_TOAST_RATE_LIMITING) public void setToastRateLimitingEnabled(boolean); + method @FlaggedApi("android.app.modes_api") public boolean updateAutomaticZenRule(@NonNull String, @NonNull android.app.AutomaticZenRule, boolean); method public void updateNotificationChannel(@NonNull String, int, @NonNull android.app.NotificationChannel); } diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index ea9bb396f83c..37692d363a4b 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -537,8 +537,8 @@ public abstract class ActivityManagerInternal { /** * Returns whether the given user requires credential entry at this time. This is used to - * intercept activity launches for locked work apps due to work challenge being triggered or - * when the profile user is yet to be unlocked. + * intercept activity launches for apps corresponding to locked profiles due to separate + * challenge being triggered or when the profile user is yet to be unlocked. */ public abstract boolean shouldConfirmCredentials(@UserIdInt int userId); diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 94385717d349..b7db5f5d8b0c 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -167,7 +167,7 @@ interface INotificationManager void requestInterruptionFilterFromListener(in INotificationListener token, int interruptionFilter); int getInterruptionFilterFromListener(in INotificationListener token); void setOnNotificationPostedTrimFromListener(in INotificationListener token, int trim); - void setInterruptionFilter(String pkg, int interruptionFilter); + void setInterruptionFilter(String pkg, int interruptionFilter, boolean fromUser); void updateNotificationChannelGroupFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user, in NotificationChannelGroup group); void updateNotificationChannelFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user, in NotificationChannel channel); @@ -205,11 +205,11 @@ interface INotificationManager @UnsupportedAppUsage ZenModeConfig getZenModeConfig(); NotificationManager.Policy getConsolidatedNotificationPolicy(); - oneway void setZenMode(int mode, in Uri conditionId, String reason); + oneway void setZenMode(int mode, in Uri conditionId, String reason, boolean fromUser); oneway void notifyConditions(String pkg, in IConditionProvider provider, in Condition[] conditions); boolean isNotificationPolicyAccessGranted(String pkg); NotificationManager.Policy getNotificationPolicy(String pkg); - void setNotificationPolicy(String pkg, in NotificationManager.Policy policy); + void setNotificationPolicy(String pkg, in NotificationManager.Policy policy, boolean fromUser); boolean isNotificationPolicyAccessGrantedForPackage(String pkg); void setNotificationPolicyAccessGranted(String pkg, boolean granted); void setNotificationPolicyAccessGrantedForUser(String pkg, int userId, boolean granted); @@ -217,12 +217,12 @@ interface INotificationManager Map<String, AutomaticZenRule> getAutomaticZenRules(); // TODO: b/310620812 - Remove getZenRules() when MODES_API is inlined. List<ZenModeConfig.ZenRule> getZenRules(); - String addAutomaticZenRule(in AutomaticZenRule automaticZenRule, String pkg); - boolean updateAutomaticZenRule(String id, in AutomaticZenRule automaticZenRule); - boolean removeAutomaticZenRule(String id); - boolean removeAutomaticZenRules(String packageName); + String addAutomaticZenRule(in AutomaticZenRule automaticZenRule, String pkg, boolean fromUser); + boolean updateAutomaticZenRule(String id, in AutomaticZenRule automaticZenRule, boolean fromUser); + boolean removeAutomaticZenRule(String id, boolean fromUser); + boolean removeAutomaticZenRules(String packageName, boolean fromUser); int getRuleInstanceCount(in ComponentName owner); - void setAutomaticZenRuleState(String id, in Condition condition); + void setAutomaticZenRuleState(String id, in Condition condition, boolean fromUser); byte[] getBackupPayload(int user); void applyRestore(in byte[] payload, int user); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index a510c7704751..476232cb40b3 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -7733,11 +7733,12 @@ public class Notification implements Parcelable } else if (mPictureIcon.getType() == Icon.TYPE_BITMAP) { // If the icon contains a bitmap, use the old extra so that listeners which look // for that extra can still find the picture. Don't include the new extra in - // that case, to avoid duplicating data. + // that case, to avoid duplicating data. Leave the unused extra set to null to avoid + // crashing apps that came to expect it to be present but null. extras.putParcelable(EXTRA_PICTURE, mPictureIcon.getBitmap()); - extras.remove(EXTRA_PICTURE_ICON); + extras.putParcelable(EXTRA_PICTURE_ICON, null); } else { - extras.remove(EXTRA_PICTURE); + extras.putParcelable(EXTRA_PICTURE, null); extras.putParcelable(EXTRA_PICTURE_ICON, mPictureIcon); } } diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index d23b16d636a7..f76a45b37661 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -1184,14 +1184,20 @@ public class NotificationManager { */ @UnsupportedAppUsage public void setZenMode(int mode, Uri conditionId, String reason) { + setZenMode(mode, conditionId, reason, /* fromUser= */ false); + } + + /** @hide */ + public void setZenMode(int mode, Uri conditionId, String reason, boolean fromUser) { INotificationManager service = getService(); try { - service.setZenMode(mode, conditionId, reason); + service.setZenMode(mode, conditionId, reason, fromUser); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } + /** * @hide */ @@ -1325,9 +1331,19 @@ public class NotificationManager { * @return The id of the newly created rule; null if the rule could not be created. */ public String addAutomaticZenRule(AutomaticZenRule automaticZenRule) { + return addAutomaticZenRule(automaticZenRule, /* fromUser= */ false); + } + + /** @hide */ + @TestApi + @FlaggedApi(Flags.FLAG_MODES_API) + @NonNull + public String addAutomaticZenRule(@NonNull AutomaticZenRule automaticZenRule, + boolean fromUser) { INotificationManager service = getService(); try { - return service.addAutomaticZenRule(automaticZenRule, mContext.getPackageName()); + return service.addAutomaticZenRule(automaticZenRule, + mContext.getPackageName(), fromUser); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1347,9 +1363,17 @@ public class NotificationManager { * @return Whether the rule was successfully updated. */ public boolean updateAutomaticZenRule(String id, AutomaticZenRule automaticZenRule) { + return updateAutomaticZenRule(id, automaticZenRule, /* fromUser= */ false); + } + + /** @hide */ + @TestApi + @FlaggedApi(Flags.FLAG_MODES_API) + public boolean updateAutomaticZenRule(@NonNull String id, + @NonNull AutomaticZenRule automaticZenRule, boolean fromUser) { INotificationManager service = getService(); try { - return service.updateAutomaticZenRule(id, automaticZenRule); + return service.updateAutomaticZenRule(id, automaticZenRule, fromUser); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1367,9 +1391,20 @@ public class NotificationManager { * @param condition The new state of this rule */ public void setAutomaticZenRuleState(@NonNull String id, @NonNull Condition condition) { + if (Flags.modesApi()) { + setAutomaticZenRuleState(id, condition, + /* fromUser= */ condition.source == Condition.SOURCE_USER_ACTION); + } else { + setAutomaticZenRuleState(id, condition, /* fromUser= */ false); + } + } + + /** @hide */ + public void setAutomaticZenRuleState(@NonNull String id, @NonNull Condition condition, + boolean fromUser) { INotificationManager service = getService(); try { - service.setAutomaticZenRuleState(id, condition); + service.setAutomaticZenRuleState(id, condition, fromUser); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1388,9 +1423,16 @@ public class NotificationManager { * @return Whether the rule was successfully deleted. */ public boolean removeAutomaticZenRule(String id) { + return removeAutomaticZenRule(id, /* fromUser= */ false); + } + + /** @hide */ + @TestApi + @FlaggedApi(Flags.FLAG_MODES_API) + public boolean removeAutomaticZenRule(@NonNull String id, boolean fromUser) { INotificationManager service = getService(); try { - return service.removeAutomaticZenRule(id); + return service.removeAutomaticZenRule(id, fromUser); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1402,9 +1444,14 @@ public class NotificationManager { * @hide */ public boolean removeAutomaticZenRules(String packageName) { + return removeAutomaticZenRules(packageName, /* fromUser= */ false); + } + + /** @hide */ + public boolean removeAutomaticZenRules(String packageName, boolean fromUser) { INotificationManager service = getService(); try { - return service.removeAutomaticZenRules(packageName); + return service.removeAutomaticZenRules(packageName, fromUser); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1685,10 +1732,15 @@ public class NotificationManager { */ // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior. public void setNotificationPolicy(@NonNull Policy policy) { + setNotificationPolicy(policy, /* fromUser= */ false); + } + + /** @hide */ + public void setNotificationPolicy(@NonNull Policy policy, boolean fromUser) { checkRequired("policy", policy); INotificationManager service = getService(); try { - service.setNotificationPolicy(mContext.getOpPackageName(), policy); + service.setNotificationPolicy(mContext.getOpPackageName(), policy, fromUser); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -2685,9 +2737,16 @@ public class NotificationManager { */ // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior. public final void setInterruptionFilter(@InterruptionFilter int interruptionFilter) { + setInterruptionFilter(interruptionFilter, /* fromUser= */ false); + } + + /** @hide */ + public final void setInterruptionFilter(@InterruptionFilter int interruptionFilter, + boolean fromUser) { final INotificationManager service = getService(); try { - service.setInterruptionFilter(mContext.getOpPackageName(), interruptionFilter); + service.setInterruptionFilter(mContext.getOpPackageName(), interruptionFilter, + fromUser); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl b/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl index fac44b50024f..44942d67d489 100644 --- a/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl +++ b/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * 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. @@ -16,11 +16,11 @@ package android.companion.virtual.camera; import android.companion.virtual.camera.VirtualCameraStreamConfig; -import android.companion.virtual.camera.VirtualCameraMetadata; import android.view.Surface; /** - * Interface for the virtual camera service and system server to talk back to the virtual camera owner. + * Interface for the virtual camera service and system server to talk back to the virtual camera + * owner. * * @hide */ @@ -40,8 +40,7 @@ interface IVirtualCameraCallback { in VirtualCameraStreamConfig streamConfig); /** - * The client application is requesting a camera frame for the given streamId with the provided - * metadata. + * The client application is requesting a camera frame for the given streamId and frameId. * * <p>The virtual camera needs to write the frame data in the {@link Surface} corresponding to * this stream that was provided during the {@link #onStreamConfigured(int, Surface, @@ -52,16 +51,14 @@ interface IVirtualCameraCallback { * VirtualCameraStreamConfig)} * @param frameId The frameId that is being requested. Each request will have a different * frameId, that will be increasing for each call with a particular streamId. - * @param metadata The metadata requested for the frame. The virtual camera should do its best - * to honor the requested metadata. */ - oneway void onProcessCaptureRequest( - int streamId, long frameId, in VirtualCameraMetadata metadata); + oneway void onProcessCaptureRequest(int streamId, long frameId); /** * The stream previously configured when {@link #onStreamConfigured(int, Surface, * VirtualCameraStreamConfig)} was called is now being closed and associated resources can be - * freed. The Surface was disposed on the client side and should not be used anymore by the virtual camera owner + * freed. The Surface was disposed on the client side and should not be used anymore by the + * virtual camera owner. * * @param streamId The id of the stream that was closed. */ diff --git a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java index a18ae03555e9..5b658b883217 100644 --- a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java +++ b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * 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. @@ -18,7 +18,6 @@ package android.companion.virtual.camera; import android.annotation.FlaggedApi; import android.annotation.NonNull; -import android.annotation.Nullable; import android.annotation.SystemApi; import android.companion.virtual.flags.Flags; import android.view.Surface; @@ -50,8 +49,7 @@ public interface VirtualCameraCallback { @NonNull VirtualCameraStreamConfig streamConfig); /** - * The client application is requesting a camera frame for the given streamId with the provided - * metadata. + * The client application is requesting a camera frame for the given streamId and frameId. * * <p>The virtual camera needs to write the frame data in the {@link Surface} corresponding to * this stream that was provided during the {@link #onStreamConfigured(int, Surface, @@ -62,12 +60,8 @@ public interface VirtualCameraCallback { * VirtualCameraStreamConfig)} * @param frameId The frameId that is being requested. Each request will have a different * frameId, that will be increasing for each call with a particular streamId. - * @param metadata The metadata requested for the frame. The virtual camera should do its best - * to honor the requested metadata but the consumer won't be informed about the metadata set - * for a particular frame. If null, the requested frame can be anything the producer sends. */ - void onProcessCaptureRequest( - int streamId, long frameId, @Nullable VirtualCameraMetadata metadata); + default void onProcessCaptureRequest(int streamId, long frameId) {} /** * The stream previously configured when {@link #onStreamConfigured(int, Surface, diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java index f1eb240301e0..a9392518d0c6 100644 --- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java +++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * 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. @@ -224,9 +224,8 @@ public final class VirtualCameraConfig implements Parcelable { } @Override - public void onProcessCaptureRequest( - int streamId, long frameId, VirtualCameraMetadata metadata) { - mExecutor.execute(() -> mCallback.onProcessCaptureRequest(streamId, frameId, metadata)); + public void onProcessCaptureRequest(int streamId, long frameId) { + mExecutor.execute(() -> mCallback.onProcessCaptureRequest(streamId, frameId)); } @Override diff --git a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.java b/core/java/android/companion/virtual/camera/VirtualCameraMetadata.java deleted file mode 100644 index 1ba36d08cbeb..000000000000 --- a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 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. - */ - -package android.companion.virtual.camera; - -import android.annotation.FlaggedApi; -import android.annotation.NonNull; -import android.annotation.SystemApi; -import android.companion.virtual.flags.Flags; -import android.os.Parcel; -import android.os.Parcelable; - -/** - * Data structure used to store camera metadata compatible with VirtualCamera. - * - * @hide - */ -@SystemApi -@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA) -public final class VirtualCameraMetadata implements Parcelable { - - /** @hide */ - public VirtualCameraMetadata(@NonNull Parcel in) {} - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) {} - - @NonNull - public static final Creator<VirtualCameraMetadata> CREATOR = - new Creator<>() { - @Override - @NonNull - public VirtualCameraMetadata createFromParcel(Parcel in) { - return new VirtualCameraMetadata(in); - } - - @Override - @NonNull - public VirtualCameraMetadata[] newArray(int size) { - return new VirtualCameraMetadata[size]; - } - }; -} diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java index e198821e860e..1272f1683230 100644 --- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java +++ b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java @@ -24,6 +24,8 @@ import android.graphics.ImageFormat; import android.os.Parcel; import android.os.Parcelable; +import java.util.Objects; + /** * The configuration of a single virtual camera stream. * @@ -98,6 +100,19 @@ public final class VirtualCameraStreamConfig implements Parcelable { return mHeight; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + VirtualCameraStreamConfig that = (VirtualCameraStreamConfig) o; + return mWidth == that.mWidth && mHeight == that.mHeight && mFormat == that.mFormat; + } + + @Override + public int hashCode() { + return Objects.hash(mWidth, mHeight, mFormat); + } + /** Returns the {@link ImageFormat} of this stream. */ @ImageFormat.Format public int getFormat() { diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index b04b7badbae0..60d5c140e2b3 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -115,3 +115,11 @@ flag { description: "Feature flag to fix duplicated PackageManager flag values" bug: "314815969" } + +flag { + name: "provide_info_of_apk_in_apex" + namespace: "package_manager_service" + description: "Feature flag to provide the information of APK-in-APEX" + bug: "306329516" + is_fixed_read_only: true +} diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index f71e853a1170..ffd7212a7525 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -715,6 +715,14 @@ public abstract class DisplayManagerInternal { boolean startOffload(); void stopOffload(); + + /** + * Called when {@link DisplayOffloadSession} tries to block screen turning on. + * + * @param unblocker a {@link Runnable} executed upon all work required before screen turning + * on is done. + */ + void onBlockingScreenOn(Runnable unblocker); } /** A session token that associates a internal display with a {@link DisplayOffloader}. */ @@ -734,6 +742,15 @@ public abstract class DisplayManagerInternal { */ void updateBrightness(float brightness); + /** + * Called while display is turning to state ON to leave a small period for displayoffload + * session to finish some work. + * + * @param unblocker a {@link Runnable} used by displayoffload session to notify + * {@link DisplayManager} that it can continue turning screen on. + */ + boolean blockScreenOn(Runnable unblocker); + /** Returns whether displayoffload supports the given display state. */ static boolean isSupportedOffloadState(int displayState) { return Display.isSuspendedState(displayState); diff --git a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl b/core/java/android/hardware/face/FaceSensorConfigurations.aidl index 6c1f0fcd622a..26367b3c3dbf 100644 --- a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl +++ b/core/java/android/hardware/face/FaceSensorConfigurations.aidl @@ -13,12 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package android.hardware.face; -package android.companion.virtual.camera; - -/** - * Data structure used to store {@link android.hardware.camera2.CameraMetadata} compatible with - * VirtualCamera. - * @hide - */ -parcelable VirtualCameraMetadata; +parcelable FaceSensorConfigurations;
\ No newline at end of file diff --git a/core/java/android/hardware/face/FaceSensorConfigurations.java b/core/java/android/hardware/face/FaceSensorConfigurations.java new file mode 100644 index 000000000000..6ef692f81069 --- /dev/null +++ b/core/java/android/hardware/face/FaceSensorConfigurations.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 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. + */ + +package android.hardware.face; + +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; + +import android.annotation.Nullable; +import android.content.Context; +import android.hardware.biometrics.face.IFace; +import android.hardware.biometrics.face.SensorProps; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; +import android.util.Log; +import android.util.Pair; +import android.util.Slog; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +/** + * Provides the sensor props for face sensor, if available. + * @hide + */ +public class FaceSensorConfigurations implements Parcelable { + private static final String TAG = "FaceSensorConfigurations"; + + private final boolean mResetLockoutRequiresChallenge; + private final Map<String, SensorProps[]> mSensorPropsMap; + + public static final Creator<FaceSensorConfigurations> CREATOR = + new Creator<FaceSensorConfigurations>() { + @Override + public FaceSensorConfigurations createFromParcel(Parcel in) { + return new FaceSensorConfigurations(in); + } + + @Override + public FaceSensorConfigurations[] newArray(int size) { + return new FaceSensorConfigurations[size]; + } + }; + + public FaceSensorConfigurations(boolean resetLockoutRequiresChallenge) { + mResetLockoutRequiresChallenge = resetLockoutRequiresChallenge; + mSensorPropsMap = new HashMap<>(); + } + + protected FaceSensorConfigurations(Parcel in) { + mResetLockoutRequiresChallenge = in.readByte() != 0; + mSensorPropsMap = in.readHashMap(null, String.class, SensorProps[].class); + } + + /** + * Process AIDL instances to extract sensor props and add it to the sensor map. + * @param aidlInstances available face AIDL instances + * @param getIFace function that provides the daemon for the specific instance + */ + public void addAidlConfigs(@NonNull String[] aidlInstances, + @NonNull Function<String, IFace> getIFace) { + for (String aidlInstance : aidlInstances) { + final String fqName = IFace.DESCRIPTOR + "/" + aidlInstance; + IFace face = getIFace.apply(fqName); + try { + if (face != null) { + mSensorPropsMap.put(aidlInstance, face.getSensorProps()); + } else { + Slog.e(TAG, "Unable to get declared service: " + fqName); + } + } catch (RemoteException e) { + Log.d(TAG, "Unable to get sensor properties!"); + } + } + } + + /** + * Parse through HIDL configuration and add it to the sensor map. + */ + public void addHidlConfigs(@NonNull String[] hidlConfigStrings, + @NonNull Context context) { + final List<HidlFaceSensorConfig> hidlFaceSensorConfigs = new ArrayList<>(); + for (String hidlConfig: hidlConfigStrings) { + final HidlFaceSensorConfig hidlFaceSensorConfig = new HidlFaceSensorConfig(); + try { + hidlFaceSensorConfig.parse(hidlConfig, context); + } catch (Exception e) { + Log.e(TAG, "HIDL sensor configuration format is incorrect."); + continue; + } + if (hidlFaceSensorConfig.getModality() == TYPE_FACE) { + hidlFaceSensorConfigs.add(hidlFaceSensorConfig); + } + } + final String hidlHalInstanceName = "defaultHIDL"; + mSensorPropsMap.put(hidlHalInstanceName, hidlFaceSensorConfigs.toArray( + new SensorProps[hidlFaceSensorConfigs.size()])); + } + + /** + * Returns true if any face sensors have been added. + */ + public boolean hasSensorConfigurations() { + return mSensorPropsMap.size() > 0; + } + + /** + * Returns true if there is only a single face sensor configuration available. + */ + public boolean isSingleSensorConfigurationPresent() { + return mSensorPropsMap.size() == 1; + } + + /** + * Return sensor props for the given instance. If instance is not available, + * then null is returned. + */ + @Nullable + public Pair<String, SensorProps[]> getSensorPairForInstance(String instance) { + if (mSensorPropsMap.containsKey(instance)) { + return new Pair<>(instance, mSensorPropsMap.get(instance)); + } + + return null; + } + + /** + * Return the first pair of instance and sensor props, which does not correspond to the given + * If instance is not available, then null is returned. + */ + @Nullable + public Pair<String, SensorProps[]> getSensorPairNotForInstance(String instance) { + Optional<String> notAVirtualInstance = mSensorPropsMap.keySet().stream().filter( + (instanceName) -> !instanceName.equals(instance)).findFirst(); + return notAVirtualInstance.map(this::getSensorPairForInstance).orElseGet( + this::getSensorPair); + } + + /** + * Returns the first pair of instance and sensor props that has been added to the map. + */ + @Nullable + public Pair<String, SensorProps[]> getSensorPair() { + Optional<String> optionalInstance = mSensorPropsMap.keySet().stream().findFirst(); + return optionalInstance.map(this::getSensorPairForInstance).orElse(null); + + } + + public boolean getResetLockoutRequiresChallenge() { + return mResetLockoutRequiresChallenge; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeByte((byte) (mResetLockoutRequiresChallenge ? 1 : 0)); + dest.writeMap(mSensorPropsMap); + } +} diff --git a/core/java/android/hardware/face/HidlFaceSensorConfig.java b/core/java/android/hardware/face/HidlFaceSensorConfig.java new file mode 100644 index 000000000000..cab146d0ff54 --- /dev/null +++ b/core/java/android/hardware/face/HidlFaceSensorConfig.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 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. + */ + +package android.hardware.face; + +import android.annotation.NonNull; +import android.content.Context; +import android.hardware.biometrics.BiometricAuthenticator; +import android.hardware.biometrics.BiometricManager; +import android.hardware.biometrics.common.CommonProps; +import android.hardware.biometrics.common.SensorStrength; +import android.hardware.biometrics.face.SensorProps; + +import com.android.internal.R; + +/** + * Parse HIDL face sensor config and map it to SensorProps.aidl to match AIDL. + * See core/res/res/values/config.xml config_biometric_sensors + * @hide + */ +public final class HidlFaceSensorConfig extends SensorProps { + private int mSensorId; + private int mModality; + private int mStrength; + + /** + * Parse through the config string and map it to SensorProps.aidl. + * @throws IllegalArgumentException when config string has unexpected format + */ + public void parse(@NonNull String config, @NonNull Context context) + throws IllegalArgumentException { + final String[] elems = config.split(":"); + if (elems.length < 3) { + throw new IllegalArgumentException(); + } + mSensorId = Integer.parseInt(elems[0]); + mModality = Integer.parseInt(elems[1]); + mStrength = Integer.parseInt(elems[2]); + mapHidlToAidlFaceSensorConfigurations(context); + } + + @BiometricAuthenticator.Modality + public int getModality() { + return mModality; + } + + private void mapHidlToAidlFaceSensorConfigurations(@NonNull Context context) { + commonProps = new CommonProps(); + commonProps.sensorId = mSensorId; + commonProps.sensorStrength = authenticatorStrengthToPropertyStrength(mStrength); + halControlsPreview = context.getResources().getBoolean( + R.bool.config_faceAuthSupportsSelfIllumination); + commonProps.maxEnrollmentsPerUser = context.getResources().getInteger( + R.integer.config_faceMaxTemplatesPerUser); + commonProps.componentInfo = null; + supportsDetectInteraction = false; + } + + private byte authenticatorStrengthToPropertyStrength( + @BiometricManager.Authenticators.Types int strength) { + switch (strength) { + case BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE: + return SensorStrength.CONVENIENCE; + case BiometricManager.Authenticators.BIOMETRIC_WEAK: + return SensorStrength.WEAK; + case BiometricManager.Authenticators.BIOMETRIC_STRONG: + return SensorStrength.STRONG; + default: + throw new IllegalArgumentException("Unknown strength: " + strength); + } + } +} diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl index 7080133dc597..0096877f548a 100644 --- a/core/java/android/hardware/face/IFaceService.aidl +++ b/core/java/android/hardware/face/IFaceService.aidl @@ -26,6 +26,7 @@ import android.hardware.face.IFaceServiceReceiver; import android.hardware.face.Face; import android.hardware.face.FaceAuthenticateOptions; import android.hardware.face.FaceSensorPropertiesInternal; +import android.hardware.face.FaceSensorConfigurations; import android.view.Surface; /** @@ -167,6 +168,10 @@ interface IFaceService { @EnforcePermission("USE_BIOMETRIC_INTERNAL") void registerAuthenticators(in List<FaceSensorPropertiesInternal> hidlSensors); + //Register all available face sensors. + @EnforcePermission("USE_BIOMETRIC_INTERNAL") + void registerAuthenticatorsLegacy(in FaceSensorConfigurations faceSensorConfigurations); + // Adds a callback which gets called when the service registers all of the face // authenticators. The callback is automatically removed after it's invoked. void addAuthenticatorsRegisteredCallback(IFaceAuthenticatorsRegisteredCallback callback); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModel.kt b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.aidl index dca151db8b58..ebb05dc88182 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModel.kt +++ b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.aidl @@ -13,14 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package android.hardware.fingerprint; -package com.android.systemui.keyguard.ui.viewmodel - -import android.graphics.Rect -import javax.inject.Inject -import kotlinx.coroutines.ExperimentalCoroutinesApi - -@ExperimentalCoroutinesApi -class UdfpsKeyguardViewModel @Inject constructor() { - var sensorBounds: Rect = Rect() -} +parcelable FingerprintSensorConfigurations;
\ No newline at end of file diff --git a/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java new file mode 100644 index 000000000000..f214494a5d7b --- /dev/null +++ b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 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. + */ + +package android.hardware.fingerprint; + +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.hardware.biometrics.fingerprint.IFingerprint; +import android.hardware.biometrics.fingerprint.SensorProps; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; +import android.util.Log; +import android.util.Pair; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +/** + * Provides the sensor props for fingerprint sensor, if available. + * @hide + */ + +public class FingerprintSensorConfigurations implements Parcelable { + private static final String TAG = "FingerprintSensorConfigurations"; + + private final Map<String, SensorProps[]> mSensorPropsMap; + private final boolean mResetLockoutRequiresHardwareAuthToken; + + public static final Creator<FingerprintSensorConfigurations> CREATOR = + new Creator<>() { + @Override + public FingerprintSensorConfigurations createFromParcel(Parcel in) { + return new FingerprintSensorConfigurations(in); + } + + @Override + public FingerprintSensorConfigurations[] newArray(int size) { + return new FingerprintSensorConfigurations[size]; + } + }; + + public FingerprintSensorConfigurations(boolean resetLockoutRequiresHardwareAuthToken) { + mResetLockoutRequiresHardwareAuthToken = resetLockoutRequiresHardwareAuthToken; + mSensorPropsMap = new HashMap<>(); + } + + /** + * Process AIDL instances to extract sensor props and add it to the sensor map. + * @param aidlInstances available face AIDL instances + * @param getIFingerprint function that provides the daemon for the specific instance + */ + public void addAidlSensors(@NonNull String[] aidlInstances, + @NonNull Function<String, IFingerprint> getIFingerprint) { + for (String aidlInstance : aidlInstances) { + try { + final String fqName = IFingerprint.DESCRIPTOR + "/" + aidlInstance; + final IFingerprint fp = getIFingerprint.apply(fqName); + if (fp != null) { + SensorProps[] props = fp.getSensorProps(); + mSensorPropsMap.put(aidlInstance, props); + } else { + Log.d(TAG, "IFingerprint null for instance " + aidlInstance); + } + } catch (RemoteException e) { + Log.d(TAG, "Unable to get sensor properties!"); + } + } + } + + /** + * Parse through HIDL configuration and add it to the sensor map. + */ + public void addHidlSensors(@NonNull String[] hidlConfigStrings, + @NonNull Context context) { + final List<HidlFingerprintSensorConfig> hidlFingerprintSensorConfigs = new ArrayList<>(); + for (String hidlConfigString : hidlConfigStrings) { + final HidlFingerprintSensorConfig hidlFingerprintSensorConfig = + new HidlFingerprintSensorConfig(); + try { + hidlFingerprintSensorConfig.parse(hidlConfigString, context); + } catch (Exception e) { + Log.e(TAG, "HIDL sensor configuration format is incorrect."); + continue; + } + if (hidlFingerprintSensorConfig.getModality() == TYPE_FINGERPRINT) { + hidlFingerprintSensorConfigs.add(hidlFingerprintSensorConfig); + } + } + final String hidlHalInstanceName = "defaultHIDL"; + mSensorPropsMap.put(hidlHalInstanceName, + hidlFingerprintSensorConfigs.toArray( + new HidlFingerprintSensorConfig[hidlFingerprintSensorConfigs.size()])); + } + + protected FingerprintSensorConfigurations(Parcel in) { + mResetLockoutRequiresHardwareAuthToken = in.readByte() != 0; + mSensorPropsMap = in.readHashMap(null /* loader */, String.class, SensorProps[].class); + } + + /** + * Returns true if any fingerprint sensors have been added. + */ + public boolean hasSensorConfigurations() { + return mSensorPropsMap.size() > 0; + } + + /** + * Returns true if there is only a single fingerprint sensor configuration available. + */ + public boolean isSingleSensorConfigurationPresent() { + return mSensorPropsMap.size() == 1; + } + + /** + * Return sensor props for the given instance. If instance is not available, + * then null is returned. + */ + @Nullable + public Pair<String, SensorProps[]> getSensorPairForInstance(String instance) { + if (mSensorPropsMap.containsKey(instance)) { + return new Pair<>(instance, mSensorPropsMap.get(instance)); + } + + return null; + } + + /** + * Return the first pair of instance and sensor props, which does not correspond to the given + * If instance is not available, then null is returned. + */ + @Nullable + public Pair<String, SensorProps[]> getSensorPairNotForInstance(String instance) { + Optional<String> notAVirtualInstance = mSensorPropsMap.keySet().stream().filter( + (instanceName) -> !instanceName.equals(instance)).findFirst(); + return notAVirtualInstance.map(this::getSensorPairForInstance).orElseGet( + this::getSensorPair); + } + + /** + * Returns the first pair of instance and sensor props that has been added to the map. + */ + @Nullable + public Pair<String, SensorProps[]> getSensorPair() { + Optional<String> optionalInstance = mSensorPropsMap.keySet().stream().findFirst(); + return optionalInstance.map(this::getSensorPairForInstance).orElse(null); + + } + + public boolean getResetLockoutRequiresHardwareAuthToken() { + return mResetLockoutRequiresHardwareAuthToken; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeByte((byte) (mResetLockoutRequiresHardwareAuthToken ? 1 : 0)); + dest.writeMap(mSensorPropsMap); + } +} diff --git a/core/java/android/hardware/fingerprint/HidlFingerprintSensorConfig.java b/core/java/android/hardware/fingerprint/HidlFingerprintSensorConfig.java new file mode 100644 index 000000000000..d481153fc642 --- /dev/null +++ b/core/java/android/hardware/fingerprint/HidlFingerprintSensorConfig.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 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. + */ + +package android.hardware.fingerprint; + +import android.annotation.NonNull; +import android.content.Context; +import android.hardware.biometrics.BiometricAuthenticator; +import android.hardware.biometrics.BiometricManager; +import android.hardware.biometrics.common.CommonProps; +import android.hardware.biometrics.common.SensorStrength; +import android.hardware.biometrics.fingerprint.FingerprintSensorType; +import android.hardware.biometrics.fingerprint.SensorLocation; +import android.hardware.biometrics.fingerprint.SensorProps; + +import com.android.internal.R; +import com.android.internal.util.ArrayUtils; + +/** + * Parse HIDL fingerprint sensor config and map it to SensorProps.aidl to match AIDL. + * See core/res/res/values/config.xml config_biometric_sensors + * @hide + */ +public final class HidlFingerprintSensorConfig extends SensorProps { + private int mSensorId; + private int mModality; + private int mStrength; + + /** + * Parse through the config string and map it to SensorProps.aidl. + * @throws IllegalArgumentException when config string has unexpected format + */ + public void parse(@NonNull String config, @NonNull Context context) + throws IllegalArgumentException { + final String[] elems = config.split(":"); + if (elems.length < 3) { + throw new IllegalArgumentException(); + } + mSensorId = Integer.parseInt(elems[0]); + mModality = Integer.parseInt(elems[1]); + mStrength = Integer.parseInt(elems[2]); + mapHidlToAidlSensorConfiguration(context); + } + + @BiometricAuthenticator.Modality + public int getModality() { + return mModality; + } + + private void mapHidlToAidlSensorConfiguration(@NonNull Context context) { + commonProps = new CommonProps(); + commonProps.componentInfo = null; + commonProps.sensorId = mSensorId; + commonProps.sensorStrength = authenticatorStrengthToPropertyStrength(mStrength); + commonProps.maxEnrollmentsPerUser = context.getResources().getInteger( + R.integer.config_fingerprintMaxTemplatesPerUser); + halControlsIllumination = false; + sensorLocations = new SensorLocation[1]; + + final int[] udfpsProps = context.getResources().getIntArray( + com.android.internal.R.array.config_udfps_sensor_props); + final boolean isUdfps = !ArrayUtils.isEmpty(udfpsProps); + // config_is_powerbutton_fps indicates whether device has a power button fingerprint sensor. + final boolean isPowerbuttonFps = context.getResources().getBoolean( + R.bool.config_is_powerbutton_fps); + + if (isUdfps) { + sensorType = FingerprintSensorType.UNKNOWN; + } else if (isPowerbuttonFps) { + sensorType = FingerprintSensorType.POWER_BUTTON; + } else { + sensorType = FingerprintSensorType.REAR; + } + + if (isUdfps && udfpsProps.length == 3) { + setSensorLocation(udfpsProps[0], udfpsProps[1], udfpsProps[2]); + } else { + setSensorLocation(540 /* sensorLocationX */, 1636 /* sensorLocationY */, + 130 /* sensorRadius */); + } + + } + + private void setSensorLocation(int sensorLocationX, + int sensorLocationY, int sensorRadius) { + sensorLocations[0] = new SensorLocation(); + sensorLocations[0].display = ""; + sensorLocations[0].sensorLocationX = sensorLocationX; + sensorLocations[0].sensorLocationY = sensorLocationY; + sensorLocations[0].sensorRadius = sensorRadius; + } + + private byte authenticatorStrengthToPropertyStrength( + @BiometricManager.Authenticators.Types int strength) { + switch (strength) { + case BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE: + return SensorStrength.CONVENIENCE; + case BiometricManager.Authenticators.BIOMETRIC_WEAK: + return SensorStrength.WEAK; + case BiometricManager.Authenticators.BIOMETRIC_STRONG: + return SensorStrength.STRONG; + default: + throw new IllegalArgumentException("Unknown strength: " + strength); + } + } +} diff --git a/core/java/android/hardware/fingerprint/IFingerprintService.aidl b/core/java/android/hardware/fingerprint/IFingerprintService.aidl index f594c00b0e47..606b171f36ba 100644 --- a/core/java/android/hardware/fingerprint/IFingerprintService.aidl +++ b/core/java/android/hardware/fingerprint/IFingerprintService.aidl @@ -31,6 +31,7 @@ import android.hardware.fingerprint.ISidefpsController; import android.hardware.fingerprint.Fingerprint; import android.hardware.fingerprint.FingerprintAuthenticateOptions; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.hardware.fingerprint.FingerprintSensorConfigurations; import java.util.List; /** @@ -173,6 +174,10 @@ interface IFingerprintService { @EnforcePermission("MANAGE_FINGERPRINT") void removeClientActiveCallback(IFingerprintClientActiveCallback callback); + //Register all available fingerprint sensors. + @EnforcePermission("USE_BIOMETRIC_INTERNAL") + void registerAuthenticatorsLegacy(in FingerprintSensorConfigurations fingerprintSensorConfigurations); + // Registers all HIDL and AIDL sensors. Only HIDL sensor properties need to be provided, because // AIDL sensor properties are retrieved directly from the available HALs. If no HIDL HALs exist, // hidlSensors must be non-null and empty. See AuthService.java diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 4bfff16d973f..7d00b80488a9 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -6911,7 +6911,11 @@ public final class ContactsContract { * <td></td> * </tr> * </table> + * + * @deprecated This field may not be well supported by some contacts apps and is discouraged + * to use. */ + @Deprecated public static final class Im implements DataColumnsWithJoins, CommonColumns, ContactCounts { /** * This utility class cannot be instantiated @@ -7721,7 +7725,11 @@ public final class ContactsContract { * <td></td> * </tr> * </table> + * + * @deprecated This field may not be well supported by some contacts apps and is discouraged + * to use. */ + @Deprecated public static final class SipAddress implements DataColumnsWithJoins, CommonColumns, ContactCounts { /** diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java index 875031fb0cb3..23c8393dadc3 100644 --- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java +++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java @@ -960,8 +960,8 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { mKeyphraseMetadata = new KeyphraseMetadata(1, mText, fakeSupportedLocales, AlwaysOnHotwordDetector.RECOGNITION_MODE_VOICE_TRIGGER); } + notifyStateChangedLocked(); } - notifyStateChanged(availability); } /** @@ -1371,8 +1371,8 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { mAvailability = STATE_INVALID; mIsAvailabilityOverriddenByTestApi = false; + notifyStateChangedLocked(); } - notifyStateChanged(STATE_INVALID); super.destroy(); } @@ -1402,8 +1402,6 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { */ // TODO(b/281608561): remove the enrollment flow from AlwaysOnHotwordDetector void onSoundModelsChanged() { - boolean notifyError = false; - synchronized (mLock) { if (mAvailability == STATE_INVALID || mAvailability == STATE_HARDWARE_UNAVAILABLE @@ -1444,9 +1442,6 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { // calling stopRecognition where there is no started session. Log.w(TAG, "Failed to stop recognition after enrollment update: code=" + result); - - // Execute a refresh availability task - which should then notify of a change. - new RefreshAvailabilityTask().execute(); } catch (Exception e) { Slog.w(TAG, "Failed to stop recognition after enrollment update", e); if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) { @@ -1455,14 +1450,14 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { + Log.getStackTraceString(e), FailureSuggestedAction.RECREATE_DETECTOR)); } else { - notifyError = true; + updateAndNotifyStateChangedLocked(STATE_ERROR); } + return; } } - } - if (notifyError) { - updateAndNotifyStateChanged(STATE_ERROR); + // Execute a refresh availability task - which should then notify of a change. + new RefreshAvailabilityTask().execute(); } } @@ -1578,11 +1573,10 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { } } - private void updateAndNotifyStateChanged(int availability) { - synchronized (mLock) { - updateAvailabilityLocked(availability); - } - notifyStateChanged(availability); + @GuardedBy("mLock") + private void updateAndNotifyStateChangedLocked(int availability) { + updateAvailabilityLocked(availability); + notifyStateChangedLocked(); } @GuardedBy("mLock") @@ -1596,17 +1590,17 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { } } - private void notifyStateChanged(int newAvailability) { + @GuardedBy("mLock") + private void notifyStateChangedLocked() { Message message = Message.obtain(mHandler, MSG_AVAILABILITY_CHANGED); - message.arg1 = newAvailability; + message.arg1 = mAvailability; message.sendToTarget(); } + @GuardedBy("mLock") private void sendUnknownFailure(String failureMessage) { - synchronized (mLock) { - // update but do not call onAvailabilityChanged callback for STATE_ERROR - updateAvailabilityLocked(STATE_ERROR); - } + // update but do not call onAvailabilityChanged callback for STATE_ERROR + updateAvailabilityLocked(STATE_ERROR); Message.obtain(mHandler, MSG_DETECTION_UNKNOWN_FAILURE, failureMessage).sendToTarget(); } @@ -1822,17 +1816,19 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { availability = STATE_KEYPHRASE_UNENROLLED; } } + updateAndNotifyStateChangedLocked(availability); } - updateAndNotifyStateChanged(availability); } catch (Exception e) { // Any exception here not caught will crash the process because AsyncTask does not // bubble up the exceptions to the client app, so we must propagate it to the app. Slog.w(TAG, "Failed to refresh availability", e); - if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) { - sendUnknownFailure( - "Failed to refresh availability: " + Log.getStackTraceString(e)); - } else { - updateAndNotifyStateChanged(STATE_ERROR); + synchronized (mLock) { + if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) { + sendUnknownFailure( + "Failed to refresh availability: " + Log.getStackTraceString(e)); + } else { + updateAndNotifyStateChangedLocked(STATE_ERROR); + } } } diff --git a/core/java/android/service/voice/VisualQueryDetector.java b/core/java/android/service/voice/VisualQueryDetector.java index b7d97057a08b..adc54f5b5a8c 100644 --- a/core/java/android/service/voice/VisualQueryDetector.java +++ b/core/java/android/service/voice/VisualQueryDetector.java @@ -94,7 +94,9 @@ public class VisualQueryDetector { */ public void updateState(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory) { - mInitializationDelegate.updateState(options, sharedMemory); + synchronized (mInitializationDelegate.getLock()) { + mInitializationDelegate.updateState(options, sharedMemory); + } } @@ -116,18 +118,21 @@ public class VisualQueryDetector { if (DEBUG) { Slog.i(TAG, "#startRecognition"); } - // check if the detector is active with the initialization delegate - mInitializationDelegate.startRecognition(); - - try { - mManagerService.startPerceiving(new BinderCallback(mExecutor, mCallback)); - } catch (SecurityException e) { - Slog.e(TAG, "startRecognition failed: " + e); - return false; - } catch (RemoteException e) { - e.rethrowFromSystemServer(); + synchronized (mInitializationDelegate.getLock()) { + // check if the detector is active with the initialization delegate + mInitializationDelegate.startRecognition(); + + try { + mManagerService.startPerceiving(new BinderCallback( + mExecutor, mCallback, mInitializationDelegate.getLock())); + } catch (SecurityException e) { + Slog.e(TAG, "startRecognition failed: " + e); + return false; + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + return true; } - return true; } /** @@ -140,15 +145,17 @@ public class VisualQueryDetector { if (DEBUG) { Slog.i(TAG, "#stopRecognition"); } - // check if the detector is active with the initialization delegate - mInitializationDelegate.startRecognition(); - - try { - mManagerService.stopPerceiving(); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); + synchronized (mInitializationDelegate.getLock()) { + // check if the detector is active with the initialization delegate + mInitializationDelegate.stopRecognition(); + + try { + mManagerService.stopPerceiving(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + return true; } - return true; } /** @@ -160,12 +167,16 @@ public class VisualQueryDetector { if (DEBUG) { Slog.i(TAG, "#destroy"); } - mInitializationDelegate.destroy(); + synchronized (mInitializationDelegate.getLock()) { + mInitializationDelegate.destroy(); + } } /** @hide */ public void dump(String prefix, PrintWriter pw) { - // TODO: implement this + synchronized (mInitializationDelegate.getLock()) { + mInitializationDelegate.dump(prefix, pw); + } } /** @hide */ @@ -175,7 +186,9 @@ public class VisualQueryDetector { /** @hide */ void registerOnDestroyListener(Consumer<AbstractDetector> onDestroyListener) { - mInitializationDelegate.registerOnDestroyListener(onDestroyListener); + synchronized (mInitializationDelegate.getLock()) { + mInitializationDelegate.registerOnDestroyListener(onDestroyListener); + } } /** @@ -282,6 +295,15 @@ public class VisualQueryDetector { public boolean isUsingSandboxedDetectionService() { return true; } + + @Override + public void dump(String prefix, PrintWriter pw) { + // No-op + } + + private Object getLock() { + return mLock; + } } private static class BinderCallback @@ -289,31 +311,43 @@ public class VisualQueryDetector { private final Executor mExecutor; private final VisualQueryDetector.Callback mCallback; - BinderCallback(Executor executor, VisualQueryDetector.Callback callback) { + private final Object mLock; + + BinderCallback(Executor executor, VisualQueryDetector.Callback callback, Object lock) { this.mExecutor = executor; this.mCallback = callback; + this.mLock = lock; } /** Called when the detected result is valid. */ @Override public void onQueryDetected(@NonNull String partialQuery) { Slog.v(TAG, "BinderCallback#onQueryDetected"); - Binder.withCleanCallingIdentity(() -> mExecutor.execute( - () -> mCallback.onQueryDetected(partialQuery))); + Binder.withCleanCallingIdentity(() -> { + synchronized (mLock) { + mCallback.onQueryDetected(partialQuery); + } + }); } @Override public void onQueryFinished() { Slog.v(TAG, "BinderCallback#onQueryFinished"); - Binder.withCleanCallingIdentity(() -> mExecutor.execute( - () -> mCallback.onQueryFinished())); + Binder.withCleanCallingIdentity(() -> { + synchronized (mLock) { + mCallback.onQueryFinished(); + } + }); } @Override public void onQueryRejected() { Slog.v(TAG, "BinderCallback#onQueryRejected"); - Binder.withCleanCallingIdentity(() -> mExecutor.execute( - () -> mCallback.onQueryRejected())); + Binder.withCleanCallingIdentity(() -> { + synchronized (mLock) { + mCallback.onQueryRejected(); + } + }); } /** Called when the detection fails due to an error. */ diff --git a/core/java/android/view/DisplayEventReceiver.java b/core/java/android/view/DisplayEventReceiver.java index 31d759ea92e6..18080e4478fc 100644 --- a/core/java/android/view/DisplayEventReceiver.java +++ b/core/java/android/view/DisplayEventReceiver.java @@ -287,6 +287,16 @@ public abstract class DisplayEventReceiver { } /** + * Called when a display hdcp levels change event is received. + * + * @param physicalDisplayId Stable display ID that uniquely describes a (display, port) pair. + * @param connectedLevel the new connected HDCP level + * @param maxLevel the maximum HDCP level + */ + public void onHdcpLevelsChanged(long physicalDisplayId, int connectedLevel, int maxLevel) { + } + + /** * Represents a mapping between a UID and an override frame rate */ public static class FrameRateOverride { @@ -374,4 +384,11 @@ public abstract class DisplayEventReceiver { onFrameRateOverridesChanged(timestampNanos, physicalDisplayId, overrides); } + // Called from native code. + @SuppressWarnings("unused") + private void dispatchHdcpLevelsChanged(long physicalDisplayId, int connectedLevel, + int maxLevel) { + onHdcpLevelsChanged(physicalDisplayId, connectedLevel, maxLevel); + } + } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index e83488e2689e..9f6395e1aab4 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -528,8 +528,6 @@ public final class ViewRootImpl implements ViewParent, // Set to true to stop input during an Activity Transition. boolean mPausedForTransition = false; - boolean mLastInCompatMode = false; - SurfaceHolder.Callback2 mSurfaceHolderCallback; BaseSurfaceHolder mSurfaceHolder; boolean mIsCreating; @@ -1375,11 +1373,6 @@ public final class ViewRootImpl implements ViewParent, } if (DEBUG_LAYOUT) Log.d(mTag, "WindowLayout in setView:" + attrs); - if (!compatibilityInfo.supportsScreen()) { - attrs.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW; - mLastInCompatMode = true; - } - mSoftInputMode = attrs.softInputMode; mWindowAttributesChanged = true; mAttachInfo.mRootView = view; @@ -1913,10 +1906,6 @@ public final class ViewRootImpl implements ViewParent, // Keep track of the actual window flags supplied by the client. mClientWindowLayoutFlags = attrs.flags; - // Preserve compatible window flag if exists. - final int compatibleWindowFlag = mWindowAttributes.privateFlags - & WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW; - // Preserve system UI visibility. final int systemUiVisibility = mWindowAttributes.systemUiVisibility; final int subtreeSystemUiVisibility = mWindowAttributes.subtreeSystemUiVisibility; @@ -1948,8 +1937,7 @@ public final class ViewRootImpl implements ViewParent, mWindowAttributes.subtreeSystemUiVisibility = subtreeSystemUiVisibility; mWindowAttributes.insetsFlags.appearance = appearance; mWindowAttributes.insetsFlags.behavior = behavior; - mWindowAttributes.privateFlags |= compatibleWindowFlag - | appearanceAndBehaviorPrivateFlags; + mWindowAttributes.privateFlags |= appearanceAndBehaviorPrivateFlags; if (mWindowAttributes.preservePreviousSurfaceInsets) { // Restore old surface insets. @@ -3150,21 +3138,6 @@ public final class ViewRootImpl implements ViewParent, final boolean shouldOptimizeMeasure = shouldOptimizeMeasure(lp); WindowManager.LayoutParams params = null; - CompatibilityInfo compatibilityInfo = - mDisplay.getDisplayAdjustments().getCompatibilityInfo(); - if (compatibilityInfo.supportsScreen() == mLastInCompatMode) { - params = lp; - mFullRedrawNeeded = true; - mLayoutRequested = true; - if (mLastInCompatMode) { - params.privateFlags &= ~WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW; - mLastInCompatMode = false; - } else { - params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW; - mLastInCompatMode = true; - } - } - Rect frame = mWinFrame; if (mFirst) { mFullRedrawNeeded = true; diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 1712fd3c3323..07a347ace313 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -3143,13 +3143,6 @@ public interface WindowManager extends ViewManager { @UnsupportedAppUsage public static final int PRIVATE_FLAG_NO_MOVE_ANIMATION = 1 << 6; - /** Window flag: special flag to limit the size of the window to be - * original size ([320x480] x density). Used to create window for applications - * running under compatibility mode. - * - * {@hide} */ - public static final int PRIVATE_FLAG_COMPATIBLE_WINDOW = 1 << 7; - /** Window flag: a special option intended for system dialogs. When * this flag is set, the window will demand focus unconditionally when * it is created. @@ -3345,7 +3338,6 @@ public interface WindowManager extends ViewManager { SYSTEM_FLAG_SHOW_FOR_ALL_USERS, PRIVATE_FLAG_UNRESTRICTED_GESTURE_EXCLUSION, PRIVATE_FLAG_NO_MOVE_ANIMATION, - PRIVATE_FLAG_COMPATIBLE_WINDOW, PRIVATE_FLAG_SYSTEM_ERROR, PRIVATE_FLAG_OPTIMIZE_MEASURE, PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS, @@ -3399,10 +3391,6 @@ public interface WindowManager extends ViewManager { equals = PRIVATE_FLAG_NO_MOVE_ANIMATION, name = "NO_MOVE_ANIMATION"), @ViewDebug.FlagToString( - mask = PRIVATE_FLAG_COMPATIBLE_WINDOW, - equals = PRIVATE_FLAG_COMPATIBLE_WINDOW, - name = "COMPATIBLE_WINDOW"), - @ViewDebug.FlagToString( mask = PRIVATE_FLAG_SYSTEM_ERROR, equals = PRIVATE_FLAG_SYSTEM_ERROR, name = "SYSTEM_ERROR"), diff --git a/core/java/android/window/BackMotionEvent.java b/core/java/android/window/BackMotionEvent.java index c47572313eeb..7cda3a36da95 100644 --- a/core/java/android/window/BackMotionEvent.java +++ b/core/java/android/window/BackMotionEvent.java @@ -36,6 +36,7 @@ public final class BackMotionEvent implements Parcelable { private final float mProgress; private final float mVelocityX; private final float mVelocityY; + private final boolean mTriggerBack; @BackEvent.SwipeEdge private final int mSwipeEdge; @@ -54,6 +55,7 @@ public final class BackMotionEvent implements Parcelable { * Value in pixels/second. {@link Float#NaN} if was not computed. * @param velocityY Y velocity computed from the touch point of this event. * Value in pixels/second. {@link Float#NaN} if was not computed. + * @param triggerBack Indicates whether the back arrow is in the triggered state or not * @param swipeEdge Indicates which edge the swipe starts from. * @param departingAnimationTarget The remote animation target of the departing * application window. @@ -64,6 +66,7 @@ public final class BackMotionEvent implements Parcelable { float progress, float velocityX, float velocityY, + boolean triggerBack, @BackEvent.SwipeEdge int swipeEdge, @Nullable RemoteAnimationTarget departingAnimationTarget) { mTouchX = touchX; @@ -71,6 +74,7 @@ public final class BackMotionEvent implements Parcelable { mProgress = progress; mVelocityX = velocityX; mVelocityY = velocityY; + mTriggerBack = triggerBack; mSwipeEdge = swipeEdge; mDepartingAnimationTarget = departingAnimationTarget; } @@ -81,6 +85,7 @@ public final class BackMotionEvent implements Parcelable { mProgress = in.readFloat(); mVelocityX = in.readFloat(); mVelocityY = in.readFloat(); + mTriggerBack = in.readBoolean(); mSwipeEdge = in.readInt(); mDepartingAnimationTarget = in.readTypedObject(RemoteAnimationTarget.CREATOR); } @@ -110,6 +115,7 @@ public final class BackMotionEvent implements Parcelable { dest.writeFloat(mProgress); dest.writeFloat(mVelocityX); dest.writeFloat(mVelocityY); + dest.writeBoolean(mTriggerBack); dest.writeInt(mSwipeEdge); dest.writeTypedObject(mDepartingAnimationTarget, flags); } @@ -157,6 +163,15 @@ public final class BackMotionEvent implements Parcelable { } /** + * Returns whether the back arrow is in the triggered state or not + * + * @return boolean indicating whether the back arrow is in the triggered state or not + */ + public boolean getTriggerBack() { + return mTriggerBack; + } + + /** * Returns the screen edge that the swipe starts from. */ @BackEvent.SwipeEdge @@ -182,6 +197,7 @@ public final class BackMotionEvent implements Parcelable { + ", mProgress=" + mProgress + ", mVelocityX=" + mVelocityX + ", mVelocityY=" + mVelocityY + + ", mTriggerBack=" + mTriggerBack + ", mSwipeEdge" + mSwipeEdge + ", mDepartingAnimationTarget" + mDepartingAnimationTarget + "}"; diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 52ad49a7cf28..216acdce38fb 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -66,4 +66,12 @@ flag { description: "Predictive back for system animations" bug: "309545085" is_fixed_read_only: true +} + +flag { + name: "activity_snapshot_by_default" + namespace: "systemui" + description: "Enable record activity snapshot by default" + bug: "259497289" + is_fixed_read_only: true }
\ No newline at end of file diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index c24d21dda68c..17aad43edb6b 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -653,6 +653,8 @@ int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote, bool p sizeof("-Xps-save-resolved-classes-delay-ms:")-1 + PROPERTY_VALUE_MAX]; char profileMinSavePeriodOptsBuf[sizeof("-Xps-min-save-period-ms:")-1 + PROPERTY_VALUE_MAX]; char profileMinFirstSaveOptsBuf[sizeof("-Xps-min-first-save-ms:") - 1 + PROPERTY_VALUE_MAX]; + char profileInlineCacheThresholdOptsBuf[ + sizeof("-Xps-inline-cache-threshold:") - 1 + PROPERTY_VALUE_MAX]; char madviseWillNeedFileSizeVdex[ sizeof("-XMadviseWillNeedVdexFileSize:")-1 + PROPERTY_VALUE_MAX]; char madviseWillNeedFileSizeOdex[ @@ -898,6 +900,9 @@ int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote, bool p parseRuntimeOption("dalvik.vm.ps-min-first-save-ms", profileMinFirstSaveOptsBuf, "-Xps-min-first-save-ms:"); + parseRuntimeOption("dalvik.vm.ps-inline-cache-threshold", profileInlineCacheThresholdOptsBuf, + "-Xps-inline-cache-threshold:"); + property_get("ro.config.low_ram", propBuf, ""); if (strcmp(propBuf, "true") == 0) { addOption("-XX:LowMemoryMode"); diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp index fef8ad7499f7..f007cc5a23bd 100644 --- a/core/jni/android_view_DisplayEventReceiver.cpp +++ b/core/jni/android_view_DisplayEventReceiver.cpp @@ -41,6 +41,7 @@ static struct { jmethodID dispatchHotplugConnectionError; jmethodID dispatchModeChanged; jmethodID dispatchFrameRateOverrides; + jmethodID dispatchHdcpLevelsChanged; struct { jclass clazz; @@ -96,6 +97,8 @@ private: void dispatchFrameRateOverrides(nsecs_t timestamp, PhysicalDisplayId displayId, std::vector<FrameRateOverride> overrides) override; void dispatchNullEvent(nsecs_t timestamp, PhysicalDisplayId displayId) override {} + void dispatchHdcpLevelsChanged(PhysicalDisplayId displayId, int connectedLevel, + int maxLevel) override; }; NativeDisplayEventReceiver::NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak, @@ -294,6 +297,22 @@ void NativeDisplayEventReceiver::dispatchFrameRateOverrides( mMessageQueue->raiseAndClearException(env, "dispatchModeChanged"); } +void NativeDisplayEventReceiver::dispatchHdcpLevelsChanged(PhysicalDisplayId displayId, + int connectedLevel, int maxLevel) { + JNIEnv* env = AndroidRuntime::getJNIEnv(); + + ScopedLocalRef<jobject> receiverObj(env, GetReferent(env, mReceiverWeakGlobal)); + if (receiverObj.get()) { + ALOGV("receiver %p ~ Invoking hdcp levels changed handler.", this); + env->CallVoidMethod(receiverObj.get(), + gDisplayEventReceiverClassInfo.dispatchHdcpLevelsChanged, + displayId.value, connectedLevel, maxLevel); + ALOGV("receiver %p ~ Returned from hdcp levels changed handler.", this); + } + + mMessageQueue->raiseAndClearException(env, "dispatchHdcpLevelsChanged"); +} + static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak, jobject vsyncEventDataWeak, jobject messageQueueObj, jint vsyncSource, jint eventRegistration, jlong layerHandle) { @@ -385,6 +404,9 @@ int register_android_view_DisplayEventReceiver(JNIEnv* env) { GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz, "dispatchFrameRateOverrides", "(JJ[Landroid/view/DisplayEventReceiver$FrameRateOverride;)V"); + gDisplayEventReceiverClassInfo.dispatchHdcpLevelsChanged = + GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz, "dispatchHdcpLevelsChanged", + "(JII)V"); jclass frameRateOverrideClazz = FindClassOrDie(env, "android/view/DisplayEventReceiver$FrameRateOverride"); diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto index c5889ba78159..75cfba02120e 100644 --- a/core/proto/android/server/activitymanagerservice.proto +++ b/core/proto/android/server/activitymanagerservice.proto @@ -429,6 +429,7 @@ message ServiceRecordProto { optional string base_dir = 1; optional string res_dir = 2; optional string data_dir = 3; + optional int32 targetSdkVersion = 4; } optional AppInfo appinfo = 8; optional ProcessRecordProto app = 9; diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index 1577d9c1c1ab..5b0502da1bdf 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -1244,29 +1244,33 @@ public class NotificationTest { } @Test - public void testBigPictureStyle_setExtras_pictureIconNull_noPictureIconKey() { + public void testBigPictureStyle_setExtras_pictureIconNull_pictureIconKeyNull() { Notification.BigPictureStyle bpStyle = new Notification.BigPictureStyle(); bpStyle.bigPicture((Bitmap) null); Bundle extras = new Bundle(); bpStyle.addExtras(extras); - assertThat(extras.containsKey(EXTRA_PICTURE_ICON)).isFalse(); + assertThat(extras.containsKey(EXTRA_PICTURE_ICON)).isTrue(); + final Parcelable pictureIcon = extras.getParcelable(EXTRA_PICTURE_ICON); + assertThat(pictureIcon).isNull(); } @Test - public void testBigPictureStyle_setExtras_pictureIconNull_noPictureKey() { + public void testBigPictureStyle_setExtras_pictureIconNull_pictureKeyNull() { Notification.BigPictureStyle bpStyle = new Notification.BigPictureStyle(); bpStyle.bigPicture((Bitmap) null); Bundle extras = new Bundle(); bpStyle.addExtras(extras); - assertThat(extras.containsKey(EXTRA_PICTURE)).isFalse(); + assertThat(extras.containsKey(EXTRA_PICTURE)).isTrue(); + final Parcelable picture = extras.getParcelable(EXTRA_PICTURE); + assertThat(picture).isNull(); } @Test - public void testBigPictureStyle_setExtras_pictureIconTypeBitmap_noPictureIconKey() { + public void testBigPictureStyle_setExtras_pictureIconTypeBitmap_pictureIconKeyNull() { Bitmap bitmap = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888); Notification.BigPictureStyle bpStyle = new Notification.BigPictureStyle(); bpStyle.bigPicture(bitmap); @@ -1274,11 +1278,13 @@ public class NotificationTest { Bundle extras = new Bundle(); bpStyle.addExtras(extras); - assertThat(extras.containsKey(EXTRA_PICTURE_ICON)).isFalse(); + assertThat(extras.containsKey(EXTRA_PICTURE_ICON)).isTrue(); + final Parcelable pictureIcon = extras.getParcelable(EXTRA_PICTURE_ICON); + assertThat(pictureIcon).isNull(); } @Test - public void testBigPictureStyle_setExtras_pictureIconTypeIcon_noPictureKey() { + public void testBigPictureStyle_setExtras_pictureIconTypeIcon_pictureKeyNull() { Icon icon = Icon.createWithResource(mContext, R.drawable.btn_plus); Notification.BigPictureStyle bpStyle = new Notification.BigPictureStyle(); bpStyle.bigPicture(icon); @@ -1286,7 +1292,9 @@ public class NotificationTest { Bundle extras = new Bundle(); bpStyle.addExtras(extras); - assertThat(extras.containsKey(EXTRA_PICTURE)).isFalse(); + assertThat(extras.containsKey(EXTRA_PICTURE)).isTrue(); + final Parcelable picture = extras.getParcelable(EXTRA_PICTURE); + assertThat(picture).isNull(); } @Test diff --git a/core/tests/coretests/src/android/hardware/face/FaceSensorConfigurationsTest.java b/core/tests/coretests/src/android/hardware/face/FaceSensorConfigurationsTest.java new file mode 100644 index 000000000000..da3a465ade7e --- /dev/null +++ b/core/tests/coretests/src/android/hardware/face/FaceSensorConfigurationsTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 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. + */ + +package android.hardware.face; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; +import android.hardware.biometrics.face.IFace; +import android.hardware.biometrics.face.SensorProps; +import android.os.RemoteException; +import android.platform.test.annotations.Presubmit; +import android.test.suitebuilder.annotation.SmallTest; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.function.Function; + +@Presubmit +@SmallTest +public class FaceSensorConfigurationsTest { + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + @Mock + private Context mContext; + @Mock + private Resources mResources; + @Mock + private IFace mFace; + @Mock + private Function<String, IFace> mGetIFace; + + private final String[] mAidlInstances = new String[]{"default", "virtual"}; + private String[] mHidlConfigStrings = new String[]{"0:2:15", "0:8:15"}; + private FaceSensorConfigurations mFaceSensorConfigurations; + + @Before + public void setUp() throws RemoteException { + when(mGetIFace.apply(anyString())).thenReturn(mFace); + when(mFace.getSensorProps()).thenReturn(new SensorProps[]{}); + when(mContext.getResources()).thenReturn(mResources); + } + + @Test + public void testAidlInstanceSensorProps() { + mFaceSensorConfigurations = new FaceSensorConfigurations(false); + mFaceSensorConfigurations.addAidlConfigs(mAidlInstances, mGetIFace); + + assertThat(mFaceSensorConfigurations.hasSensorConfigurations()).isTrue(); + assertThat(!mFaceSensorConfigurations.isSingleSensorConfigurationPresent()).isTrue(); + assertThat(mFaceSensorConfigurations.getResetLockoutRequiresChallenge()) + .isFalse(); + } + + @Test + public void testHidlConfigStrings() { + mFaceSensorConfigurations = new FaceSensorConfigurations(true); + mFaceSensorConfigurations.addHidlConfigs(mHidlConfigStrings, mContext); + + assertThat(mFaceSensorConfigurations.isSingleSensorConfigurationPresent()).isTrue(); + assertThat(mFaceSensorConfigurations.getResetLockoutRequiresChallenge()) + .isTrue(); + } + + @Test + public void testHidlConfigStrings_incorrectFormat() { + mHidlConfigStrings = new String[]{"0:8:15", "0:2", "0:face:15"}; + mFaceSensorConfigurations = new FaceSensorConfigurations(true); + mFaceSensorConfigurations.addHidlConfigs(mHidlConfigStrings, mContext); + + assertThat(mFaceSensorConfigurations.isSingleSensorConfigurationPresent()).isTrue(); + assertThat(mFaceSensorConfigurations.getResetLockoutRequiresChallenge()) + .isTrue(); + } +} diff --git a/core/tests/coretests/src/android/hardware/fingerprint/FingerprintSensorConfigurationsTest.java b/core/tests/coretests/src/android/hardware/fingerprint/FingerprintSensorConfigurationsTest.java new file mode 100644 index 000000000000..613089c8777d --- /dev/null +++ b/core/tests/coretests/src/android/hardware/fingerprint/FingerprintSensorConfigurationsTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 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. + */ + +package android.hardware.fingerprint; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; +import android.hardware.biometrics.fingerprint.IFingerprint; +import android.hardware.biometrics.fingerprint.SensorProps; +import android.os.RemoteException; +import android.platform.test.annotations.Presubmit; +import android.test.suitebuilder.annotation.SmallTest; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.function.Function; + + +@Presubmit +@SmallTest +public class FingerprintSensorConfigurationsTest { + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + @Mock + private Context mContext; + @Mock + private Resources mResources; + @Mock + private IFingerprint mFingerprint; + @Mock + private Function<String, IFingerprint> mGetIFingerprint; + + private final String[] mAidlInstances = new String[]{"default", "virtual"}; + private String[] mHidlConfigStrings = new String[]{"0:2:15", "0:8:15"}; + private FingerprintSensorConfigurations mFingerprintSensorConfigurations; + + @Before + public void setUp() throws RemoteException { + when(mGetIFingerprint.apply(anyString())).thenReturn(mFingerprint); + when(mFingerprint.getSensorProps()).thenReturn(new SensorProps[]{}); + when(mContext.getResources()).thenReturn(mResources); + } + + @Test + public void testAidlInstanceSensorProps() { + mFingerprintSensorConfigurations = new FingerprintSensorConfigurations( + true /* resetLockoutRequiresHardwareAuthToken */); + mFingerprintSensorConfigurations.addAidlSensors(mAidlInstances, mGetIFingerprint); + + assertThat(mFingerprintSensorConfigurations.hasSensorConfigurations()).isTrue(); + assertThat(!mFingerprintSensorConfigurations.isSingleSensorConfigurationPresent()) + .isTrue(); + assertThat(mFingerprintSensorConfigurations.getResetLockoutRequiresHardwareAuthToken()) + .isTrue(); + } + + @Test + public void testHidlConfigStrings() { + mFingerprintSensorConfigurations = new FingerprintSensorConfigurations( + false /* resetLockoutRequiresHardwareAuthToken */); + mFingerprintSensorConfigurations.addHidlSensors(mHidlConfigStrings, mContext); + + assertThat(mFingerprintSensorConfigurations.isSingleSensorConfigurationPresent()).isTrue(); + assertThat(mFingerprintSensorConfigurations.getResetLockoutRequiresHardwareAuthToken()) + .isFalse(); + } + + @Test + public void testHidlConfigStrings_incorrectFormat() { + mHidlConfigStrings = new String[]{"0:8:15", "0:2", "0:fingerprint:15"}; + mFingerprintSensorConfigurations = new FingerprintSensorConfigurations( + false /* resetLockoutRequiresHardwareAuthToken */); + mFingerprintSensorConfigurations.addHidlSensors(mHidlConfigStrings, mContext); + + assertThat(mFingerprintSensorConfigurations.isSingleSensorConfigurationPresent()).isTrue(); + assertThat(mFingerprintSensorConfigurations.getResetLockoutRequiresHardwareAuthToken()) + .isFalse(); + } +} diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java index 68c0693fb23a..a709d7be898b 100644 --- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java +++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java @@ -84,6 +84,7 @@ public class WindowOnBackInvokedDispatcherTest { /* progress = */ 0, /* velocityX = */ 0, /* velocityY = */ 0, + /* triggerBack = */ false, /* swipeEdge = */ BackEvent.EDGE_LEFT, /* departingAnimationTarget = */ null); diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 69a6e6d998a4..c6f920f55c07 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -518,6 +518,8 @@ applications that come with the platform <permission name="android.permission.ACCESS_BROADCAST_RADIO"/> <!-- Permission required for CTS test - CtsAmbientContextServiceTestCases --> <permission name="android.permission.ACCESS_AMBIENT_CONTEXT_EVENT"/> + <!-- Permission required for CTS test - CtsWearableSensingServiceTestCases --> + <permission name="android.permission.MANAGE_WEARABLE_SENSING_SERVICE"/> <!-- Permission required for CTS test - CtsTelephonyProviderTestCases --> <permission name="android.permission.WRITE_APN_SETTINGS"/> <!-- Permission required for GTS test - GtsStatsdHostTestCases --> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java index e06d3ef4e1ab..5b0de5070a60 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java @@ -21,5 +21,4 @@ package com.android.wm.shell.back; */ class BackAnimationConstants { static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.20f; - static final float PROGRESS_COMMIT_THRESHOLD = 0.1f; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index d8c691b01b61..a49823648d01 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -70,9 +70,11 @@ import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import java.io.PrintWriter; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -124,6 +126,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private final Context mContext; private final ContentResolver mContentResolver; private final ShellController mShellController; + private final ShellCommandHandler mShellCommandHandler; private final ShellExecutor mShellExecutor; private final Handler mBgHandler; @@ -180,7 +183,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @NonNull @ShellBackgroundThread Handler backgroundHandler, Context context, @NonNull BackAnimationBackground backAnimationBackground, - ShellBackAnimationRegistry shellBackAnimationRegistry) { + ShellBackAnimationRegistry shellBackAnimationRegistry, + ShellCommandHandler shellCommandHandler) { this( shellInit, shellController, @@ -190,7 +194,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont context, context.getContentResolver(), backAnimationBackground, - shellBackAnimationRegistry); + shellBackAnimationRegistry, + shellCommandHandler); } @VisibleForTesting @@ -203,7 +208,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont Context context, ContentResolver contentResolver, @NonNull BackAnimationBackground backAnimationBackground, - ShellBackAnimationRegistry shellBackAnimationRegistry) { + ShellBackAnimationRegistry shellBackAnimationRegistry, + ShellCommandHandler shellCommandHandler) { mShellController = shellController; mShellExecutor = shellExecutor; mActivityTaskManager = activityTaskManager; @@ -219,6 +225,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont .build(); mShellBackAnimationRegistry = shellBackAnimationRegistry; mLatencyTracker = LatencyTracker.getInstance(mContext); + mShellCommandHandler = shellCommandHandler; } private void onInit() { @@ -227,6 +234,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont createAdapter(); mShellController.addExternalInterface(KEY_EXTRA_SHELL_BACK_ANIMATION, this::createExternalInterface, this); + mShellCommandHandler.addDumpCallback(this::dump, this); } private void setupAnimationDeveloperSettingsObserver( @@ -968,4 +976,20 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont }; mBackAnimationAdapter = new BackAnimationAdapter(runner); } + + /** + * Description of current BackAnimationController state. + */ + private void dump(PrintWriter pw, String prefix) { + pw.println(prefix + "BackAnimationController state:"); + pw.println(prefix + " mEnableAnimations=" + mEnableAnimations.get()); + pw.println(prefix + " mBackGestureStarted=" + mBackGestureStarted); + pw.println(prefix + " mPostCommitAnimationInProgress=" + mPostCommitAnimationInProgress); + pw.println(prefix + " mShouldStartOnNextMoveEvent=" + mShouldStartOnNextMoveEvent); + pw.println(prefix + " mCurrentTracker state:"); + mCurrentTracker.dump(pw, prefix + " "); + pw.println(prefix + " mQueuedTracker state:"); + mQueuedTracker.dump(pw, prefix + " "); + } + } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java index 215a6cc99e58..30d5edb59c85 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java @@ -18,9 +18,9 @@ package com.android.wm.shell.back; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; +import static android.window.BackEvent.EDGE_RIGHT; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY; -import static com.android.wm.shell.back.BackAnimationConstants.PROGRESS_COMMIT_THRESHOLD; import static com.android.wm.shell.back.BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; @@ -91,7 +91,7 @@ public class CrossActivityBackAnimation extends ShellBackAnimation { } }; private static final float MIN_WINDOW_ALPHA = 0.01f; - private static final float WINDOW_X_SHIFT_DP = 96; + private static final float WINDOW_X_SHIFT_DP = 48; private static final int SCALE_FACTOR = 100; // TODO(b/264710590): Use the progress commit threshold from ViewConfiguration once it exists. private static final float TARGET_COMMIT_PROGRESS = 0.5f; @@ -126,6 +126,8 @@ public class CrossActivityBackAnimation extends ShellBackAnimation { private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); private boolean mBackInProgress = false; + private boolean mIsRightEdge; + private boolean mTriggerBack = false; private PointF mTouchPos = new PointF(); private IRemoteAnimationFinishedCallback mFinishCallback; @@ -209,6 +211,7 @@ public class CrossActivityBackAnimation extends ShellBackAnimation { private void finishAnimation() { if (mEnteringTarget != null) { + mTransaction.setCornerRadius(mEnteringTarget.leash, 0); mEnteringTarget.leash.release(); mEnteringTarget = null; } @@ -241,14 +244,15 @@ public class CrossActivityBackAnimation extends ShellBackAnimation { private void onGestureProgress(@NonNull BackEvent backEvent) { if (!mBackInProgress) { + mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT; mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY()); mBackInProgress = true; } mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY()); float progress = backEvent.getProgress(); - float springProgress = (progress > PROGRESS_COMMIT_THRESHOLD - ? mapLinear(progress, 0.1f, 1, TARGET_COMMIT_PROGRESS, 1) + float springProgress = (mTriggerBack + ? mapLinear(progress, 0f, 1, TARGET_COMMIT_PROGRESS, 1) : mapLinear(progress, 0, 1f, 0, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR; mLeavingProgressSpring.animateToFinalPosition(springProgress); mEnteringProgressSpring.animateToFinalPosition(springProgress); @@ -312,7 +316,7 @@ public class CrossActivityBackAnimation extends ShellBackAnimation { transformWithProgress( mEnteringProgress, Math.max( - smoothstep(ENTER_ALPHA_THRESHOLD, 1, mEnteringProgress), + smoothstep(ENTER_ALPHA_THRESHOLD, 0.7f, mEnteringProgress), MIN_WINDOW_ALPHA), /* alpha */ mEnteringTarget.leash, mEnteringRect, @@ -337,14 +341,13 @@ public class CrossActivityBackAnimation extends ShellBackAnimation { mClosingTarget.leash, mClosingRect, 0, - mWindowXShift + mIsRightEdge ? 0 : mWindowXShift ); } } private void transformWithProgress(float progress, float alpha, SurfaceControl surface, RectF targetRect, float deltaXMin, float deltaXMax) { - final float touchY = mTouchPos.y; final int width = mStartTaskRect.width(); final int height = mStartTaskRect.height(); @@ -376,12 +379,14 @@ public class CrossActivityBackAnimation extends ShellBackAnimation { private final class Callback extends IOnBackInvokedCallback.Default { @Override public void onBackStarted(BackMotionEvent backEvent) { + mTriggerBack = backEvent.getTriggerBack(); mProgressAnimator.onBackStarted(backEvent, CrossActivityBackAnimation.this::onGestureProgress); } @Override public void onBackProgressed(@NonNull BackMotionEvent backEvent) { + mTriggerBack = backEvent.getTriggerBack(); mProgressAnimator.onBackProgressed(backEvent); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java index 4bd56d460818..6213f628dfd3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java @@ -24,6 +24,8 @@ import android.view.RemoteAnimationTarget; import android.window.BackEvent; import android.window.BackMotionEvent; +import java.io.PrintWriter; + /** * Helper class to record the touch location for gesture and generate back events. */ @@ -129,6 +131,7 @@ class TouchTracker { /* progress = */ 0, /* velocityX = */ 0, /* velocityY = */ 0, + /* triggerBack = */ mTriggerBack, /* swipeEdge = */ mSwipeEdge, /* departingAnimationTarget = */ target); } @@ -204,6 +207,7 @@ class TouchTracker { /* progress = */ progress, /* velocityX = */ mLatestVelocityX, /* velocityY = */ mLatestVelocityY, + /* triggerBack = */ mTriggerBack, /* swipeEdge = */ mSwipeEdge, /* departingAnimationTarget = */ null); } @@ -219,6 +223,12 @@ class TouchTracker { mNonLinearFactor = nonLinearFactor; } + void dump(PrintWriter pw, String prefix) { + pw.println(prefix + "TouchTracker state:"); + pw.println(prefix + " mState=" + mState); + pw.println(prefix + " mTriggerBack=" + mTriggerBack); + } + enum TouchTrackerState { INITIAL, ACTIVE, FINISHED } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index b7f749e8a8b6..470a82511481 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -1291,7 +1291,7 @@ public class BubbleStackView extends FrameLayout // We only show user education for conversation bubbles right now return false; } - final boolean seen = getPrefBoolean(ManageEducationViewKt.PREF_MANAGED_EDUCATION); + final boolean seen = getPrefBoolean(ManageEducationView.PREF_MANAGED_EDUCATION); final boolean shouldShow = (!seen || BubbleDebugConfig.forceShowUserEducation(mContext)) && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null; if (BubbleDebugConfig.DEBUG_USER_EDUCATION) { @@ -1342,7 +1342,7 @@ public class BubbleStackView extends FrameLayout // We only show user education for conversation bubbles right now return false; } - final boolean seen = getPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION); + final boolean seen = getPrefBoolean(StackEducationView.PREF_STACK_EDUCATION); final boolean shouldShow = !seen || BubbleDebugConfig.forceShowUserEducation(mContext); if (BubbleDebugConfig.DEBUG_USER_EDUCATION) { Log.d(TAG, "Show stack edu: " + shouldShow); @@ -2323,7 +2323,8 @@ public class BubbleStackView extends FrameLayout updateOverflowVisibility(); updatePointerPosition(false /* forIme */); mExpandedAnimationController.expandFromStack(() -> { - if (mIsExpanded && mExpandedBubble.getExpandedView() != null) { + if (mIsExpanded && mExpandedBubble != null + && mExpandedBubble.getExpandedView() != null) { maybeShowManageEdu(); } updateOverflowDotVisibility(true /* expanding */); @@ -2384,7 +2385,7 @@ public class BubbleStackView extends FrameLayout } mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); - if (mExpandedBubble.getExpandedView() != null) { + if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { mExpandedBubble.getExpandedView().setContentAlpha(0f); mExpandedBubble.getExpandedView().setBackgroundAlpha(0f); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt index 61e17c8ec459..da71b1c741bb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt @@ -33,15 +33,16 @@ import com.android.wm.shell.animation.Interpolators * User education view to highlight the manage button that allows a user to configure the settings * for the bubble. Shown only the first time a user expands a bubble. */ -class ManageEducationView(context: Context, positioner: BubblePositioner) : LinearLayout(context) { - - private val TAG = - if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "ManageEducationView" - else BubbleDebugConfig.TAG_BUBBLES - - private val ANIMATE_DURATION: Long = 200 +class ManageEducationView( + context: Context, + private val positioner: BubblePositioner +) : LinearLayout(context) { + + companion object { + const val PREF_MANAGED_EDUCATION: String = "HasSeenBubblesManageOnboarding" + private const val ANIMATE_DURATION: Long = 200 + } - private val positioner: BubblePositioner = positioner private val manageView by lazy { requireViewById<ViewGroup>(R.id.manage_education_view) } private val manageButton by lazy { requireViewById<Button>(R.id.manage_button) } private val gotItButton by lazy { requireViewById<Button>(R.id.got_it) } @@ -128,7 +129,7 @@ class ManageEducationView(context: Context, positioner: BubblePositioner) : Line .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) .alpha(1f) } - setShouldShow(false) + updateManageEducationSeen() } /** @@ -218,13 +219,11 @@ class ManageEducationView(context: Context, positioner: BubblePositioner) : Line } } - private fun setShouldShow(shouldShow: Boolean) { + private fun updateManageEducationSeen() { context .getSharedPreferences(context.packageName, Context.MODE_PRIVATE) .edit() - .putBoolean(PREF_MANAGED_EDUCATION, !shouldShow) + .putBoolean(PREF_MANAGED_EDUCATION, true) .apply() } } - -const val PREF_MANAGED_EDUCATION: String = "HasSeenBubblesManageOnboarding" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt index 2cabb65abe7a..95f101722e89 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt @@ -34,19 +34,15 @@ import com.android.wm.shell.animation.Interpolators */ class StackEducationView( context: Context, - positioner: BubblePositioner, - controller: BubbleController + private val positioner: BubblePositioner, + private val controller: BubbleController ) : LinearLayout(context) { - private val TAG = - if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleStackEducationView" - else BubbleDebugConfig.TAG_BUBBLES - - private val ANIMATE_DURATION: Long = 200 - private val ANIMATE_DURATION_SHORT: Long = 40 - - private val positioner: BubblePositioner = positioner - private val controller: BubbleController = controller + companion object { + const val PREF_STACK_EDUCATION: String = "HasSeenBubblesOnboarding" + private const val ANIMATE_DURATION: Long = 200 + private const val ANIMATE_DURATION_SHORT: Long = 40 + } private val view by lazy { requireViewById<View>(R.id.stack_education_layout) } private val titleTextView by lazy { requireViewById<TextView>(R.id.stack_education_title) } @@ -175,7 +171,7 @@ class StackEducationView( .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) .alpha(1f) } - setShouldShow(false) + updateStackEducationSeen() return true } @@ -196,13 +192,11 @@ class StackEducationView( .withEndAction { visibility = GONE } } - private fun setShouldShow(shouldShow: Boolean) { + private fun updateStackEducationSeen() { context .getSharedPreferences(context.packageName, Context.MODE_PRIVATE) .edit() - .putBoolean(PREF_STACK_EDUCATION, !shouldShow) + .putBoolean(PREF_STACK_EDUCATION, true) .apply() } } - -const val PREF_STACK_EDUCATION: String = "HasSeenBubblesOnboarding" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java index 5b0239f6d659..02af2d06a1dd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java @@ -563,7 +563,7 @@ public class ExpandedAnimationController ? p.x - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR : p.x + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR; animationForChild(child) - .translationX(fromX, p.y) + .translationX(fromX, p.x) .start(); } else { float fromY = p.y - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR; @@ -634,4 +634,9 @@ public class ExpandedAnimationController .start(); } } + + /** Returns true if we're in the middle of a collapse or expand animation. */ + boolean isAnimating() { + return mAnimatingCollapse || mAnimatingExpand; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 3c6bc1754c5c..fc97c7988a0f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -363,7 +363,8 @@ public abstract class WMShellBaseModule { @ShellMainThread ShellExecutor shellExecutor, @ShellBackgroundThread Handler backgroundHandler, BackAnimationBackground backAnimationBackground, - Optional<ShellBackAnimationRegistry> shellBackAnimationRegistry) { + Optional<ShellBackAnimationRegistry> shellBackAnimationRegistry, + ShellCommandHandler shellCommandHandler) { if (BackAnimationController.IS_ENABLED) { return shellBackAnimationRegistry.map( (animations) -> @@ -374,7 +375,8 @@ public abstract class WMShellBaseModule { backgroundHandler, context, backAnimationBackground, - animations)); + animations, + shellCommandHandler)); } return Optional.empty(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java index ae21c4bf5450..f58aeac918b5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java @@ -277,11 +277,6 @@ public class SplashscreenContentDrawer { params.token = appToken; params.packageName = activityInfo.packageName; params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; - - if (!context.getResources().getCompatibilityInfo().supportsScreen()) { - params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW; - } - params.setTitle("Splash Screen " + title); return params; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index 771876f7ce5d..9ded6ea1d187 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -63,6 +63,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.util.test.FakeSettingsProvider; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.sysui.ShellSharedConstants; @@ -110,6 +111,8 @@ public class BackAnimationControllerTest extends ShellTestCase { @Mock private InputManager mInputManager; + @Mock + private ShellCommandHandler mShellCommandHandler; private BackAnimationController mController; private TestableContentResolver mContentResolver; @@ -145,7 +148,8 @@ public class BackAnimationControllerTest extends ShellTestCase { mContext, mContentResolver, mAnimationBackground, - mShellBackAnimationRegistry); + mShellBackAnimationRegistry, + mShellCommandHandler); mShellInit.init(); mShellExecutor.flushAll(); } @@ -298,7 +302,8 @@ public class BackAnimationControllerTest extends ShellTestCase { mContext, mContentResolver, mAnimationBackground, - mShellBackAnimationRegistry); + mShellBackAnimationRegistry, + mShellCommandHandler); shellInit.init(); registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java index 874ef80c29f0..91503b1c3619 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java @@ -53,6 +53,7 @@ public class BackProgressAnimatorTest { /* progress = */ progress, /* velocityX = */ 0, /* velocityY = */ 0, + /* triggerBack = */ false, /* swipeEdge = */ BackEvent.EDGE_LEFT, /* departingAnimationTarget = */ null); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java index c1ff260836b8..60f1d271c3af 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java @@ -16,52 +16,51 @@ package com.android.wm.shell.bubbles.animation; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.annotation.SuppressLint; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Insets; import android.graphics.PointF; import android.graphics.Rect; -import android.testing.AndroidTestingRunner; import android.view.View; import android.view.WindowManager; import android.widget.FrameLayout; import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleStackView; +import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + @SmallTest -@RunWith(AndroidTestingRunner.class) +@RunWith(AndroidJUnit4.class) public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestCase { - private int mDisplayWidth = 500; - private int mDisplayHeight = 1000; - - private Runnable mOnBubbleAnimatedOutAction = mock(Runnable.class); + private final Semaphore mBubbleRemovedSemaphore = new Semaphore(0); + private final Runnable mOnBubbleAnimatedOutAction = mBubbleRemovedSemaphore::release; ExpandedAnimationController mExpandedController; private int mStackOffset; private PointF mExpansionPoint; private BubblePositioner mPositioner; - private BubbleStackView.StackViewState mStackViewState = new BubbleStackView.StackViewState(); + private final BubbleStackView.StackViewState mStackViewState = + new BubbleStackView.StackViewState(); - @SuppressLint("VisibleForTests") @Before public void setUp() throws Exception { super.setUp(); @@ -70,15 +69,13 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC getContext().getSystemService(WindowManager.class)); mPositioner.updateInternal(Configuration.ORIENTATION_PORTRAIT, Insets.of(0, 0, 0, 0), - new Rect(0, 0, mDisplayWidth, mDisplayHeight)); + new Rect(0, 0, 500, 1000)); BubbleStackView stackView = mock(BubbleStackView.class); - when(stackView.getState()).thenReturn(getStackViewState()); mExpandedController = new ExpandedAnimationController(mPositioner, mOnBubbleAnimatedOutAction, stackView); - spyOn(mExpandedController); addOneMoreThanBubbleLimitBubbles(); mLayout.setActiveController(mExpandedController); @@ -86,9 +83,18 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC Resources res = mLayout.getResources(); mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); mExpansionPoint = new PointF(100, 100); + + getStackViewState(); + when(stackView.getState()).thenAnswer(i -> getStackViewState()); + waitForMainThread(); } - public BubbleStackView.StackViewState getStackViewState() { + @After + public void tearDown() { + waitForMainThread(); + } + + private BubbleStackView.StackViewState getStackViewState() { mStackViewState.numberOfBubbles = mLayout.getChildCount(); mStackViewState.selectedIndex = 0; mStackViewState.onLeft = mPositioner.isStackOnLeft(mExpansionPoint); @@ -96,68 +102,71 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC } @Test - @Ignore - public void testExpansionAndCollapse() throws InterruptedException { - Runnable afterExpand = mock(Runnable.class); - mExpandedController.expandFromStack(afterExpand); - waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); - + public void testExpansionAndCollapse() throws Exception { + expand(); testBubblesInCorrectExpandedPositions(); - verify(afterExpand).run(); + waitForMainThread(); - Runnable afterCollapse = mock(Runnable.class); + final Semaphore semaphore = new Semaphore(0); + Runnable afterCollapse = semaphore::release; mExpandedController.collapseBackToStack(mExpansionPoint, false, afterCollapse); - waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); - - testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y, -1); - verify(afterExpand).run(); + assertThat(semaphore.tryAcquire(1, 2, TimeUnit.SECONDS)).isTrue(); + waitForAnimation(); + testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y); } @Test - @Ignore - public void testOnChildAdded() throws InterruptedException { + public void testOnChildAdded() throws Exception { expand(); + waitForMainThread(); // Add another new view and wait for its animation. final View newView = new FrameLayout(getContext()); mLayout.addView(newView, 0); - waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + waitForAnimation(); testBubblesInCorrectExpandedPositions(); } @Test - @Ignore - public void testOnChildRemoved() throws InterruptedException { + public void testOnChildRemoved() throws Exception { expand(); + waitForMainThread(); - // Remove some views and see if the remaining child views still pass the expansion test. + // Remove some views and verify the remaining child views still pass the expansion test. mLayout.removeView(mViews.get(0)); mLayout.removeView(mViews.get(3)); - waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + + // Removing a view will invoke onBubbleAnimatedOutAction. Block until it gets called twice. + assertThat(mBubbleRemovedSemaphore.tryAcquire(2, 2, TimeUnit.SECONDS)).isTrue(); + + waitForAnimation(); testBubblesInCorrectExpandedPositions(); } @Test - public void testDragBubbleOutDoesntNPE() throws InterruptedException { + public void testDragBubbleOutDoesntNPE() { mExpandedController.onGestureFinished(); mExpandedController.dragBubbleOut(mViews.get(0), 1, 1); } /** Expand the stack and wait for animations to finish. */ private void expand() throws InterruptedException { - mExpandedController.expandFromStack(mock(Runnable.class)); - waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + final Semaphore semaphore = new Semaphore(0); + Runnable afterExpand = semaphore::release; + + mExpandedController.expandFromStack(afterExpand); + assertThat(semaphore.tryAcquire(1, TimeUnit.SECONDS)).isTrue(); } /** Check that children are in the correct positions for being stacked. */ - private void testStackedAtPosition(float x, float y, int offsetMultiplier) { + private void testStackedAtPosition(float x, float y) { // Make sure the rest of the stack moved again, including the first bubble not moving, and // is stacked to the right now that we're on the right side of the screen. for (int i = 0; i < mLayout.getChildCount(); i++) { - assertEquals(x + i * offsetMultiplier * mStackOffset, - mLayout.getChildAt(i).getTranslationX(), 2f); - assertEquals(y, mLayout.getChildAt(i).getTranslationY(), 2f); + assertEquals(x, mLayout.getChildAt(i).getTranslationX(), 2f); + assertEquals(y + Math.min(i, 1) * mStackOffset, mLayout.getChildAt(i).getTranslationY(), + 2f); assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f); } } @@ -175,4 +184,22 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC mLayout.getChildAt(i).getTranslationY(), 2f); } } + + private void waitForAnimation() throws Exception { + final Semaphore semaphore = new Semaphore(0); + boolean[] animating = new boolean[]{ true }; + for (int i = 0; i < 4; i++) { + if (animating[0]) { + mMainThreadHandler.post(() -> { + if (!mExpandedController.isAnimating()) { + animating[0] = false; + semaphore.release(); + } + }); + Thread.sleep(500); + } + } + assertThat(semaphore.tryAcquire(1, 2, TimeUnit.SECONDS)).isTrue(); + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java index 48ae2961b4be..2ed5addd900c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java @@ -164,11 +164,17 @@ public class PhysicsAnimationLayoutTestCase extends ShellTestCase { @Override public void cancelAllAnimations() { + if (mLayout.getChildCount() == 0) { + return; + } mMainThreadHandler.post(super::cancelAllAnimations); } @Override public void cancelAnimationsOnView(View view) { + if (mLayout.getChildCount() == 0) { + return; + } mMainThreadHandler.post(() -> super.cancelAnimationsOnView(view)); } @@ -221,6 +227,9 @@ public class PhysicsAnimationLayoutTestCase extends ShellTestCase { @Override protected void startPathAnimation() { + if (mLayout.getChildCount() == 0) { + return; + } mMainThreadHandler.post(super::startPathAnimation); } } @@ -322,4 +331,9 @@ public class PhysicsAnimationLayoutTestCase extends ShellTestCase { e.printStackTrace(); } } + + /** Waits for the main thread to finish processing all pending runnables. */ + public void waitForMainThread() { + runOnMainThreadAndBlock(() -> {}); + } } diff --git a/location/java/android/location/flags/gnss.aconfig b/location/java/android/location/flags/gnss.aconfig index b6055e818f8c..f4b1056019cb 100644 --- a/location/java/android/location/flags/gnss.aconfig +++ b/location/java/android/location/flags/gnss.aconfig @@ -20,3 +20,10 @@ flag { description: "Flag for GnssMeasurementRequest WorkSource API" bug: "295235160" } + +flag { + name: "release_supl_connection_on_timeout" + namespace: "location" + description: "Flag for releasing SUPL connection on timeout" + bug: "315024652" +} diff --git a/media/java/android/media/AudioDeviceInfo.java b/media/java/android/media/AudioDeviceInfo.java index 5a72b0be3405..1a3d7b7d5868 100644 --- a/media/java/android/media/AudioDeviceInfo.java +++ b/media/java/android/media/AudioDeviceInfo.java @@ -23,6 +23,8 @@ import android.annotation.RequiresPermission; import android.annotation.TestApi; import android.util.SparseIntArray; +import com.android.internal.annotations.VisibleForTesting; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; @@ -402,7 +404,9 @@ public final class AudioDeviceInfo { private final AudioDevicePort mPort; - AudioDeviceInfo(AudioDevicePort port) { + /** @hide */ + @VisibleForTesting + public AudioDeviceInfo(AudioDevicePort port) { mPort = port; } diff --git a/media/java/android/media/AudioDevicePort.java b/media/java/android/media/AudioDevicePort.java index 73bc6f96bf8b..2de8eefc4e78 100644 --- a/media/java/android/media/AudioDevicePort.java +++ b/media/java/android/media/AudioDevicePort.java @@ -20,6 +20,8 @@ import android.annotation.NonNull; import android.compat.annotation.UnsupportedAppUsage; import android.os.Build; +import com.android.aconfig.annotations.VisibleForTesting; + import java.util.Arrays; import java.util.List; @@ -38,6 +40,26 @@ import java.util.List; public class AudioDevicePort extends AudioPort { + /** @hide */ + // TODO: b/316864909 - Remove this method once there's a way to fake audio device ports further + // down the stack. + @VisibleForTesting + public static AudioDevicePort createForTesting( + int type, @NonNull String name, @NonNull String address) { + return new AudioDevicePort( + new AudioHandle(/* id= */ 0), + name, + /* samplingRates= */ null, + /* channelMasks= */ null, + /* channelIndexMasks= */ null, + /* formats= */ null, + /* gains= */ null, + type, + address, + /* encapsulationModes= */ null, + /* encapsulationMetadataTypes= */ null); + } + private final int mType; private final String mAddress; private final int[] mEncapsulationModes; diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 3dfd5726455d..a5a69f987113 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -22,6 +22,7 @@ import static android.content.Context.DEVICE_ID_DEFAULT; import static android.media.audio.Flags.autoPublicVolumeApiHardening; import static android.media.audio.Flags.automaticBtDeviceType; import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API; +import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION; import android.Manifest; import android.annotation.CallbackExecutor; @@ -5120,6 +5121,71 @@ public class AudioManager { } /** + * Notifies an application with a focus listener of gain or loss of audio focus + * + * <p>This is similar to {@link #dispatchAudioFocusChange(AudioFocusInfo, int, AudioPolicy)} but + * with additional functionality of fade. The players of the application with audio focus + * change, provided they meet the active {@link FadeManagerConfiguration} requirements, are + * faded before dispatching the callback to the application. For example, players of the + * application losing audio focus will be faded out, whereas players of the application gaining + * audio focus will be faded in, if needed. + * + * <p>The applicability of fade is decided against the supplied active {@link AudioFocusInfo}. + * This list cannot be {@code null}. The list can be empty if no other active + * {@link AudioFocusInfo} available at the time of the dispatch. + * + * <p>The {@link FadeManagerConfiguration} supplied here is prioritized over existing fade + * configurations. If none supplied, either the {@link FadeManagerConfiguration} set through + * {@link AudioPolicy} or the default will be used to determine the fade properties. + * + * <p>This method can only be used by owners of an {@link AudioPolicy} configured with + * {@link AudioPolicy.Builder#setIsAudioFocusPolicy(boolean)} set to true. + * + * @param afi the recipient of the focus change, that has previously requested audio focus, and + * that was received by the {@code AudioPolicy} through + * {@link AudioPolicy.AudioPolicyFocusListener#onAudioFocusRequest(AudioFocusInfo, int)} + * @param focusChange one of focus gain types ({@link #AUDIOFOCUS_GAIN}, + * {@link #AUDIOFOCUS_GAIN_TRANSIENT}, {@link #AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK} or + * {@link #AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}) + * or one of the focus loss types ({@link AudioManager#AUDIOFOCUS_LOSS}, + * {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT}, + * or {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK}). + * <br>For the focus gain, the change type should be the same as the app requested + * @param ap a valid registered {@link AudioPolicy} configured as a focus policy. + * @param otherActiveAfis active {@link AudioFocusInfo} that are granted audio focus at the time + * of dispatch + * @param transientFadeMgrConfig {@link FadeManagerConfiguration} that will be used for fading + * players resulting from this dispatch. This is a transient configuration that is only + * valid for this focus change and shall be discarded after processing this request. + * @return {@link #AUDIOFOCUS_REQUEST_FAILED} if the focus client didn't have a listener or if + * there was an error sending the request, or {@link #AUDIOFOCUS_REQUEST_GRANTED} if the + * dispatch was successfully sent, or {@link #AUDIOFOCUS_REQUEST_DELAYED} if + * the request was successful but the dispatch of focus change was delayed due to a fade + * operation. + * @throws NullPointerException if the {@link AudioFocusInfo} or {@link AudioPolicy} or list of + * other active {@link AudioFocusInfo} are {@code null}. + * @hide + */ + @FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION) + @SystemApi + @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public int dispatchAudioFocusChangeWithFade(@NonNull AudioFocusInfo afi, int focusChange, + @NonNull AudioPolicy ap, @NonNull List<AudioFocusInfo> otherActiveAfis, + @Nullable FadeManagerConfiguration transientFadeMgrConfig) { + Objects.requireNonNull(afi, "AudioFocusInfo cannot be null"); + Objects.requireNonNull(ap, "AudioPolicy cannot be null"); + Objects.requireNonNull(otherActiveAfis, "Other active AudioFocusInfo list cannot be null"); + + IAudioService service = getService(); + try { + return service.dispatchFocusChangeWithFade(afi, focusChange, ap.cb(), otherActiveAfis, + transientFadeMgrConfig); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * @hide * Used internally by telephony package to abandon audio focus, typically after a call or * when ringing ends and the call is rejected or not answered. diff --git a/media/java/android/media/FadeManagerConfiguration.java b/media/java/android/media/FadeManagerConfiguration.java index 337d4b0a916c..40b0e3e03ef6 100644 --- a/media/java/android/media/FadeManagerConfiguration.java +++ b/media/java/android/media/FadeManagerConfiguration.java @@ -16,12 +16,13 @@ package android.media; -import static com.android.media.flags.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION; +import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; import android.util.ArrayMap; @@ -93,11 +94,9 @@ import java.util.Objects; * Helps with recreating a new instance from another to simply change/add on top of the * existing ones</li> * </ul> - * TODO(b/304835727): Convert into system API so that it can be set through AudioPolicy - * * @hide */ - +@SystemApi @FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION) public final class FadeManagerConfiguration implements Parcelable { @@ -523,6 +522,7 @@ public final class FadeManagerConfiguration implements Parcelable { * * @param fadeState one of the fade state in {@link FadeStateEnum} * @return human-readable string + * @hide */ @NonNull public static String fadeStateToString(@FadeStateEnum int fadeState) { @@ -712,7 +712,8 @@ public final class FadeManagerConfiguration implements Parcelable { * * <p><b>Notes:</b> * <ul> - * <li>When fade state is set to enabled, the builder expects at least one valid usage to be + * <li>When fade state is set to {@link #FADE_STATE_ENABLED_DEFAULT} or + * {@link #FADE_STATE_ENABLED_AUTO}, the builder expects at least one valid usage to be * set/added. Failure to do so will result in an exception during {@link #build()}</li> * <li>Every usage added to the fadeable list should have corresponding volume shaper * configs defined. This can be achieved by setting either the duration or volume shaper @@ -720,8 +721,8 @@ public final class FadeManagerConfiguration implements Parcelable { * {@link #setFadeOutVolumeShaperConfigForUsage(int, VolumeShaper.Configuration)}</li> * <li> It is recommended to set volume shaper configurations individually for fade out and * fade in</li> - * <li>For any incomplete volume shaper configs a volume shaper configuration will be - * created using either the default fade durations or the ones provided as part of the + * <li>For any incomplete volume shaper configurations, a volume shaper configuration will + * be created using either the default fade durations or the ones provided as part of the * {@link #Builder(long, long)}</li> * <li>Additional volume shaper configs can also configured for a given usage * with additional attributes like content-type in order to achieve finer fade controls. diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index a52f0b08330d..5c268d4ab652 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -28,6 +28,7 @@ import android.media.AudioPlaybackConfiguration; import android.media.AudioRecordingConfiguration; import android.media.AudioRoutesInfo; import android.media.BluetoothProfileConnectionInfo; +import android.media.FadeManagerConfiguration; import android.media.IAudioDeviceVolumeDispatcher; import android.media.IAudioFocusDispatcher; import android.media.IAudioModeDispatcher; @@ -398,6 +399,14 @@ interface IAudioService { int dispatchFocusChange(in AudioFocusInfo afi, in int focusChange, in IAudioPolicyCallback pcb); + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)") + int dispatchFocusChangeWithFade(in AudioFocusInfo afi, + in int focusChange, + in IAudioPolicyCallback pcb, + in List<AudioFocusInfo> otherActiveAfis, + in FadeManagerConfiguration transientFadeMgrConfig); + oneway void playerHasOpPlayAudio(in int piid, in boolean hasOpPlayAudio); @EnforcePermission("BLUETOOTH_STACK") @@ -754,4 +763,16 @@ interface IAudioService { oneway void removeLoudnessCodecInfo(int piid, in LoudnessCodecInfo codecInfo); PersistableBundle getLoudnessParams(int piid, in LoudnessCodecInfo codecInfo); + + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)") + int setFadeManagerConfigurationForFocusLoss(in FadeManagerConfiguration fmcForFocusLoss); + + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)") + int clearFadeManagerConfigurationForFocusLoss(); + + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)") + FadeManagerConfiguration getFadeManagerConfigurationForFocusLoss(); } diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java index e16849811b9d..b85decc74ff8 100644 --- a/media/java/android/media/audiopolicy/AudioPolicy.java +++ b/media/java/android/media/audiopolicy/AudioPolicy.java @@ -16,6 +16,8 @@ package android.media.audiopolicy; +import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION; + import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; @@ -34,6 +36,7 @@ import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioRecord; import android.media.AudioTrack; +import android.media.FadeManagerConfiguration; import android.media.IAudioService; import android.media.MediaRecorder; import android.media.projection.MediaProjection; @@ -49,6 +52,7 @@ import android.util.Pair; import android.util.Slog; import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -612,6 +616,110 @@ public class AudioPolicy { return mRegistrationId; } + /** + * Sets a custom {@link FadeManagerConfiguration} to handle fade cycle of players during + * {@link android.media.AudioManager#AUDIOFOCUS_LOSS} + * + * @param fmcForFocusLoss custom {@link FadeManagerConfiguration} + * @return {@link AudioManager#SUCCESS} if the update was successful, + * {@link AudioManager#ERROR} otherwise + * @throws IllegalStateException if the audio policy is not registered + * @hide + */ + @FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION) + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + @SystemApi + public int setFadeManagerConfigurationForFocusLoss( + @NonNull FadeManagerConfiguration fmcForFocusLoss) { + Objects.requireNonNull(fmcForFocusLoss, + "FadeManagerConfiguration for focus loss cannot be null"); + + IAudioService service = getService(); + synchronized (mLock) { + Preconditions.checkState(isAudioPolicyRegisteredLocked(), + "Cannot set FadeManagerConfiguration with unregistered AudioPolicy"); + + try { + return service.setFadeManagerConfigurationForFocusLoss(fmcForFocusLoss); + } catch (RemoteException e) { + Log.e(TAG, "Received remote exception for setFadeManagerConfigurationForFocusLoss:", + e); + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Clear the current {@link FadeManagerConfiguration} set to handle fade cycles of players + * during {@link android.media.AudioManager#AUDIOFOCUS_LOSS} + * + * <p>In the absence of custom {@link FadeManagerConfiguration}, the default configurations will + * be used to handle fade cycles during audio focus loss. + * + * @return {@link AudioManager#SUCCESS} if the update was successful, + * {@link AudioManager#ERROR} otherwise + * @throws IllegalStateException if the audio policy is not registered + * @see #setFadeManagerConfigurationForFocusLoss(FadeManagerConfiguration) + * @hide + */ + @FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION) + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + @SystemApi + public int clearFadeManagerConfigurationForFocusLoss() { + IAudioService service = getService(); + synchronized (mLock) { + Preconditions.checkState(isAudioPolicyRegisteredLocked(), + "Cannot clear FadeManagerConfiguration from unregistered AudioPolicy"); + + try { + return service.clearFadeManagerConfigurationForFocusLoss(); + } catch (RemoteException e) { + Log.e(TAG, "Received remote exception for " + + "clearFadeManagerConfigurationForFocusLoss:", e); + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Get the current fade manager configuration used for fade operations during + * {@link android.media.AudioManager#AUDIOFOCUS_LOSS} + * + * <p>If no custom {@link FadeManagerConfiguration} is set, the default configuration currently + * active will be returned. + * + * @return the active {@link FadeManagerConfiguration} used during audio focus loss + * @throws IllegalStateException if the audio policy is not registered + * @see #setFadeManagerConfigurationForFocusLoss(FadeManagerConfiguration) + * @see #clearFadeManagerConfigurationForFocusLoss() + * @hide + */ + @FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION) + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + @SystemApi + @NonNull + public FadeManagerConfiguration getFadeManagerConfigurationForFocusLoss() { + IAudioService service = getService(); + synchronized (mLock) { + Preconditions.checkState(isAudioPolicyRegisteredLocked(), + "Cannot get FadeManagerConfiguration from unregistered AudioPolicy"); + + try { + return service.getFadeManagerConfigurationForFocusLoss(); + } catch (RemoteException e) { + Log.e(TAG, "Received remote exception for getFadeManagerConfigurationForFocusLoss:", + e); + throw e.rethrowFromSystemServer(); + + } + } + } + + @GuardedBy("mLock") + private boolean isAudioPolicyRegisteredLocked() { + return mStatus == POLICY_STATUS_REGISTERED; + } + private boolean policyReadyToUse() { synchronized (mLock) { if (mStatus != POLICY_STATUS_REGISTERED) { diff --git a/media/java/android/media/flags/fade_manager_configuration.aconfig b/media/java/android/media/flags/fade_manager_configuration.aconfig deleted file mode 100644 index 100e2235a7a8..000000000000 --- a/media/java/android/media/flags/fade_manager_configuration.aconfig +++ /dev/null @@ -1,8 +0,0 @@ -package: "com.android.media.flags" - -flag { - namespace: "media_solutions" - name: "enable_fade_manager_configuration" - description: "Enable Fade Manager Configuration support to determine fade properties" - bug: "307354764" -}
\ No newline at end of file diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java index fb6bd489d5d0..f105ae9cc33e 100644 --- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java +++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java @@ -16,7 +16,7 @@ package com.android.audiopolicytest; -import static com.android.media.flags.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION; +import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION; import static org.junit.Assert.assertThrows; diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java b/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java index 562d20d05429..7fb959ae4d76 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java @@ -20,6 +20,7 @@ import android.annotation.Nullable; import android.app.ActivityManager; import android.app.AlarmManager; import android.app.AlertDialog; +import android.app.Flags; import android.app.NotificationManager; import android.content.Context; import android.content.DialogInterface; @@ -143,9 +144,16 @@ public class EnableZenModeDialog { Slog.d(TAG, "Invalid manual condition: " + tag.condition); } // always triggers priority-only dnd with chosen condition - mNotificationManager.setZenMode( - Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, - getRealConditionId(tag.condition), TAG); + if (Flags.modesApi()) { + mNotificationManager.setZenMode( + Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, + getRealConditionId(tag.condition), TAG, + /* fromUser= */ true); + } else { + mNotificationManager.setZenMode( + Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, + getRealConditionId(tag.condition), TAG); + } } }); diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp index adebdcdcf26a..d5814e3a9b79 100644 --- a/packages/SettingsProvider/Android.bp +++ b/packages/SettingsProvider/Android.bp @@ -59,10 +59,10 @@ android_test { // Note we statically link SettingsProviderLib to do some unit tests. It's not accessible otherwise // because this test is not an instrumentation test. (because the target runs in the system process.) "SettingsProviderLib", - "androidx.test.rules", "flag-junit", "junit", + "libaconfig_java_proto_lite", "mockito-target-minus-junit4", "platform-test-annotations", "truth", diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index 8f459c647316..73c2e22240d3 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -18,6 +18,9 @@ package com.android.providers.settings; import static android.os.Process.FIRST_APPLICATION_UID; +import android.aconfig.Aconfig.flag_state; +import android.aconfig.Aconfig.parsed_flag; +import android.aconfig.Aconfig.parsed_flags; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; @@ -147,6 +150,17 @@ final class SettingsState { */ private static final String CONFIG_STAGED_PREFIX = "staged/"; + private static final List<String> sAconfigTextProtoFilesOnDevice = List.of( + "/system/etc/aconfig_flags.pb", + "/system_ext/etc/aconfig_flags.pb", + "/product/etc/aconfig_flags.pb", + "/vendor/etc/aconfig_flags.pb"); + + /** + * This tag is applied to all aconfig default value-loaded flags. + */ + private static final String BOOT_LOADED_DEFAULT_TAG = "BOOT_LOADED_DEFAULT"; + // This was used in version 120 and before. private static final String NULL_VALUE_OLD_STYLE = "null"; @@ -315,6 +329,59 @@ final class SettingsState { synchronized (mLock) { readStateSyncLocked(); + + if (Flags.loadAconfigDefaults()) { + // Only load aconfig defaults if this is the first boot, the XML + // file doesn't exist yet, or this device is on its first boot after + // an OTA. + boolean shouldLoadAconfigValues = isConfigSettingsKey(mKey) + && (!file.exists() + || mContext.getPackageManager().isDeviceUpgrading()); + if (shouldLoadAconfigValues) { + loadAconfigDefaultValuesLocked(); + } + } + } + } + + @GuardedBy("mLock") + private void loadAconfigDefaultValuesLocked() { + for (String fileName : sAconfigTextProtoFilesOnDevice) { + try (FileInputStream inputStream = new FileInputStream(fileName)) { + byte[] contents = inputStream.readAllBytes(); + loadAconfigDefaultValues(contents); + } catch (IOException e) { + Slog.e(LOG_TAG, "failed to read protobuf", e); + } + } + } + + @VisibleForTesting + @GuardedBy("mLock") + public void loadAconfigDefaultValues(byte[] fileContents) { + try { + parsed_flags parsedFlags = parsed_flags.parseFrom(fileContents); + + if (parsedFlags == null) { + Slog.e(LOG_TAG, "failed to parse aconfig protobuf"); + return; + } + + for (parsed_flag flag : parsedFlags.getParsedFlagList()) { + String flagName = flag.getNamespace() + "/" + + flag.getPackage() + "." + flag.getName(); + String value = flag.getState() == flag_state.ENABLED ? "true" : "false"; + + Setting existingSetting = getSettingLocked(flagName); + boolean isDefaultLoaded = existingSetting.getTag() != null + && existingSetting.getTag().equals(BOOT_LOADED_DEFAULT_TAG); + if (existingSetting.getValue() == null || isDefaultLoaded) { + insertSettingLocked(flagName, value, BOOT_LOADED_DEFAULT_TAG, + false, flag.getPackage()); + } + } + } catch (IOException e) { + Slog.e(LOG_TAG, "failed to parse protobuf", e); } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig index 27ce0d4c7252..ecac5ee18582 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig +++ b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig @@ -6,3 +6,11 @@ flag { description: "When enabled, allows setting and displaying local overrides via adb." bug: "298392357" } + +flag { + name: "load_aconfig_defaults" + namespace: "core_experiments_team_internal" + description: "When enabled, loads aconfig default values into DeviceConfig on boot." + bug: "311155098" + is_fixed_read_only: true +} diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java index 02a7bc1646ba..24625eaa5e13 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java @@ -15,6 +15,9 @@ */ package com.android.providers.settings; +import android.aconfig.Aconfig; +import android.aconfig.Aconfig.parsed_flag; +import android.aconfig.Aconfig.parsed_flags; import android.os.Looper; import android.test.AndroidTestCase; import android.util.Xml; @@ -84,6 +87,86 @@ public class SettingsStateTest extends AndroidTestCase { super.tearDown(); } + public void testLoadValidAconfigProto() { + int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0); + Object lock = new Object(); + SettingsState settingsState = new SettingsState( + getContext(), lock, mSettingsFile, configKey, + SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); + + parsed_flags flags = parsed_flags + .newBuilder() + .addParsedFlag(parsed_flag + .newBuilder() + .setPackage("com.android.flags") + .setName("flag1") + .setNamespace("test_namespace") + .setDescription("test flag") + .addBug("12345678") + .setState(Aconfig.flag_state.DISABLED) + .setPermission(Aconfig.flag_permission.READ_WRITE)) + .addParsedFlag(parsed_flag + .newBuilder() + .setPackage("com.android.flags") + .setName("flag2") + .setNamespace("test_namespace") + .setDescription("another test flag") + .addBug("12345678") + .setState(Aconfig.flag_state.ENABLED) + .setPermission(Aconfig.flag_permission.READ_WRITE)) + .build(); + + synchronized (lock) { + settingsState.loadAconfigDefaultValues(flags.toByteArray()); + settingsState.persistSettingsLocked(); + } + settingsState.waitForHandler(); + + synchronized (lock) { + assertEquals("false", + settingsState.getSettingLocked( + "test_namespace/com.android.flags.flag1").getValue()); + assertEquals("true", + settingsState.getSettingLocked( + "test_namespace/com.android.flags.flag2").getValue()); + } + } + + public void testSkipLoadingAconfigFlagWithMissingFields() { + int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0); + Object lock = new Object(); + SettingsState settingsState = new SettingsState( + getContext(), lock, mSettingsFile, configKey, + SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); + + parsed_flags flags = parsed_flags + .newBuilder() + .addParsedFlag(parsed_flag + .newBuilder() + .setDescription("test flag") + .addBug("12345678") + .setState(Aconfig.flag_state.DISABLED) + .setPermission(Aconfig.flag_permission.READ_WRITE)) + .build(); + + synchronized (lock) { + settingsState.loadAconfigDefaultValues(flags.toByteArray()); + settingsState.persistSettingsLocked(); + } + settingsState.waitForHandler(); + + synchronized (lock) { + assertEquals(null, + settingsState.getSettingLocked( + "test_namespace/com.android.flags.flag1").getValue()); + } + } + + public void testInvalidAconfigProtoDoesNotCrash() { + SettingsState settingsState = getSettingStateObject(); + settingsState.loadAconfigDefaultValues("invalid protobuf".getBytes()); + } + public void testIsBinary() { assertFalse(SettingsState.isBinary(" abc 日本語")); diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 6e65c16c41a1..477c42e01fba 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -688,6 +688,9 @@ <!-- Permission required for CTS test - CtsAmbientContextDetectionServiceDeviceTest --> <uses-permission android:name="android.permission.ACCESS_AMBIENT_CONTEXT_EVENT" /> + <!-- Permission required for CTS test - CtsWearableSensingServiceTestCases --> + <uses-permission android:name="android.permission.MANAGE_WEARABLE_SENSING_SERVICE" /> + <!-- Permission required for CTS test - CallAudioInterceptionTest --> <uses-permission android:name="android.permission.CALL_AUDIO_INTERCEPTION" /> diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 1a35f04b393f..a03fa9b39bfc 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -256,6 +256,9 @@ <!-- launcher apps --> <uses-permission android:name="android.permission.ACCESS_SHORTCUTS" /> + <!-- Permission to start Launcher's widget picker activity. --> + <uses-permission android:name="android.permission.START_WIDGET_PICKER_ACTIVITY" /> + <uses-permission android:name="android.permission.MODIFY_THEME_OVERLAY" /> <!-- accessibility --> @@ -974,15 +977,6 @@ android:excludeFromRecents="true" android:visibleToInstantApps="true"/> - <activity android:name="com.android.systemui.communal.widgets.WidgetPickerActivity" - android:theme="@style/Theme.EditWidgetsActivity" - android:excludeFromRecents="true" - android:autoRemoveFromRecents="true" - android:showOnLockScreen="true" - android:launchMode="singleTop" - android:exported="false"> - </activity> - <activity android:name="com.android.systemui.communal.widgets.EditWidgetsActivity" android:theme="@style/Theme.EditWidgetsActivity" android:excludeFromRecents="true" diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index ab4fe76a58c7..8b27960af309 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -45,6 +45,13 @@ flag { } flag { + name: "notifications_improved_hun_animation" + namespace: "systemui" + description: "Adds a translateY animation, and other improvements to match the motion specs of the HUN Intro + Outro animations." + bug: "243302608" +} + +flag { name: "notification_lifetime_extension_refactor" namespace: "systemui" description: "Enables moving notification lifetime extension management from SystemUI to " @@ -241,10 +248,16 @@ flag { } flag { + name: "switch_user_on_bg" + namespace: "systemui" + description: "Does user switching on a background thread" + bug: "284095720" +} + +flag { name: "status_bar_static_inout_indicators" namespace: "systemui" description: "(Upstream request) Always show the network activity inout indicators and " "prefer using alpha to distinguish network activity." bug: "310715220" } - diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt index 4d53cca66e5b..cbf249653577 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt @@ -21,6 +21,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.KeyguardViewConfigurator import com.android.systemui.keyguard.qualifiers.KeyguardRootView import com.android.systemui.keyguard.ui.composable.LockscreenScene +import com.android.systemui.keyguard.ui.composable.LockscreenSceneBlueprintModule import com.android.systemui.scene.shared.model.Scene import dagger.Binds import dagger.Module @@ -29,7 +30,12 @@ import dagger.multibindings.IntoSet import javax.inject.Provider import kotlinx.coroutines.ExperimentalCoroutinesApi -@Module +@Module( + includes = + [ + LockscreenSceneBlueprintModule::class, + ], +) interface LockscreenSceneModule { @Binds @IntoSet fun lockscreenScene(scene: LockscreenScene): Scene diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt new file mode 100644 index 000000000000..2cb00342cba5 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.keyguard.ui.composable + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.SceneTransitionLayout +import com.android.compose.animation.scene.transitions +import com.android.systemui.keyguard.ui.composable.blueprint.LockscreenSceneBlueprint +import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel +import javax.inject.Inject + +/** + * Renders the content of the lockscreen. + * + * This is separate from the [LockscreenScene] because it's meant to support usage of this UI from + * outside the scene container framework. + */ +class LockscreenContent +@Inject +constructor( + private val viewModel: LockscreenContentViewModel, + private val blueprints: Set<@JvmSuppressWildcards LockscreenSceneBlueprint>, +) { + + private val sceneKeyByBlueprint: Map<LockscreenSceneBlueprint, SceneKey> by lazy { + blueprints.associateWith { blueprint -> SceneKey(blueprint.id) } + } + private val sceneKeyByBlueprintId: Map<String, SceneKey> by lazy { + sceneKeyByBlueprint.entries.associate { (blueprint, sceneKey) -> blueprint.id to sceneKey } + } + + @Composable + fun Content( + modifier: Modifier = Modifier, + ) { + val coroutineScope = rememberCoroutineScope() + val blueprintId by viewModel.blueprintId(coroutineScope).collectAsState() + + // Switch smoothly between blueprints, any composable tagged with element() will be + // transition-animated between any two blueprints, if they both display the same element. + SceneTransitionLayout( + currentScene = checkNotNull(sceneKeyByBlueprintId[blueprintId]), + onChangeScene = {}, + transitions = + transitions { sceneKeyByBlueprintId.values.forEach { sceneKey -> to(sceneKey) } }, + modifier = modifier, + ) { + sceneKeyByBlueprint.entries.forEach { (blueprint, sceneKey) -> + scene(sceneKey) { with(blueprint) { Content(Modifier.fillMaxSize()) } } + } + } + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt index 30536547ae5c..93f31ec8f7ed 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalFoundationApi::class) +@file:OptIn(ExperimentalFoundationApi::class) package com.android.systemui.keyguard.ui.composable @@ -50,14 +50,17 @@ import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.scene.shared.model.UserAction import com.android.systemui.scene.ui.composable.ComposableScene +import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +/** Set this to `true` to use the LockscreenContent replacement of KeyguardRootView. */ +private val UseLockscreenContent = false + /** The lock screen scene shows when the device is locked. */ @SysUISingleton class LockscreenScene @@ -66,6 +69,7 @@ constructor( @Application private val applicationScope: CoroutineScope, private val viewModel: LockscreenSceneViewModel, @KeyguardRootView private val viewProvider: () -> @JvmSuppressWildcards View, + private val lockscreenContent: Lazy<LockscreenContent>, ) : ComposableScene { override val key = SceneKey.Lockscreen @@ -91,6 +95,7 @@ constructor( LockscreenScene( viewProvider = viewProvider, viewModel = viewModel, + lockscreenContent = lockscreenContent, modifier = modifier, ) } @@ -113,6 +118,7 @@ constructor( private fun SceneScope.LockscreenScene( viewProvider: () -> View, viewModel: LockscreenSceneViewModel, + lockscreenContent: Lazy<LockscreenContent>, modifier: Modifier = Modifier, ) { fun findSettingsMenu(): View { @@ -133,16 +139,24 @@ private fun SceneScope.LockscreenScene( modifier = Modifier.fillMaxSize(), ) - AndroidView( - factory = { _ -> - val keyguardRootView = viewProvider() - // Remove the KeyguardRootView from any parent it might already have in legacy code - // just in case (a view can't have two parents). - (keyguardRootView.parent as? ViewGroup)?.removeView(keyguardRootView) - keyguardRootView - }, - modifier = Modifier.fillMaxSize(), - ) + if (UseLockscreenContent) { + lockscreenContent + .get() + .Content( + modifier = Modifier.fillMaxSize(), + ) + } else { + AndroidView( + factory = { _ -> + val keyguardRootView = viewProvider() + // Remove the KeyguardRootView from any parent it might already have in legacy + // code just in case (a view can't have two parents). + (keyguardRootView.parent as? ViewGroup)?.removeView(keyguardRootView) + keyguardRootView + }, + modifier = Modifier.fillMaxSize(), + ) + } val notificationStackPosition by viewModel.keyguardRoot.notificationBounds.collectAsState() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt new file mode 100644 index 000000000000..9abb50c35ccf --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.keyguard.ui.composable + +import com.android.systemui.keyguard.ui.composable.blueprint.CommunalBlueprintModule +import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule +import com.android.systemui.keyguard.ui.composable.blueprint.ShortcutsBesideUdfpsBlueprintModule +import com.android.systemui.keyguard.ui.composable.blueprint.SplitShadeBlueprintModule +import dagger.Module + +@Module( + includes = + [ + CommunalBlueprintModule::class, + DefaultBlueprintModule::class, + ShortcutsBesideUdfpsBlueprintModule::class, + SplitShadeBlueprintModule::class, + ], +) +interface LockscreenSceneBlueprintModule diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt new file mode 100644 index 000000000000..7eddaaf9ba4b --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.keyguard.ui.composable.blueprint + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.android.compose.animation.scene.SceneScope +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoSet +import javax.inject.Inject + +/** Renders the lockscreen scene when showing the communal glanceable hub. */ +class CommunalBlueprint @Inject constructor() : LockscreenSceneBlueprint { + + override val id: String = "communal" + + @Composable + override fun SceneScope.Content(modifier: Modifier) { + Box(modifier.background(Color.Black)) { + Text( + text = "TODO(b/316211368): communal blueprint", + color = Color.White, + modifier = Modifier.align(Alignment.Center), + ) + } + } +} + +@Module +interface CommunalBlueprintModule { + @Binds @IntoSet fun blueprint(blueprint: CommunalBlueprint): LockscreenSceneBlueprint +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt new file mode 100644 index 000000000000..fc1df848d46f --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.keyguard.ui.composable.blueprint + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.IntOffset +import com.android.compose.animation.scene.SceneScope +import com.android.compose.modifiers.height +import com.android.compose.modifiers.width +import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection +import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection +import com.android.systemui.keyguard.ui.composable.section.ClockSection +import com.android.systemui.keyguard.ui.composable.section.LockSection +import com.android.systemui.keyguard.ui.composable.section.NotificationSection +import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection +import com.android.systemui.keyguard.ui.composable.section.StatusBarSection +import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoSet +import javax.inject.Inject + +/** + * Renders the lockscreen scene when showing with the default layout (e.g. vertical phone form + * factor). + */ +class DefaultBlueprint +@Inject +constructor( + private val viewModel: LockscreenContentViewModel, + private val statusBarSection: StatusBarSection, + private val clockSection: ClockSection, + private val smartSpaceSection: SmartSpaceSection, + private val notificationSection: NotificationSection, + private val lockSection: LockSection, + private val ambientIndicationSection: AmbientIndicationSection, + private val bottomAreaSection: BottomAreaSection, +) : LockscreenSceneBlueprint { + + override val id: String = "default" + + @Composable + override fun SceneScope.Content(modifier: Modifier) { + val context = LocalContext.current + val lockIconBounds = lockSection.lockIconBounds(context) + val isUdfpsVisible = viewModel.isUdfpsVisible + + Box( + modifier = modifier, + ) { + Column( + modifier = Modifier.fillMaxWidth().height { lockIconBounds.top }, + ) { + with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) } + with(clockSection) { SmallClock(modifier = Modifier.fillMaxWidth()) } + with(smartSpaceSection) { SmartSpace(modifier = Modifier.fillMaxWidth()) } + with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) } + with(notificationSection) { + Notifications(modifier = Modifier.fillMaxWidth().weight(1f)) + } + if (!isUdfpsVisible) { + with(ambientIndicationSection) { + AmbientIndication(modifier = Modifier.fillMaxWidth()) + } + } + } + + with(lockSection) { + LockIcon( + modifier = + Modifier.width { lockIconBounds.width() } + .height { lockIconBounds.height() } + .offset { IntOffset(lockIconBounds.left, lockIconBounds.top) } + ) + } + + Column(modifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter)) { + if (isUdfpsVisible) { + with(ambientIndicationSection) { + AmbientIndication(modifier = Modifier.fillMaxWidth()) + } + } + + with(bottomAreaSection) { BottomArea(modifier = Modifier.fillMaxWidth()) } + } + } + } +} + +@Module +interface DefaultBlueprintModule { + @Binds @IntoSet fun blueprint(blueprint: DefaultBlueprint): LockscreenSceneBlueprint +} diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/android/test/AndroidTestCase.java b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/LockscreenSceneBlueprint.kt index e6d38668335a..6d9cba4acb39 100644 --- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/android/test/AndroidTestCase.java +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/LockscreenSceneBlueprint.kt @@ -13,15 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package android.test; -import android.content.Context; +package com.android.systemui.keyguard.ui.composable.blueprint -import junit.framework.TestCase; +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.android.compose.animation.scene.SceneScope -public class AndroidTestCase extends TestCase { - protected Context mContext; - public Context getContext() { - throw new RuntimeException("[ravenwood] Class Context is not supported yet."); - } +/** Defines interface for classes that can render the content for a specific blueprint/layout. */ +interface LockscreenSceneBlueprint { + + /** The ID that uniquely identifies this blueprint across all other blueprints. */ + val id: String + + /** Renders the content of this blueprint. */ + @Composable fun SceneScope.Content(modifier: Modifier) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt new file mode 100644 index 000000000000..fa913f1695fe --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.keyguard.ui.composable.blueprint + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.IntOffset +import com.android.compose.animation.scene.SceneScope +import com.android.compose.modifiers.height +import com.android.compose.modifiers.width +import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection +import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection +import com.android.systemui.keyguard.ui.composable.section.ClockSection +import com.android.systemui.keyguard.ui.composable.section.LockSection +import com.android.systemui.keyguard.ui.composable.section.NotificationSection +import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection +import com.android.systemui.keyguard.ui.composable.section.StatusBarSection +import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel +import com.android.systemui.res.R +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoSet +import javax.inject.Inject +import kotlin.math.roundToInt + +/** + * Renders the lockscreen scene when showing with the default layout (e.g. vertical phone form + * factor). + */ +class ShortcutsBesideUdfpsBlueprint +@Inject +constructor( + private val viewModel: LockscreenContentViewModel, + private val statusBarSection: StatusBarSection, + private val clockSection: ClockSection, + private val smartSpaceSection: SmartSpaceSection, + private val notificationSection: NotificationSection, + private val lockSection: LockSection, + private val ambientIndicationSection: AmbientIndicationSection, + private val bottomAreaSection: BottomAreaSection, +) : LockscreenSceneBlueprint { + + override val id: String = "shortcuts-besides-udfps" + + @Composable + override fun SceneScope.Content(modifier: Modifier) { + val context = LocalContext.current + val lockIconBounds = lockSection.lockIconBounds(context) + val isUdfpsVisible = viewModel.isUdfpsVisible + + Box( + modifier = modifier, + ) { + Column( + modifier = Modifier.fillMaxWidth().height { lockIconBounds.top }, + ) { + with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) } + with(clockSection) { SmallClock(modifier = Modifier.fillMaxWidth()) } + with(smartSpaceSection) { SmartSpace(modifier = Modifier.fillMaxWidth()) } + with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) } + with(notificationSection) { + Notifications(modifier = Modifier.fillMaxWidth().weight(1f)) + } + if (!isUdfpsVisible) { + with(ambientIndicationSection) { + AmbientIndication(modifier = Modifier.fillMaxWidth()) + } + } + } + + val shortcutSizePx = + with(LocalDensity.current) { bottomAreaSection.shortcutSizeDp().toSize() } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.fillMaxWidth().offset { + val rowTop = + if (shortcutSizePx.height > lockIconBounds.height()) { + (lockIconBounds.top - + (shortcutSizePx.height + lockIconBounds.height()) / 2) + .roundToInt() + } else { + lockIconBounds.top + } + + IntOffset(0, rowTop) + }, + ) { + Spacer(Modifier.weight(1f)) + + with(bottomAreaSection) { Shortcut(isStart = true) } + + Spacer(Modifier.weight(1f)) + + with(lockSection) { + LockIcon( + modifier = + Modifier.width { lockIconBounds.width() } + .height { lockIconBounds.height() } + ) + } + + Spacer(Modifier.weight(1f)) + + with(bottomAreaSection) { Shortcut(isStart = false) } + + Spacer(Modifier.weight(1f)) + } + + Column(modifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter)) { + if (isUdfpsVisible) { + with(ambientIndicationSection) { + AmbientIndication(modifier = Modifier.fillMaxWidth()) + } + } + + with(bottomAreaSection) { + IndicationArea( + modifier = + Modifier.fillMaxWidth() + .padding( + horizontal = + dimensionResource( + R.dimen.keyguard_affordance_horizontal_offset + ) + ) + .padding( + bottom = + dimensionResource( + R.dimen.keyguard_affordance_vertical_offset + ) + ) + .heightIn(min = shortcutSizeDp().height), + ) + } + } + } + } +} + +@Module +interface ShortcutsBesideUdfpsBlueprintModule { + @Binds + @IntoSet + fun blueprint(blueprint: ShortcutsBesideUdfpsBlueprint): LockscreenSceneBlueprint +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt new file mode 100644 index 000000000000..7545d5fcc2b3 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.keyguard.ui.composable.blueprint + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.android.compose.animation.scene.SceneScope +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoSet +import javax.inject.Inject + +/** + * Renders the lockscreen scene when showing with a split shade (e.g. unfolded foldable and/or + * tablet form factor). + */ +class SplitShadeBlueprint @Inject constructor() : LockscreenSceneBlueprint { + + override val id: String = "split-shade" + + @Composable + override fun SceneScope.Content(modifier: Modifier) { + Box(modifier.background(Color.Black)) { + Text( + text = "TODO(b/316211368): split shade blueprint", + color = Color.White, + modifier = Modifier.align(Alignment.Center), + ) + } + } +} + +@Module +interface SplitShadeBlueprintModule { + @Binds @IntoSet fun blueprint(blueprint: SplitShadeBlueprint): LockscreenSceneBlueprint +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt new file mode 100644 index 000000000000..0e7ac5ec046a --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.keyguard.ui.composable.section + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.SceneScope +import javax.inject.Inject + +class AmbientIndicationSection @Inject constructor() { + @Composable + fun SceneScope.AmbientIndication(modifier: Modifier = Modifier) { + MovableElement( + key = AmbientIndicationElementKey, + modifier = modifier, + ) { + Box( + modifier = Modifier.fillMaxWidth().background(Color.Green), + ) { + Text( + text = "TODO(b/316211368): Ambient indication", + color = Color.White, + modifier = Modifier.align(Alignment.Center), + ) + } + } + } +} + +private val AmbientIndicationElementKey = ElementKey("AmbientIndication") diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt new file mode 100644 index 000000000000..53e4be34dcfa --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt @@ -0,0 +1,225 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.keyguard.ui.composable.section + +import android.view.View +import android.widget.ImageView +import androidx.annotation.IdRes +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.res.ResourcesCompat +import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.SceneScope +import com.android.systemui.animation.view.LaunchableImageView +import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder +import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder +import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea +import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.res.R +import com.android.systemui.statusbar.KeyguardIndicationController +import com.android.systemui.statusbar.VibratorHelper +import javax.inject.Inject +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.flow.Flow + +class BottomAreaSection +@Inject +constructor( + private val viewModel: KeyguardQuickAffordancesCombinedViewModel, + private val falsingManager: FalsingManager, + private val vibratorHelper: VibratorHelper, + private val indicationController: KeyguardIndicationController, + private val indicationAreaViewModel: KeyguardIndicationAreaViewModel, + private val keyguardRootViewModel: KeyguardRootViewModel, +) { + @Composable + fun SceneScope.BottomArea( + modifier: Modifier = Modifier, + ) { + Row( + modifier = + modifier + .padding( + horizontal = + dimensionResource(R.dimen.keyguard_affordance_horizontal_offset) + ) + .padding( + bottom = dimensionResource(R.dimen.keyguard_affordance_vertical_offset) + ), + ) { + Shortcut( + isStart = true, + ) + + IndicationArea( + modifier = Modifier.weight(1f), + ) + + Shortcut( + isStart = false, + ) + } + } + + @Composable + fun SceneScope.Shortcut( + isStart: Boolean, + modifier: Modifier = Modifier, + ) { + MovableElement( + key = if (isStart) StartButtonElementKey else EndButtonElementKey, + modifier = modifier, + ) { + Shortcut( + viewId = if (isStart) R.id.start_button else R.id.end_button, + viewModel = if (isStart) viewModel.startButton else viewModel.endButton, + transitionAlpha = viewModel.transitionAlpha, + falsingManager = falsingManager, + vibratorHelper = vibratorHelper, + indicationController = indicationController, + ) + } + } + + @Composable + fun SceneScope.IndicationArea( + modifier: Modifier = Modifier, + ) { + MovableElement( + key = IndicationAreaElementKey, + modifier = modifier, + ) { + IndicationArea( + indicationAreaViewModel = indicationAreaViewModel, + keyguardRootViewModel = keyguardRootViewModel, + indicationController = indicationController, + ) + } + } + + @Composable + fun shortcutSizeDp(): DpSize { + return DpSize( + width = dimensionResource(R.dimen.keyguard_affordance_fixed_width), + height = dimensionResource(R.dimen.keyguard_affordance_fixed_height), + ) + } + + @Composable + private fun Shortcut( + @IdRes viewId: Int, + viewModel: Flow<KeyguardQuickAffordanceViewModel>, + transitionAlpha: Flow<Float>, + falsingManager: FalsingManager, + vibratorHelper: VibratorHelper, + indicationController: KeyguardIndicationController, + modifier: Modifier = Modifier, + ) { + val (binding, setBinding) = mutableStateOf<KeyguardQuickAffordanceViewBinder.Binding?>(null) + + AndroidView( + factory = { context -> + val padding = + context.resources.getDimensionPixelSize( + R.dimen.keyguard_affordance_fixed_padding + ) + val view = + LaunchableImageView(context, null).apply { + id = viewId + scaleType = ImageView.ScaleType.FIT_CENTER + background = + ResourcesCompat.getDrawable( + context.resources, + R.drawable.keyguard_bottom_affordance_bg, + context.theme + ) + foreground = + ResourcesCompat.getDrawable( + context.resources, + R.drawable.keyguard_bottom_affordance_selected_border, + context.theme + ) + visibility = View.INVISIBLE + setPadding(padding, padding, padding, padding) + } + + setBinding( + KeyguardQuickAffordanceViewBinder.bind( + view, + viewModel, + transitionAlpha, + falsingManager, + vibratorHelper, + ) { + indicationController.showTransientIndication(it) + } + ) + + view + }, + onRelease = { binding?.destroy() }, + modifier = + modifier.size( + width = shortcutSizeDp().width, + height = shortcutSizeDp().height, + ) + ) + } + + @Composable + private fun IndicationArea( + indicationAreaViewModel: KeyguardIndicationAreaViewModel, + keyguardRootViewModel: KeyguardRootViewModel, + indicationController: KeyguardIndicationController, + modifier: Modifier = Modifier, + ) { + val (disposable, setDisposable) = mutableStateOf<DisposableHandle?>(null) + + AndroidView( + factory = { context -> + val view = KeyguardIndicationArea(context, null) + setDisposable( + KeyguardIndicationAreaBinder.bind( + view = view, + viewModel = indicationAreaViewModel, + keyguardRootViewModel = keyguardRootViewModel, + indicationController = indicationController, + ) + ) + view + }, + onRelease = { disposable?.dispose() }, + modifier = modifier.fillMaxWidth(), + ) + } +} + +private val StartButtonElementKey = ElementKey("StartButton") +private val EndButtonElementKey = ElementKey("EndButton") +private val IndicationAreaElementKey = ElementKey("IndicationArea") diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt new file mode 100644 index 000000000000..eaf8063b6f15 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.keyguard.ui.composable.section + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.SceneScope +import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel +import javax.inject.Inject + +class ClockSection +@Inject +constructor( + private val viewModel: KeyguardClockViewModel, +) { + @Composable + fun SceneScope.SmallClock(modifier: Modifier = Modifier) { + if (viewModel.useLargeClock) { + return + } + + MovableElement( + key = ClockElementKey, + modifier = modifier, + ) { + Box( + modifier = Modifier.fillMaxWidth().background(Color.Magenta), + ) { + Text( + text = "TODO(b/316211368): Small clock", + color = Color.White, + modifier = Modifier.align(Alignment.Center), + ) + } + } + } + + @Composable + fun SceneScope.LargeClock(modifier: Modifier = Modifier) { + if (!viewModel.useLargeClock) { + return + } + + MovableElement( + key = ClockElementKey, + modifier = modifier, + ) { + Box( + modifier = Modifier.fillMaxWidth().background(Color.Blue), + ) { + Text( + text = "TODO(b/316211368): Large clock", + color = Color.White, + modifier = Modifier.align(Alignment.Center), + ) + } + } + } +} + +private val ClockElementKey = ElementKey("Clock") diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt new file mode 100644 index 000000000000..8bbe424b9a49 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.keyguard.ui.composable.section + +import android.content.Context +import android.graphics.Point +import android.graphics.Rect +import android.util.DisplayMetrics +import android.view.WindowManager +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.SceneScope +import com.android.systemui.biometrics.AuthController +import com.android.systemui.flags.FeatureFlagsClassic +import com.android.systemui.flags.Flags +import com.android.systemui.res.R +import javax.inject.Inject + +class LockSection +@Inject +constructor( + private val windowManager: WindowManager, + private val authController: AuthController, + private val featureFlags: FeatureFlagsClassic, +) { + @Composable + fun SceneScope.LockIcon(modifier: Modifier = Modifier) { + MovableElement( + key = LockIconElementKey, + modifier = modifier, + ) { + Box( + modifier = Modifier.background(Color.Red), + ) { + Text( + text = "TODO(b/316211368): Lock", + color = Color.White, + modifier = Modifier.align(Alignment.Center), + ) + } + } + } + + /** + * Returns the bounds of the lock icon, in window view coordinates. + * + * On devices that support UDFPS (under-display fingerprint sensor), the bounds of the icon are + * the same as the bounds of the sensor. + */ + fun lockIconBounds( + context: Context, + ): Rect { + val windowViewBounds = windowManager.currentWindowMetrics.bounds + var widthPx = windowViewBounds.right.toFloat() + if (featureFlags.isEnabled(Flags.LOCKSCREEN_ENABLE_LANDSCAPE)) { + val insets = windowManager.currentWindowMetrics.windowInsets + // Assumed to be initially neglected as there are no left or right insets in portrait. + // However, on landscape, these insets need to included when calculating the midpoint. + @Suppress("DEPRECATION") + widthPx -= (insets.systemWindowInsetLeft + insets.systemWindowInsetRight).toFloat() + } + val defaultDensity = + DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() / + DisplayMetrics.DENSITY_DEFAULT.toFloat() + val lockIconRadiusPx = (defaultDensity * 36).toInt() + + val udfpsLocation = authController.udfpsLocation + return if (authController.isUdfpsSupported && udfpsLocation != null) { + centerLockIcon(udfpsLocation, authController.udfpsRadius) + } else { + val scaleFactor = authController.scaleFactor + val bottomPaddingPx = + context.resources.getDimensionPixelSize(R.dimen.lock_icon_margin_bottom) + val heightPx = windowViewBounds.bottom.toFloat() + + centerLockIcon( + Point( + (widthPx / 2).toInt(), + (heightPx - ((bottomPaddingPx + lockIconRadiusPx) * scaleFactor)).toInt() + ), + lockIconRadiusPx * scaleFactor + ) + } + } + + private fun centerLockIcon( + center: Point, + radius: Float, + ): Rect { + return Rect().apply { + set( + center.x - radius.toInt(), + center.y - radius.toInt(), + center.x + radius.toInt(), + center.y + radius.toInt(), + ) + } + } +} + +private val LockIconElementKey = ElementKey("LockIcon") diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt new file mode 100644 index 000000000000..f135be260cbd --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.keyguard.ui.composable.section + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.SceneScope +import javax.inject.Inject + +class NotificationSection @Inject constructor() { + @Composable + fun SceneScope.Notifications(modifier: Modifier = Modifier) { + MovableElement( + key = NotificationsElementKey, + modifier = modifier, + ) { + Box( + modifier = Modifier.fillMaxSize().background(Color.Yellow), + ) { + Text( + text = "TODO(b/316211368): Notifications", + color = Color.White, + modifier = Modifier.align(Alignment.Center), + ) + } + } + } +} + +private val NotificationsElementKey = ElementKey("Notifications") diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt new file mode 100644 index 000000000000..ca03af566fb2 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.keyguard.ui.composable.section + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.SceneScope +import javax.inject.Inject + +class SmartSpaceSection @Inject constructor() { + @Composable + fun SceneScope.SmartSpace(modifier: Modifier = Modifier) { + MovableElement(key = SmartSpaceElementKey, modifier = modifier) { + Box( + modifier = Modifier.fillMaxWidth().background(Color.Cyan), + ) { + Text( + text = "TODO(b/316211368): Smart space", + color = Color.White, + modifier = Modifier.align(Alignment.Center), + ) + } + } + } +} + +private val SmartSpaceElementKey = ElementKey("SmartSpace") diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt new file mode 100644 index 000000000000..6811eb4cea5c --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.keyguard.ui.composable.section + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.viewinterop.AndroidView +import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.SceneScope +import com.android.compose.modifiers.height +import com.android.keyguard.dagger.KeyguardStatusBarViewComponent +import com.android.systemui.res.R +import com.android.systemui.shade.NotificationPanelView +import com.android.systemui.shade.ShadeViewStateProvider +import com.android.systemui.statusbar.phone.KeyguardStatusBarView +import com.android.systemui.util.Utils +import dagger.Lazy +import javax.inject.Inject + +class StatusBarSection +@Inject +constructor( + private val componentFactory: KeyguardStatusBarViewComponent.Factory, + private val notificationPanelView: Lazy<NotificationPanelView>, +) { + @Composable + fun SceneScope.StatusBar(modifier: Modifier = Modifier) { + val context = LocalContext.current + + MovableElement( + key = StatusBarElementKey, + modifier = modifier, + ) { + AndroidView( + factory = { + notificationPanelView.get().findViewById<View>(R.id.keyguard_header)?.let { + (it.parent as ViewGroup).removeView(it) + } + + val provider = + object : ShadeViewStateProvider { + override val lockscreenShadeDragProgress: Float = 0f + override val panelViewExpandedHeight: Float = 0f + override fun shouldHeadsUpBeVisible(): Boolean { + return false + } + } + + @SuppressLint("InflateParams") + val view = + LayoutInflater.from(context) + .inflate( + R.layout.keyguard_status_bar, + null, + false, + ) as KeyguardStatusBarView + componentFactory.build(view, provider).keyguardStatusBarViewController.init() + view + }, + modifier = + Modifier.fillMaxWidth().height { + Utils.getStatusBarHeaderHeightKeyguard(context) + }, + ) + } + } +} + +private val StatusBarElementKey = ElementKey("StatusBar") diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index 9d9b0a9fe496..a85d9bff283e 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -16,6 +16,7 @@ package com.android.compose.animation.scene +import android.graphics.Picture import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue @@ -66,12 +67,21 @@ internal class Element(val key: ElementKey) { * The movable content of this element, if this element is composed using * [SceneScope.MovableElement]. */ - val movableContent by - // This is only accessed from the composition (main) thread, so no need to use the default - // lock of lazy {} to synchronize. - lazy(mode = LazyThreadSafetyMode.NONE) { - movableContentOf { content: @Composable () -> Unit -> content() } - } + private var _movableContent: (@Composable (@Composable () -> Unit) -> Unit)? = null + val movableContent: @Composable (@Composable () -> Unit) -> Unit + get() = + _movableContent + ?: movableContentOf { content: @Composable () -> Unit -> content() } + .also { _movableContent = it } + + /** + * The [Picture] to which we save the last drawing commands of this element, if it is movable. + * This is necessary because the content of this element might not be composed in the scene it + * should currently be drawn. + */ + private var _picture: Picture? = null + val picture: Picture + get() = _picture ?: Picture().also { _picture = it } override fun toString(): String { return "Element(key=$key)" @@ -521,11 +531,6 @@ private fun IntermediateMeasureScope.place( sceneValues.targetOffset = targetOffsetInScene } - // No need to place the element in this scene if we don't want to draw it anyways. - if (!shouldDrawElement(layoutImpl, scene, element)) { - return - } - val currentOffset = lookaheadScopeCoordinates.localPositionOf(coords, Offset.Zero) val lastSharedValues = element.lastSharedValues val lastValues = sceneValues.lastValues @@ -548,6 +553,13 @@ private fun IntermediateMeasureScope.place( lastSharedValues.offset = targetOffset lastValues.offset = targetOffset + // No need to place the element in this scene if we don't want to draw it anyways. Note that + // it's still important to compute the target offset and update lastValues, otherwise it + // will be out of date. + if (!shouldDrawElement(layoutImpl, scene, element)) { + return + } + val offset = (targetOffset - currentOffset).round() if (isElementOpaque(layoutImpl, element, scene, sceneValues)) { // TODO(b/291071158): Call placeWithLayer() if offset != IntOffset.Zero and size is not diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt index 306f27626e19..49df2f6b6062 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt @@ -16,7 +16,6 @@ package com.android.compose.animation.scene -import android.graphics.Picture import android.util.Log import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer @@ -60,7 +59,7 @@ internal fun MovableElement( // The [Picture] to which we save the last drawing commands of this element. This is // necessary because the content of this element might not be composed in this scene, in // which case we still need to draw it. - val picture = remember { Picture() } + val picture = element.picture // Whether we should compose the movable element here. The scene picker logic to know in // which scene we should compose/draw a movable element might depend on the current diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt index 6a7a3a00d4fe..30e50a972230 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.layout.intermediateLayout import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.IntSize import androidx.compose.ui.zIndex +import com.android.compose.animation.scene.modifiers.noResizeDuringTransitions /** A scene in a [SceneTransitionLayout]. */ @Stable @@ -152,4 +153,8 @@ private class SceneScopeImpl( bounds: ElementKey, shape: Shape ): Modifier = punchHole(layoutImpl, element, bounds, shape) + + override fun Modifier.noResizeDuringTransitions(): Modifier { + return noResizeDuringTransitions(layoutState = layoutImpl.state) + } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 3608e374fdbc..5eb339e4a5e4 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -65,11 +65,11 @@ fun SceneTransitionLayout( SceneTransitionLayoutForTesting( currentScene, onChangeScene, + modifier, transitions, state, edgeDetector, transitionInterceptionThreshold, - modifier, onLayoutImpl = null, scenes, ) @@ -205,6 +205,12 @@ interface SceneScope { * the result. */ fun Modifier.punchHole(element: ElementKey, bounds: ElementKey, shape: Shape): Modifier + + /** + * Don't resize during transitions. This can for instance be used to make sure that scrollable + * lists keep a constant size during transitions even if its elements are growing/shrinking. + */ + fun Modifier.noResizeDuringTransitions(): Modifier } // TODO(b/291053742): Add animateSharedValueAsState(targetValue) without any ValueKey and ElementKey @@ -257,12 +263,12 @@ enum class SwipeDirection(val orientation: Orientation) { internal fun SceneTransitionLayoutForTesting( currentScene: SceneKey, onChangeScene: (SceneKey) -> Unit, - transitions: SceneTransitions, - state: SceneTransitionLayoutState, - edgeDetector: EdgeDetector, - transitionInterceptionThreshold: Float, - modifier: Modifier, - onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)?, + modifier: Modifier = Modifier, + transitions: SceneTransitions = transitions {}, + state: SceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) }, + edgeDetector: EdgeDetector = DefaultEdgeDetector, + transitionInterceptionThreshold: Float = 0f, + onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null, scenes: SceneTransitionLayoutScope.() -> Unit, ) { val density = LocalDensity.current @@ -280,6 +286,10 @@ internal fun SceneTransitionLayoutForTesting( .also { onLayoutImpl?.invoke(it) } } + // TODO(b/317014852): Move this into the SideEffect {} again once STLImpl.scenes is not a + // SnapshotStateMap anymore. + layoutImpl.updateScenes(scenes) + val targetSceneChannel = remember { Channel<SceneKey>(Channel.CONFLATED) } SideEffect { if (state != layoutImpl.state) { @@ -293,7 +303,6 @@ internal fun SceneTransitionLayoutForTesting( (state as SceneTransitionLayoutStateImpl).transitions = transitions layoutImpl.density = density layoutImpl.edgeDetector = edgeDetector - layoutImpl.updateScenes(scenes) state.transitions = transitions diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index c99c3250bbb1..45e1a0fa8f77 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -46,7 +46,12 @@ internal class SceneTransitionLayoutImpl( builder: SceneTransitionLayoutScope.() -> Unit, coroutineScope: CoroutineScope, ) { - internal val scenes = mutableMapOf<SceneKey, Scene>() + /** + * The map of [Scene]s. + * + * TODO(b/317014852): Make this a normal MutableMap instead. + */ + internal val scenes = SnapshotStateMap<SceneKey, Scene>() /** * The map of [Element]s. diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/modifiers/Size.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/modifiers/Size.kt new file mode 100644 index 000000000000..bd36cb8655ac --- /dev/null +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/modifiers/Size.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 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. + */ + +package com.android.compose.animation.scene.modifiers + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.intermediateLayout +import androidx.compose.ui.unit.Constraints +import com.android.compose.animation.scene.SceneTransitionLayoutState + +@OptIn(ExperimentalComposeUiApi::class) +internal fun Modifier.noResizeDuringTransitions(layoutState: SceneTransitionLayoutState): Modifier { + return intermediateLayout { measurable, constraints -> + if (layoutState.currentTransition == null) { + return@intermediateLayout measurable.measure(constraints).run { + layout(width, height) { place(0, 0) } + } + } + + // Make sure that this layout node has the same size than when we are at rest. + val sizeAtRest = lookaheadSize + measurable.measure(Constraints.fixed(sizeAtRest.width, sizeAtRest.height)).run { + layout(width, height) { place(0, 0) } + } + } +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index d332910c54b1..da5a0a04ed63 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -16,6 +16,7 @@ package com.android.compose.animation.scene +import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box @@ -30,16 +31,21 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.layout.intermediateLayout +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.test.subjects.DpOffsetSubject +import com.android.compose.test.subjects.assertThat import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -259,11 +265,6 @@ class ElementTest { SceneTransitionLayoutForTesting( currentScene = currentScene, onChangeScene = { currentScene = it }, - transitions = remember { transitions {} }, - state = remember { SceneTransitionLayoutState(currentScene) }, - edgeDetector = DefaultEdgeDetector, - modifier = Modifier, - transitionInterceptionThreshold = 0f, onLayoutImpl = { nullableLayoutImpl = it }, ) { scene(TestScenes.SceneA) { /* Nothing */} @@ -429,11 +430,6 @@ class ElementTest { SceneTransitionLayoutForTesting( currentScene = TestScenes.SceneA, onChangeScene = {}, - transitions = remember { transitions {} }, - state = remember { SceneTransitionLayoutState(TestScenes.SceneA) }, - edgeDetector = DefaultEdgeDetector, - modifier = Modifier, - transitionInterceptionThreshold = 0f, onLayoutImpl = { nullableLayoutImpl = it }, ) { scene(TestScenes.SceneA) { Box(Modifier.element(key)) } @@ -484,11 +480,6 @@ class ElementTest { SceneTransitionLayoutForTesting( currentScene = TestScenes.SceneA, onChangeScene = {}, - transitions = remember { transitions {} }, - state = remember { SceneTransitionLayoutState(TestScenes.SceneA) }, - edgeDetector = DefaultEdgeDetector, - modifier = Modifier, - transitionInterceptionThreshold = 0f, onLayoutImpl = { nullableLayoutImpl = it }, ) { scene(TestScenes.SceneA) { @@ -574,4 +565,86 @@ class ElementTest { after { assertThat(fooCompositions).isEqualTo(1) } } } + + @Test + fun sharedElementOffsetIsUpdatedEvenWhenNotPlaced() { + var nullableLayoutImpl: SceneTransitionLayoutImpl? = null + var density: Density? = null + + fun layoutImpl() = nullableLayoutImpl ?: error("nullableLayoutImpl was not set") + + fun density() = density ?: error("density was not set") + + fun Offset.toDpOffset() = with(density()) { DpOffset(x.toDp(), y.toDp()) } + + fun foo() = layoutImpl().elements[TestElements.Foo] ?: error("Foo not in elements map") + + fun Element.lastSharedOffset() = lastSharedValues.offset.toDpOffset() + + fun Element.lastOffsetIn(scene: SceneKey) = + (sceneValues[scene] ?: error("$scene not in sceneValues map")) + .lastValues + .offset + .toDpOffset() + + rule.testTransition( + from = TestScenes.SceneA, + to = TestScenes.SceneB, + transitionLayout = { currentScene, onChangeScene -> + density = LocalDensity.current + + SceneTransitionLayoutForTesting( + currentScene = currentScene, + onChangeScene = onChangeScene, + onLayoutImpl = { nullableLayoutImpl = it }, + transitions = + transitions { + from(TestScenes.SceneA, to = TestScenes.SceneB) { + spec = tween(durationMillis = 4 * 16, easing = LinearEasing) + } + } + ) { + scene(TestScenes.SceneA) { Box(Modifier.element(TestElements.Foo)) } + scene(TestScenes.SceneB) { + Box(Modifier.offset(x = 40.dp, y = 80.dp).element(TestElements.Foo)) + } + } + } + ) { + val tolerance = DpOffsetSubject.DefaultTolerance + + before { + val expected = DpOffset(0.dp, 0.dp) + assertThat(foo().lastSharedOffset()).isWithin(tolerance).of(expected) + assertThat(foo().lastOffsetIn(TestScenes.SceneA)).isWithin(tolerance).of(expected) + } + + at(16) { + val expected = DpOffset(10.dp, 20.dp) + assertThat(foo().lastSharedOffset()).isWithin(tolerance).of(expected) + assertThat(foo().lastOffsetIn(TestScenes.SceneA)).isWithin(tolerance).of(expected) + assertThat(foo().lastOffsetIn(TestScenes.SceneB)).isWithin(tolerance).of(expected) + } + + at(32) { + val expected = DpOffset(20.dp, 40.dp) + assertThat(foo().lastSharedOffset()).isWithin(tolerance).of(expected) + assertThat(foo().lastOffsetIn(TestScenes.SceneA)).isWithin(tolerance).of(expected) + assertThat(foo().lastOffsetIn(TestScenes.SceneB)).isWithin(tolerance).of(expected) + } + + at(48) { + val expected = DpOffset(30.dp, 60.dp) + assertThat(foo().lastSharedOffset()).isWithin(tolerance).of(expected) + assertThat(foo().lastOffsetIn(TestScenes.SceneA)).isWithin(tolerance).of(expected) + assertThat(foo().lastOffsetIn(TestScenes.SceneB)).isWithin(tolerance).of(expected) + } + + after { + val expected = DpOffset(40.dp, 80.dp) + assertThat(foo().lastSharedOffset()).isWithin(tolerance).of(expected) + assertThat(foo().lastOffsetIn(TestScenes.SceneB)).isWithin(tolerance).of(expected) + } + } + } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/modifiers/SizeTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/modifiers/SizeTest.kt new file mode 100644 index 000000000000..2c159d15fa66 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/modifiers/SizeTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 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. + */ + +package com.android.compose.animation.scene.modifiers + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.TestElements +import com.android.compose.animation.scene.element +import com.android.compose.animation.scene.testTransition +import com.android.compose.test.assertSizeIsEqualTo +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SizeTest { + @get:Rule val rule = createComposeRule() + + @Test + fun noResizeDuringTransitions() { + // The tag for the parent of the shared Foo element. + val parentTag = "parent" + + rule.testTransition( + fromSceneContent = { Box(Modifier.element(TestElements.Foo).size(100.dp)) }, + toSceneContent = { + // Don't resize the parent of Foo during transitions so that it's always the same + // size as when there is no transition (200dp). + Box(Modifier.noResizeDuringTransitions().testTag(parentTag)) { + Box(Modifier.element(TestElements.Foo).size(200.dp)) + } + }, + transition = { spec = tween(durationMillis = 4 * 16, easing = LinearEasing) }, + ) { + at(16) { rule.onNodeWithTag(parentTag).assertSizeIsEqualTo(200.dp, 200.dp) } + at(32) { rule.onNodeWithTag(parentTag).assertSizeIsEqualTo(200.dp, 200.dp) } + at(48) { rule.onNodeWithTag(parentTag).assertSizeIsEqualTo(200.dp, 200.dp) } + after { rule.onNodeWithTag(parentTag).assertSizeIsEqualTo(200.dp, 200.dp) } + } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt index 0b7c3f987db2..26da1f025544 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt @@ -17,56 +17,39 @@ package com.android.systemui.shared.notifications.data.repository import android.provider.Settings -import com.android.systemui.shared.notifications.shared.model.NotificationSettingsModel import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext -/** Provides access to state related to notifications. */ +/** Provides access to state related to notification settings. */ class NotificationSettingsRepository( scope: CoroutineScope, private val backgroundDispatcher: CoroutineDispatcher, private val secureSettingsRepository: SecureSettingsRepository, ) { /** The current state of the notification setting. */ - val settings: SharedFlow<NotificationSettingsModel> = + val isShowNotificationsOnLockScreenEnabled: StateFlow<Boolean> = secureSettingsRepository .intSetting( name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, ) - .map { lockScreenShowNotificationsInt -> - NotificationSettingsModel( - isShowNotificationsOnLockScreenEnabled = lockScreenShowNotificationsInt == 1, - ) - } - .shareIn( + .map { it == 1 } + .stateIn( scope = scope, started = SharingStarted.WhileSubscribed(), - replay = 1, + initialValue = false, ) - suspend fun getSettings(): NotificationSettingsModel { - return withContext(backgroundDispatcher) { - NotificationSettingsModel( - isShowNotificationsOnLockScreenEnabled = - secureSettingsRepository.get( - name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, - defaultValue = 0, - ) == 1 - ) - } - } - - suspend fun setSettings(model: NotificationSettingsModel) { + suspend fun setShowNotificationsOnLockscreenEnabled(enabled: Boolean) { withContext(backgroundDispatcher) { secureSettingsRepository.set( name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, - value = if (model.isShowNotificationsOnLockScreenEnabled) 1 else 0, + value = if (enabled) 1 else 0, ) } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt index 21f3acaf15ff..9ec6ec8d3811 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt @@ -17,32 +17,23 @@ package com.android.systemui.shared.notifications.domain.interactor import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository -import com.android.systemui.shared.notifications.shared.model.NotificationSettingsModel -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow /** Encapsulates business logic for interacting with notification settings. */ class NotificationSettingsInteractor( private val repository: NotificationSettingsRepository, ) { - /** The current state of the notification setting. */ - val settings: Flow<NotificationSettingsModel> = repository.settings + /** Should notifications be visible on the lockscreen? */ + val isShowNotificationsOnLockScreenEnabled: StateFlow<Boolean> = + repository.isShowNotificationsOnLockScreenEnabled - /** Toggles the setting to show or hide notifications on the lock screen. */ - suspend fun toggleShowNotificationsOnLockScreenEnabled() { - val currentModel = repository.getSettings() - setSettings( - currentModel.copy( - isShowNotificationsOnLockScreenEnabled = - !currentModel.isShowNotificationsOnLockScreenEnabled, - ) - ) - } - - suspend fun setSettings(model: NotificationSettingsModel) { - repository.setSettings(model) + suspend fun setShowNotificationsOnLockscreenEnabled(enabled: Boolean) { + repository.setShowNotificationsOnLockscreenEnabled(enabled) } - suspend fun getSettings(): NotificationSettingsModel { - return repository.getSettings() + /** Toggles the setting to show or hide notifications on the lock screen. */ + suspend fun toggleShowNotificationsOnLockscreenEnabled() { + val current = repository.isShowNotificationsOnLockScreenEnabled.value + repository.setShowNotificationsOnLockscreenEnabled(!current) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index dddcf18c1ede..a4b55e765c92 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -90,7 +90,6 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; -import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -124,8 +123,6 @@ import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.List; -import javax.inject.Provider; - @SmallTest @RunWith(AndroidJUnit4.class) @RunWithLooper(setAsMainLooper = true) @@ -216,8 +213,6 @@ public class UdfpsControllerTest extends SysuiTestCase { @Mock private UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate; @Mock - private Provider<UdfpsKeyguardViewModels> mUdfpsKeyguardViewModels; - @Mock private SelectedUserInteractor mSelectedUserInteractor; // Capture listeners so that they can be used to send events @@ -339,7 +334,6 @@ public class UdfpsControllerTest extends SysuiTestCase { mInputManager, mock(KeyguardFaceAuthInteractor.class), mUdfpsKeyguardAccessibilityDelegate, - mUdfpsKeyguardViewModels, mSelectedUserInteractor, mFpsUnlockTracker, mKeyguardTransitionInteractor, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt index 0ab596c82d6f..1f8854ffc1c8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt @@ -616,4 +616,28 @@ class UdfpsKeyguardViewLegacyControllerWithCoroutinesTest : .onDozeAmountChanged(eq(.3f), eq(.3f), eq(ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN)) job.cancel() } + + @Test + fun bouncerToAod_dozeAmountChanged() = + testScope.runTest { + // GIVEN view is attached + mController.onViewAttached() + Mockito.reset(mView) + + val job = mController.listenForPrimaryBouncerToAodTransitions(this) + // WHEN alternate bouncer to aod transition in progress + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.AOD, + value = .3f, + transitionState = TransitionState.RUNNING + ) + ) + runCurrent() + + // THEN doze amount is updated to + verify(mView).onDozeAmountChanged(eq(.3f), eq(.3f), eq(ANIMATE_APPEAR_ON_SCREEN_OFF)) + job.cancel() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt index cf076c557765..a84b9fa32d85 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt @@ -24,16 +24,19 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues -import com.android.systemui.qs.external.FakeCustomTileStatePersister +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope import com.android.systemui.qs.external.TileServiceKey import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tiles.impl.custom.TileSubject.Companion.assertThat import com.android.systemui.qs.tiles.impl.custom.commons.copy +import com.android.systemui.qs.tiles.impl.custom.customTileStatePersister import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults +import com.android.systemui.qs.tiles.impl.custom.packageManagerAdapterFacade +import com.android.systemui.qs.tiles.impl.custom.tileSpec import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -44,194 +47,260 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) class CustomTileRepositoryTest : SysuiTestCase() { - private val testScope = TestScope() - - private val persister = FakeCustomTileStatePersister() - + private val kosmos = Kosmos().apply { tileSpec = TileSpec.create(TEST_COMPONENT) } private val underTest: CustomTileRepository = - CustomTileRepositoryImpl( - TileSpec.create(TEST_COMPONENT), - persister, - testScope.testScheduler, - ) + with(kosmos) { + CustomTileRepositoryImpl( + tileSpec, + customTileStatePersister, + packageManagerAdapterFacade.packageManagerAdapter, + testScope.testScheduler, + ) + } @Test fun persistableTileIsRestoredForUser() = - testScope.runTest { - persister.persistState(TEST_TILE_KEY_1, TEST_TILE_1) - persister.persistState(TEST_TILE_KEY_2, TEST_TILE_2) + with(kosmos) { + testScope.runTest { + customTileStatePersister.persistState(TEST_TILE_KEY_1, TEST_TILE_1) + customTileStatePersister.persistState(TEST_TILE_KEY_2, TEST_TILE_2) - underTest.restoreForTheUserIfNeeded(TEST_USER_1, true) - runCurrent() + underTest.restoreForTheUserIfNeeded(TEST_USER_1, true) + runCurrent() - assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1) - assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1) + assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1) + assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1) + } } @Test fun notPersistableTileIsNotRestored() = - testScope.runTest { - persister.persistState(TEST_TILE_KEY_1, TEST_TILE_1) - val tiles = collectValues(underTest.getTiles(TEST_USER_1)) + with(kosmos) { + testScope.runTest { + customTileStatePersister.persistState(TEST_TILE_KEY_1, TEST_TILE_1) + val tiles = collectValues(underTest.getTiles(TEST_USER_1)) - underTest.restoreForTheUserIfNeeded(TEST_USER_1, false) - runCurrent() + underTest.restoreForTheUserIfNeeded(TEST_USER_1, false) + runCurrent() - assertThat(tiles()).isEmpty() + assertThat(tiles()).isEmpty() + } } @Test fun emptyPersistedStateIsHandled() = - testScope.runTest { - val tiles = collectValues(underTest.getTiles(TEST_USER_1)) + with(kosmos) { + testScope.runTest { + val tiles = collectValues(underTest.getTiles(TEST_USER_1)) - underTest.restoreForTheUserIfNeeded(TEST_USER_1, true) - runCurrent() + underTest.restoreForTheUserIfNeeded(TEST_USER_1, true) + runCurrent() - assertThat(tiles()).isEmpty() + assertThat(tiles()).isEmpty() + } } @Test fun updatingWithPersistableTilePersists() = - testScope.runTest { - underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true) - runCurrent() + with(kosmos) { + testScope.runTest { + underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true) + runCurrent() - assertThat(persister.readState(TEST_TILE_KEY_1)).isEqualTo(TEST_TILE_1) + assertThat(customTileStatePersister.readState(TEST_TILE_KEY_1)) + .isEqualTo(TEST_TILE_1) + } } @Test fun updatingWithNotPersistableTileDoesntPersist() = - testScope.runTest { - underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, false) - runCurrent() + with(kosmos) { + testScope.runTest { + underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, false) + runCurrent() - assertThat(persister.readState(TEST_TILE_KEY_1)).isNull() + assertThat(customTileStatePersister.readState(TEST_TILE_KEY_1)).isNull() + } } @Test fun updateWithTileEmits() = - testScope.runTest { - underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true) - runCurrent() + with(kosmos) { + testScope.runTest { + underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true) + runCurrent() - assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1) - assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1) + assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1) + assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1) + } } @Test fun updatingPeristableWithDefaultsPersists() = - testScope.runTest { - underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true) - runCurrent() + with(kosmos) { + testScope.runTest { + underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true) + runCurrent() - assertThat(persister.readState(TEST_TILE_KEY_1)).isEqualTo(TEST_TILE_1) + assertThat(customTileStatePersister.readState(TEST_TILE_KEY_1)) + .isEqualTo(TEST_TILE_1) + } } @Test fun updatingNotPersistableWithDefaultsDoesntPersist() = - testScope.runTest { - underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, false) - runCurrent() + with(kosmos) { + testScope.runTest { + underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, false) + runCurrent() - assertThat(persister.readState(TEST_TILE_KEY_1)).isNull() + assertThat(customTileStatePersister.readState(TEST_TILE_KEY_1)).isNull() + } } @Test fun updatingPeristableWithErrorDefaultsDoesntPersist() = - testScope.runTest { - underTest.updateWithDefaults(TEST_USER_1, CustomTileDefaults.Error, true) - runCurrent() + with(kosmos) { + testScope.runTest { + underTest.updateWithDefaults(TEST_USER_1, CustomTileDefaults.Error, true) + runCurrent() - assertThat(persister.readState(TEST_TILE_KEY_1)).isNull() + assertThat(customTileStatePersister.readState(TEST_TILE_KEY_1)).isNull() + } } @Test fun updateWithDefaultsEmits() = - testScope.runTest { - underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true) - runCurrent() + with(kosmos) { + testScope.runTest { + underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true) + runCurrent() - assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1) - assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1) + assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1) + assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1) + } } @Test fun getTileForAnotherUserReturnsNull() = - testScope.runTest { - underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true) - runCurrent() + with(kosmos) { + testScope.runTest { + underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true) + runCurrent() - assertThat(underTest.getTile(TEST_USER_2)).isNull() + assertThat(underTest.getTile(TEST_USER_2)).isNull() + } } @Test fun getTilesForAnotherUserEmpty() = - testScope.runTest { - val tiles = collectValues(underTest.getTiles(TEST_USER_2)) + with(kosmos) { + testScope.runTest { + val tiles = collectValues(underTest.getTiles(TEST_USER_2)) - underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true) - runCurrent() + underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true) + runCurrent() - assertThat(tiles()).isEmpty() + assertThat(tiles()).isEmpty() + } } @Test fun updatingWithTileForTheSameUserAddsData() = - testScope.runTest { - underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true) - runCurrent() - - underTest.updateWithTile(TEST_USER_1, Tile().apply { subtitle = "test_subtitle" }, true) - runCurrent() - - val expectedTile = TEST_TILE_1.copy().apply { subtitle = "test_subtitle" } - assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(expectedTile) - assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(expectedTile) + with(kosmos) { + testScope.runTest { + underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true) + runCurrent() + + underTest.updateWithTile( + TEST_USER_1, + Tile().apply { subtitle = "test_subtitle" }, + true + ) + runCurrent() + + val expectedTile = TEST_TILE_1.copy().apply { subtitle = "test_subtitle" } + assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(expectedTile) + assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(expectedTile) + } } @Test fun updatingWithTileForAnotherUserOverridesTile() = - testScope.runTest { - underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true) - runCurrent() - - val tiles = collectValues(underTest.getTiles(TEST_USER_2)) - underTest.updateWithTile(TEST_USER_2, TEST_TILE_2, true) - runCurrent() - - assertThat(underTest.getTile(TEST_USER_2)).isEqualTo(TEST_TILE_2) - assertThat(tiles()).hasSize(1) - assertThat(tiles().last()).isEqualTo(TEST_TILE_2) + with(kosmos) { + testScope.runTest { + underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true) + runCurrent() + + val tiles = collectValues(underTest.getTiles(TEST_USER_2)) + underTest.updateWithTile(TEST_USER_2, TEST_TILE_2, true) + runCurrent() + + assertThat(underTest.getTile(TEST_USER_2)).isEqualTo(TEST_TILE_2) + assertThat(tiles()).hasSize(1) + assertThat(tiles().last()).isEqualTo(TEST_TILE_2) + } } @Test fun updatingWithDefaultsForTheSameUserAddsData() = - testScope.runTest { - underTest.updateWithTile(TEST_USER_1, Tile().apply { subtitle = "test_subtitle" }, true) - runCurrent() - - underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true) - runCurrent() - - val expectedTile = TEST_TILE_1.copy().apply { subtitle = "test_subtitle" } - assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(expectedTile) - assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(expectedTile) + with(kosmos) { + testScope.runTest { + underTest.updateWithTile( + TEST_USER_1, + Tile().apply { subtitle = "test_subtitle" }, + true + ) + runCurrent() + + underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true) + runCurrent() + + val expectedTile = TEST_TILE_1.copy().apply { subtitle = "test_subtitle" } + assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(expectedTile) + assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(expectedTile) + } } @Test fun updatingWithDefaultsForAnotherUserOverridesTile() = - testScope.runTest { - underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true) - runCurrent() + with(kosmos) { + testScope.runTest { + underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true) + runCurrent() + + val tiles = collectValues(underTest.getTiles(TEST_USER_2)) + underTest.updateWithDefaults(TEST_USER_2, TEST_DEFAULTS_2, true) + runCurrent() + + assertThat(underTest.getTile(TEST_USER_2)).isEqualTo(TEST_TILE_2) + assertThat(tiles()).hasSize(1) + assertThat(tiles().last()).isEqualTo(TEST_TILE_2) + } + } - val tiles = collectValues(underTest.getTiles(TEST_USER_2)) - underTest.updateWithDefaults(TEST_USER_2, TEST_DEFAULTS_2, true) - runCurrent() + @Test + fun isActiveFollowsPackageManagerAdapter() = + with(kosmos) { + testScope.runTest { + packageManagerAdapterFacade.setIsActive(false) + assertThat(underTest.isTileActive()).isFalse() + + packageManagerAdapterFacade.setIsActive(true) + assertThat(underTest.isTileActive()).isTrue() + } + } - assertThat(underTest.getTile(TEST_USER_2)).isEqualTo(TEST_TILE_2) - assertThat(tiles()).hasSize(1) - assertThat(tiles().last()).isEqualTo(TEST_TILE_2) + @Test + fun isToggleableFollowsPackageManagerAdapter() = + with(kosmos) { + testScope.runTest { + packageManagerAdapterFacade.setIsToggleable(false) + assertThat(underTest.isTileToggleable()).isFalse() + + packageManagerAdapterFacade.setIsToggleable(true) + assertThat(underTest.isTileToggleable()).isTrue() + } } private companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt index eebb145ef384..90779cb1c0b3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt @@ -25,157 +25,159 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues -import com.android.systemui.qs.external.FakeCustomTileStatePersister +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope import com.android.systemui.qs.external.TileServiceKey -import com.android.systemui.qs.external.TileServiceManager import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tiles.impl.custom.TileSubject.Companion.assertThat +import com.android.systemui.qs.tiles.impl.custom.customTileDefaultsRepository +import com.android.systemui.qs.tiles.impl.custom.customTileRepository +import com.android.systemui.qs.tiles.impl.custom.customTileStatePersister import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults -import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileDefaultsRepository -import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileRepository -import com.android.systemui.util.mockito.whenever +import com.android.systemui.qs.tiles.impl.custom.tileSpec import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) class CustomTileInteractorTest : SysuiTestCase() { - @Mock private lateinit var tileServiceManager: TileServiceManager + private val kosmos = Kosmos().apply { tileSpec = TileSpec.create(TEST_COMPONENT) } - private val testScope = TestScope() - - private val defaultsRepository = FakeCustomTileDefaultsRepository() - private val customTileStatePersister = FakeCustomTileStatePersister() - private val customTileRepository = - FakeCustomTileRepository( - TEST_TILE_SPEC, - customTileStatePersister, - testScope.testScheduler, - ) - - private lateinit var underTest: CustomTileInteractor - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - - underTest = + private val underTest: CustomTileInteractor = + with(kosmos) { CustomTileInteractor( - TEST_USER, - defaultsRepository, + customTileDefaultsRepository, customTileRepository, - tileServiceManager, testScope.backgroundScope, testScope.testScheduler, ) - } + } @Test fun activeTileIsAvailableAfterRestored() = - testScope.runTest { - whenever(tileServiceManager.isActiveTile).thenReturn(true) - customTileStatePersister.persistState( - TileServiceKey(TEST_COMPONENT, TEST_USER.identifier), - TEST_TILE, - ) - - underTest.init() - - assertThat(underTest.tile).isEqualTo(TEST_TILE) - assertThat(underTest.tiles.first()).isEqualTo(TEST_TILE) + with(kosmos) { + testScope.runTest { + customTileRepository.setTileActive(true) + customTileStatePersister.persistState( + TileServiceKey(TEST_COMPONENT, TEST_USER.identifier), + TEST_TILE, + ) + + underTest.initForUser(TEST_USER) + + assertThat(underTest.getTile(TEST_USER)).isEqualTo(TEST_TILE) + assertThat(underTest.getTiles(TEST_USER).first()).isEqualTo(TEST_TILE) + } } @Test fun notActiveTileIsAvailableAfterUpdated() = - testScope.runTest { - whenever(tileServiceManager.isActiveTile).thenReturn(false) - customTileStatePersister.persistState( - TileServiceKey(TEST_COMPONENT, TEST_USER.identifier), - TEST_TILE, - ) - val tiles = collectValues(underTest.tiles) - val initJob = launch { underTest.init() } - - underTest.updateTile(TEST_TILE) - runCurrent() - initJob.join() - - assertThat(tiles()).hasSize(1) - assertThat(tiles().last()).isEqualTo(TEST_TILE) + with(kosmos) { + testScope.runTest { + customTileRepository.setTileActive(false) + customTileStatePersister.persistState( + TileServiceKey(TEST_COMPONENT, TEST_USER.identifier), + TEST_TILE, + ) + val tiles = collectValues(underTest.getTiles(TEST_USER)) + val initJob = launch { underTest.initForUser(TEST_USER) } + + underTest.updateTile(TEST_TILE) + runCurrent() + initJob.join() + + assertThat(tiles()).hasSize(1) + assertThat(tiles().last()).isEqualTo(TEST_TILE) + } } @Test fun notActiveTileIsAvailableAfterDefaultsUpdated() = - testScope.runTest { - whenever(tileServiceManager.isActiveTile).thenReturn(false) - customTileStatePersister.persistState( - TileServiceKey(TEST_COMPONENT, TEST_USER.identifier), - TEST_TILE, - ) - val tiles = collectValues(underTest.tiles) - val initJob = launch { underTest.init() } - - defaultsRepository.putDefaults(TEST_USER, TEST_COMPONENT, TEST_DEFAULTS) - defaultsRepository.requestNewDefaults(TEST_USER, TEST_COMPONENT) - runCurrent() - initJob.join() - - assertThat(tiles()).hasSize(1) - assertThat(tiles().last()).isEqualTo(TEST_TILE) + with(kosmos) { + testScope.runTest { + customTileRepository.setTileActive(false) + customTileStatePersister.persistState( + TileServiceKey(TEST_COMPONENT, TEST_USER.identifier), + TEST_TILE, + ) + val tiles = collectValues(underTest.getTiles(TEST_USER)) + val initJob = launch { underTest.initForUser(TEST_USER) } + + customTileDefaultsRepository.putDefaults(TEST_USER, TEST_COMPONENT, TEST_DEFAULTS) + customTileDefaultsRepository.requestNewDefaults(TEST_USER, TEST_COMPONENT) + runCurrent() + initJob.join() + + assertThat(tiles()).hasSize(1) + assertThat(tiles().last()).isEqualTo(TEST_TILE) + } } @Test(expected = IllegalStateException::class) - fun getTileBeforeInitThrows() = testScope.runTest { underTest.tile } + fun getTileBeforeInitThrows() = + with(kosmos) { testScope.runTest { underTest.getTile(TEST_USER) } } @Test fun initSuspendsForActiveTileNotRestoredAndNotUpdated() = - testScope.runTest { - whenever(tileServiceManager.isActiveTile).thenReturn(true) - val tiles = collectValues(underTest.tiles) + with(kosmos) { + testScope.runTest { + customTileRepository.setTileActive(true) + val tiles = collectValues(underTest.getTiles(TEST_USER)) - val initJob = backgroundScope.launch { underTest.init() } - advanceTimeBy(1 * DateUtils.DAY_IN_MILLIS) + val initJob = backgroundScope.launch { underTest.initForUser(TEST_USER) } + advanceTimeBy(1 * DateUtils.DAY_IN_MILLIS) - // Is still suspended - assertThat(initJob.isActive).isTrue() - assertThat(tiles()).isEmpty() + // Is still suspended + assertThat(initJob.isActive).isTrue() + assertThat(tiles()).isEmpty() + } } @Test fun initSuspendedForNotActiveTileWithoutUpdates() = - testScope.runTest { - whenever(tileServiceManager.isActiveTile).thenReturn(false) - customTileStatePersister.persistState( - TileServiceKey(TEST_COMPONENT, TEST_USER.identifier), - TEST_TILE, - ) - val tiles = collectValues(underTest.tiles) - - val initJob = backgroundScope.launch { underTest.init() } - advanceTimeBy(1 * DateUtils.DAY_IN_MILLIS) + with(kosmos) { + testScope.runTest { + customTileRepository.setTileActive(false) + customTileStatePersister.persistState( + TileServiceKey(TEST_COMPONENT, TEST_USER.identifier), + TEST_TILE, + ) + val tiles = collectValues(underTest.getTiles(TEST_USER)) + + val initJob = backgroundScope.launch { underTest.initForUser(TEST_USER) } + advanceTimeBy(1 * DateUtils.DAY_IN_MILLIS) + + // Is still suspended + assertThat(initJob.isActive).isTrue() + assertThat(tiles()).isEmpty() + } + } - // Is still suspended - assertThat(initJob.isActive).isTrue() - assertThat(tiles()).isEmpty() + @Test + fun toggleableFollowsTheRepository() { + with(kosmos) { + testScope.runTest { + customTileRepository.setTileToggleable(false) + assertThat(underTest.isTileToggleable()).isFalse() + + customTileRepository.setTileToggleable(true) + assertThat(underTest.isTileToggleable()).isTrue() + } } + } private companion object { val TEST_COMPONENT = ComponentName("test.pkg", "test.cls") - val TEST_TILE_SPEC = TileSpec.create(TEST_COMPONENT) val TEST_USER = UserHandle.of(1)!! val TEST_TILE = Tile().apply { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt index 2ecf01f6999d..123734742820 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt @@ -16,7 +16,8 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel -import android.platform.test.flag.junit.SetFlagsRule +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION @@ -60,8 +61,6 @@ class WifiViewModelTest : SysuiTestCase() { private lateinit var underTest: WifiViewModel - private val setFlagsRule = SetFlagsRule() - @Mock private lateinit var tableLogBuffer: TableLogBuffer @Mock private lateinit var connectivityConstants: ConnectivityConstants @Mock private lateinit var wifiConstants: WifiConstants @@ -187,11 +186,9 @@ class WifiViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS) fun activity_nullSsid_outputsFalse_staticFlagOff() = testScope.runTest { - // GIVEN flag is disabled - setFlagsRule.disableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS) - whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() @@ -214,11 +211,9 @@ class WifiViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS) fun activity_nullSsid_outputsFalse_staticFlagOn() = testScope.runTest { - // GIVEN flag is enabled - setFlagsRule.enableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS) - whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() @@ -371,11 +366,9 @@ class WifiViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS) fun activityContainer_inAndOutFalse_outputsTrue_staticFlagOff() = testScope.runTest { - // GIVEN the flag is off - setFlagsRule.disableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS) - whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -389,11 +382,9 @@ class WifiViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS) fun activityContainer_inAndOutFalse_outputsTrue_staticFlagOn() = testScope.runTest { - // GIVEN the flag is on - setFlagsRule.enableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS) - whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) diff --git a/packages/SystemUI/res/drawable/ic_memory.xml b/packages/SystemUI/res/drawable/ic_memory.xml deleted file mode 100644 index ada36c58ff1d..000000000000 --- a/packages/SystemUI/res/drawable/ic_memory.xml +++ /dev/null @@ -1,28 +0,0 @@ -<!-- -Copyright (C) 2018 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="24dp" - android:height="24dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0"> - <path - android:pathData="M16.0,5.0l-8.0,0.0l0.0,14.0l8.0,0.0z" - android:fillAlpha="0.5" - android:fillColor="#000000"/> - <path - android:pathData="M6,9 L6,7 L4,7 L4,5 L6,5 C6,3.9 6.9,3 8,3 L16,3 C17.1,3 18,3.9 18,5 L20,5 L20,7 L18,7 L18,9 L20,9 L20,11 L18,11 L18,13 L20,13 L20,15 L18,15 L18,17 L20,17 L20,19 L18,19 C18,20.1 17.1,21 16,21 L8,21 C6.9,21 6,20.1 6,19 L4,19 L4,17 L6,17 L6,15 L4,15 L4,13 L6,13 L6,11 L4,11 L4,9 L6,9 Z M16,19 L16,5 L8,5 L8,19 L16,19 Z" - android:fillColor="#000000"/> -</vector> diff --git a/packages/SystemUI/res/layout/widget_picker.xml b/packages/SystemUI/res/layout/widget_picker.xml deleted file mode 100644 index 21dc224a6f14..000000000000 --- a/packages/SystemUI/res/layout/widget_picker.xml +++ /dev/null @@ -1,30 +0,0 @@ -<!-- - ~ Copyright (C) 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. - --> - -<HorizontalScrollView - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <LinearLayout - android:id="@+id/widgets_container" - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:gravity="center_vertical" - android:orientation="horizontal"> - </LinearLayout> - -</HorizontalScrollView> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index deed6c8c7f25..e01a2aa674b3 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -480,9 +480,6 @@ This name is in the ComponentName flattened format (package/class) --> <string name="config_remoteCopyPackage" translatable="false"></string> - <!-- On debuggable builds, alert the user if SystemUI PSS goes over this number (in kb) --> - <integer name="watch_heap_limit">256000</integer> - <!-- SystemUI Plugins that can be loaded on user builds. --> <string-array name="config_pluginAllowlist" translatable="false"> <item>com.android.systemui</item> @@ -966,14 +963,6 @@ <bool name="config_edgeToEdgeBottomSheetDialog">true</bool> <!-- - Whether the scene container framework is enabled. - - The scene container framework is a newer (2023) way to organize the various "scenes" between the - bouncer, lockscreen, shade, and quick settings. - --> - <bool name="config_sceneContainerFrameworkEnabled">true</bool> - - <!-- Time in milliseconds the user has to touch the side FPS sensor to successfully authenticate TODO(b/302332976) Get this value from the HAL if they can provide an API for it. --> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 0267454c9161..ee89edefcdbf 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -274,6 +274,10 @@ <!-- Side padding on the side of notifications --> <dimen name="notification_side_paddings">16dp</dimen> + <!-- Starting translateY offset of the HUN appear and disappear animations. Indicates + the amount by the view is positioned above the screen before the animation starts. --> + <dimen name="heads_up_appear_y_above_screen">32dp</dimen> + <!-- padding between the heads up and the statusbar --> <dimen name="heads_up_status_bar_padding">8dp</dimen> diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 6cb2d457daea..d511caba941b 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -262,4 +262,8 @@ <!--Id for the device-entry UDFPS icon that lives in the alternate bouncer. --> <item type="id" name="alternate_bouncer_udfps_icon_view" /> + + <!-- Id for the udfps accessibility overlay --> + <item type="id" name="udfps_accessibility_overlay" /> + <item type="id" name="udfps_accessibility_overlay_top_guideline" /> </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index e10925d551e2..f4b25a701825 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2397,10 +2397,6 @@ <!-- URl of the webpage that explains battery saver. --> <string name="help_uri_battery_saver_learn_more_link_target" translatable="false"></string> - <!-- Name for a quick settings tile, used only by platform developers, to extract the SystemUI process memory and send it to another - app for debugging. Will not be seen by users. [CHAR LIMIT=20] --> - <string name="heap_dump_tile_name">Dump SysUI Heap</string> - <!-- Title for the privacy indicators dialog, only appears as part of a11y descriptions [CHAR LIMIT=NONE] --> <string name="ongoing_privacy_dialog_a11y_title">In use</string> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index cdd7b804fdf8..74b975cb7232 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -39,7 +39,6 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.systemui.Dumpable; -import com.android.systemui.common.ui.ConfigurationState; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; @@ -48,9 +47,7 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl; -import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder; import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager; -import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel; import com.android.systemui.log.LogBuffer; import com.android.systemui.log.core.LogLevel; import com.android.systemui.log.dagger.KeyguardClockLog; @@ -62,17 +59,11 @@ import com.android.systemui.shared.regionsampling.RegionSampler; import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController; import com.android.systemui.statusbar.notification.AnimatableProperty; import com.android.systemui.statusbar.notification.PropertyAnimator; -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore; -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder; -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker; -import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel; +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerAlwaysOnDisplayViewBinder; import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; import com.android.systemui.statusbar.notification.stack.AnimationProperties; -import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.NotificationIconAreaController; import com.android.systemui.statusbar.phone.NotificationIconContainer; -import com.android.systemui.statusbar.phone.ScreenOffAnimationController; -import com.android.systemui.statusbar.ui.SystemBarUtilsState; import com.android.systemui.util.ViewController; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.settings.SecureSettings; @@ -102,14 +93,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS private final DumpManager mDumpManager; private final ClockEventController mClockEventController; private final LogBuffer mLogBuffer; - private final NotificationIconContainerAlwaysOnDisplayViewModel mAodIconsViewModel; - private final KeyguardRootViewModel mKeyguardRootViewModel; - private final ConfigurationState mConfigurationState; - private final SystemBarUtilsState mSystemBarUtilsState; - private final DozeParameters mDozeParameters; - private final ScreenOffAnimationController mScreenOffAnimationController; - private final AlwaysOnDisplayNotificationIconViewStore mAodIconViewStore; - private final StatusBarIconViewBindingFailureTracker mIconViewBindingFailureTracker; + private final NotificationIconContainerAlwaysOnDisplayViewBinder mNicViewBinder; private FrameLayout mSmallClockFrame; // top aligned clock private FrameLayout mLargeClockFrame; // centered clock @@ -183,9 +167,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS KeyguardSliceViewController keyguardSliceViewController, NotificationIconAreaController notificationIconAreaController, LockscreenSmartspaceController smartspaceController, - SystemBarUtilsState systemBarUtilsState, - ScreenOffAnimationController screenOffAnimationController, - StatusBarIconViewBindingFailureTracker iconViewBindingFailureTracker, + NotificationIconContainerAlwaysOnDisplayViewBinder nicViewBinder, KeyguardUnlockAnimationController keyguardUnlockAnimationController, SecureSettings secureSettings, @Main DelayableExecutor uiExecutor, @@ -193,11 +175,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS DumpManager dumpManager, ClockEventController clockEventController, @KeyguardClockLog LogBuffer logBuffer, - NotificationIconContainerAlwaysOnDisplayViewModel aodIconsViewModel, - KeyguardRootViewModel keyguardRootViewModel, - ConfigurationState configurationState, - DozeParameters dozeParameters, - AlwaysOnDisplayNotificationIconViewStore aodIconViewStore, KeyguardInteractor keyguardInteractor, KeyguardClockInteractor keyguardClockInteractor, FeatureFlagsClassic featureFlags, @@ -208,9 +185,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mKeyguardSliceViewController = keyguardSliceViewController; mNotificationIconAreaController = notificationIconAreaController; mSmartspaceController = smartspaceController; - mSystemBarUtilsState = systemBarUtilsState; - mScreenOffAnimationController = screenOffAnimationController; - mIconViewBindingFailureTracker = iconViewBindingFailureTracker; + mNicViewBinder = nicViewBinder; mSecureSettings = secureSettings; mUiExecutor = uiExecutor; mBgExecutor = bgExecutor; @@ -218,11 +193,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mDumpManager = dumpManager; mClockEventController = clockEventController; mLogBuffer = logBuffer; - mAodIconsViewModel = aodIconsViewModel; - mKeyguardRootViewModel = keyguardRootViewModel; - mConfigurationState = configurationState; - mDozeParameters = dozeParameters; - mAodIconViewStore = aodIconViewStore; mView.setLogBuffer(mLogBuffer); mFeatureFlags = featureFlags; mKeyguardInteractor = keyguardInteractor; @@ -619,28 +589,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mAodIconsBindHandle.dispose(); } if (nic != null) { - final DisposableHandle viewHandle = - NotificationIconContainerViewBinder.bindWhileAttached( - nic, - mAodIconsViewModel, - mConfigurationState, - mSystemBarUtilsState, - mIconViewBindingFailureTracker, - mAodIconViewStore); - final DisposableHandle visHandle = KeyguardRootViewBinder.bindAodIconVisibility( - nic, - mKeyguardRootViewModel.isNotifIconContainerVisible(), - mConfigurationState, - mFeatureFlags, - mScreenOffAnimationController); - if (visHandle == null) { - mAodIconsBindHandle = viewHandle; - } else { - mAodIconsBindHandle = () -> { - viewHandle.dispose(); - visHandle.dispose(); - }; - } + mAodIconsBindHandle = mNicViewBinder.bindWhileAttached(nic); mAodIconContainer = nic; } } else { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 65668b56a9f3..240728a8f1e8 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -86,8 +86,6 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; -import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter; -import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -115,7 +113,6 @@ import java.util.Set; import java.util.concurrent.Executor; import javax.inject.Inject; -import javax.inject.Provider; import kotlinx.coroutines.ExperimentalCoroutinesApi; @@ -152,7 +149,6 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull private final SystemUIDialogManager mDialogManager; @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; @NonNull private final KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor; - @NonNull private final Provider<UdfpsKeyguardViewModels> mUdfpsKeyguardViewModels; @NonNull private final VibratorHelper mVibrator; @NonNull private final FalsingManager mFalsingManager; @NonNull private final PowerManager mPowerManager; @@ -623,7 +619,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { } else { onKeyguard = mOverlay != null && mOverlay.getAnimationViewController() - instanceof UdfpsKeyguardViewControllerAdapter; + instanceof UdfpsKeyguardViewControllerLegacy; } return onKeyguard && mKeyguardStateController.canDismissLockScreen() @@ -666,7 +662,6 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull InputManager inputManager, @NonNull KeyguardFaceAuthInteractor keyguardFaceAuthInteractor, @NonNull UdfpsKeyguardAccessibilityDelegate udfpsKeyguardAccessibilityDelegate, - @NonNull Provider<UdfpsKeyguardViewModels> udfpsKeyguardViewModelsProvider, @NonNull SelectedUserInteractor selectedUserInteractor, @NonNull FpsUnlockTracker fpsUnlockTracker, @NonNull KeyguardTransitionInteractor keyguardTransitionInteractor, @@ -737,7 +732,6 @@ public class UdfpsController implements DozeReceiver, Dumpable { return Unit.INSTANCE; }); mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor; - mUdfpsKeyguardViewModels = udfpsKeyguardViewModelsProvider; final UdfpsOverlayController mUdfpsOverlayController = new UdfpsOverlayController(); mFingerprintManager.setUdfpsOverlayController(mUdfpsOverlayController); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index dae6d08f7331..aabee93fd0a0 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -55,7 +55,6 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R import com.android.systemui.statusbar.LockscreenShadeTransitionController @@ -438,7 +437,7 @@ class UdfpsControllerOverlay @JvmOverloads constructor( if (DeviceEntryUdfpsRefactor.isEnabled) { !keyguardStateController.isShowing } else { - animation !is UdfpsKeyguardViewControllerAdapter + animation !is UdfpsKeyguardViewControllerLegacy } if (keyguardNotShowing) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt index 63fe26a37e46..64148f6035e0 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt @@ -33,7 +33,6 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState -import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R @@ -81,8 +80,7 @@ open class UdfpsKeyguardViewControllerLegacy( primaryBouncerInteractor, systemUIDialogManager, dumpManager, - ), - UdfpsKeyguardViewControllerAdapter { + ) { private val uniqueIdentifier = this.toString() private var showingUdfpsBouncer = false private var udfpsRequested = false @@ -199,11 +197,27 @@ open class UdfpsKeyguardViewControllerLegacy( listenForAodToOccludedTransitions(this) listenForAlternateBouncerToAodTransitions(this) listenForDreamingToAodTransitions(this) + listenForPrimaryBouncerToAodTransitions(this) } } } @VisibleForTesting + suspend fun listenForPrimaryBouncerToAodTransitions(scope: CoroutineScope): Job { + return scope.launch { + transitionInteractor + .transition(KeyguardState.PRIMARY_BOUNCER, KeyguardState.AOD) + .collect { transitionStep -> + view.onDozeAmountChanged( + transitionStep.value, + transitionStep.value, + ANIMATE_APPEAR_ON_SCREEN_OFF, + ) + } + } + } + + @VisibleForTesting suspend fun listenForDreamingToAodTransitions(scope: CoroutineScope): Job { return scope.launch { transitionInteractor.transition(KeyguardState.DREAMING, KeyguardState.AOD).collect { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/controller/UdfpsKeyguardViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/controller/UdfpsKeyguardViewController.kt deleted file mode 100644 index 6f4e1a3cae46..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/controller/UdfpsKeyguardViewController.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 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. - */ - -package com.android.systemui.biometrics.ui.controller - -import com.android.systemui.biometrics.UdfpsAnimationViewController -import com.android.systemui.biometrics.UdfpsKeyguardView -import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor -import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor -import com.android.systemui.dump.DumpManager -import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter -import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels -import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.statusbar.phone.SystemUIDialogManager -import kotlinx.coroutines.ExperimentalCoroutinesApi - -/** Class that coordinates non-HBM animations during keyguard authentication. */ -@ExperimentalCoroutinesApi -open class UdfpsKeyguardViewController( - val view: UdfpsKeyguardView, - statusBarStateController: StatusBarStateController, - primaryBouncerInteractor: PrimaryBouncerInteractor, - systemUIDialogManager: SystemUIDialogManager, - dumpManager: DumpManager, - private val alternateBouncerInteractor: AlternateBouncerInteractor, - udfpsKeyguardViewModels: UdfpsKeyguardViewModels, -) : - UdfpsAnimationViewController<UdfpsKeyguardView>( - view, - statusBarStateController, - primaryBouncerInteractor, - systemUIDialogManager, - dumpManager, - ), - UdfpsKeyguardViewControllerAdapter { - private val uniqueIdentifier = this.toString() - override val tag: String - get() = TAG - - init { - udfpsKeyguardViewModels.bindViews(view) - } - - public override fun onViewAttached() { - super.onViewAttached() - alternateBouncerInteractor.setAlternateBouncerUIAvailable(true, uniqueIdentifier) - } - - public override fun onViewDetached() { - super.onViewDetached() - alternateBouncerInteractor.setAlternateBouncerUIAvailable(false, uniqueIdentifier) - } - - override fun shouldPauseAuth(): Boolean { - return !view.isVisible() - } - - override fun listenForTouchesOutsideView(): Boolean { - return true - } - - companion object { - private const val TAG = "UdfpsKeyguardViewController" - } -} diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index 887b18cfe4c9..0a13e4887ebe 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -16,8 +16,9 @@ package com.android.systemui.communal.widgets -import android.appwidget.AppWidgetProviderInfo +import android.content.ComponentName import android.content.Intent +import android.content.pm.PackageManager import android.os.Bundle import android.os.RemoteException import android.util.Log @@ -39,10 +40,8 @@ constructor( private var windowManagerService: IWindowManager? = null, ) : ComponentActivity() { companion object { - /** - * Intent extra name for the {@link AppWidgetProviderInfo} of a widget to add to hub mode. - */ - const val ADD_WIDGET_INFO = "add_widget_info" + private const val EXTRA_FILTER_STRATEGY = "filter_strategy" + private const val FILTER_STRATEGY_GLANCEABLE_HUB = 1 private const val TAG = "EditWidgetsActivity" } @@ -51,13 +50,8 @@ constructor( when (result.resultCode) { RESULT_OK -> { result.data - ?.let { - it.getParcelableExtra( - ADD_WIDGET_INFO, - AppWidgetProviderInfo::class.java - ) - } - ?.let { communalInteractor.addWidget(it.provider, 0) } + ?.getParcelableExtra(Intent.EXTRA_COMPONENT_NAME, ComponentName::class.java) + ?.let { communalInteractor.addWidget(it, 0) } ?: run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") } } else -> @@ -71,13 +65,35 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setShowWhenLocked(true) + setCommunalEditWidgetActivityContent( activity = this, viewModel = communalViewModel, onOpenWidgetPicker = { - addWidgetActivityLauncher.launch( - Intent(applicationContext, WidgetPickerActivity::class.java) - ) + val localPackageManager: PackageManager = getPackageManager() + val intent = + Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) } + localPackageManager + .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) + ?.activityInfo + ?.packageName + ?.let { packageName -> + try { + addWidgetActivityLauncher.launch( + Intent(Intent.ACTION_PICK).also { + it.setPackage(packageName) + it.putExtra( + EXTRA_FILTER_STRATEGY, + FILTER_STRATEGY_GLANCEABLE_HUB + ) + } + ) + } catch (e: Exception) { + Log.e(TAG, "Failed to launch widget picker activity", e) + } + } + ?: run { Log.e(TAG, "Couldn't resolve launcher package name") } }, onEditDone = { try { diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt deleted file mode 100644 index a26afc86aa2e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 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. - */ - -package com.android.systemui.communal.widgets - -import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProviderInfo -import android.content.Intent -import android.graphics.Color -import android.os.Bundle -import android.util.Log -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.LinearLayout -import androidx.activity.ComponentActivity -import androidx.core.view.setMargins -import androidx.core.view.setPadding -import com.android.systemui.res.R -import javax.inject.Inject - -/** - * An Activity responsible for displaying a list of widgets to add to the hub mode grid. This is - * essentially a placeholder until Launcher's widget picker can be used. - */ -class WidgetPickerActivity -@Inject -constructor( - private val appWidgetManager: AppWidgetManager, -) : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContentView(R.layout.widget_picker) - loadWidgets() - } - - private fun loadWidgets() { - val containerView: ViewGroup? = findViewById(R.id.widgets_container) - containerView?.apply { - try { - appWidgetManager - .getInstalledProviders(AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD) - ?.stream() - ?.forEach { widgetInfo -> - val activity = this@WidgetPickerActivity - (widgetInfo.loadPreviewImage(activity, 0) - ?: widgetInfo.loadIcon(activity, 0)) - ?.let { - addView( - ImageView(activity).also { v -> - v.setImageDrawable(it) - v.setBackgroundColor(WIDGET_PREVIEW_BACKGROUND_COLOR) - v.setPadding(WIDGET_PREVIEW_PADDING) - v.layoutParams = - LinearLayout.LayoutParams( - WIDGET_PREVIEW_SIZE, - WIDGET_PREVIEW_SIZE - ) - .also { lp -> - lp.setMargins(WIDGET_PREVIEW_MARGINS) - } - v.setOnClickListener { - setResult( - RESULT_OK, - Intent() - .putExtra( - EditWidgetsActivity.ADD_WIDGET_INFO, - widgetInfo - ) - ) - finish() - } - } - ) - } - } - } catch (e: RuntimeException) { - Log.e(TAG, "Exception fetching widget providers", e) - } - } - } - - companion object { - private const val WIDGET_PREVIEW_SIZE = 600 - private const val WIDGET_PREVIEW_MARGINS = 32 - private const val WIDGET_PREVIEW_PADDING = 32 - private val WIDGET_PREVIEW_BACKGROUND_COLOR = Color.rgb(216, 225, 220) - private const val TAG = "WidgetPickerActivity" - } -} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java index 4b27af1fc989..9afd5ede0b4c 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java @@ -20,7 +20,6 @@ import android.app.Activity; import com.android.systemui.ForegroundServicesDialog; import com.android.systemui.communal.widgets.EditWidgetsActivity; -import com.android.systemui.communal.widgets.WidgetPickerActivity; import com.android.systemui.contrast.ContrastDialogActivity; import com.android.systemui.keyguard.WorkLockActivity; import com.android.systemui.people.PeopleSpaceActivity; @@ -158,12 +157,6 @@ public abstract class DefaultActivityBinder { @ClassKey(EditWidgetsActivity.class) public abstract Activity bindEditWidgetsActivity(EditWidgetsActivity activity); - /** Inject into WidgetPickerActivity. */ - @Binds - @IntoMap - @ClassKey(WidgetPickerActivity.class) - public abstract Activity bindWidgetPickerActivity(WidgetPickerActivity activity); - /** Inject into SwitchToManagedProfileForCallActivity. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt index 0113628c30ca..ef2537c1bebb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt @@ -15,40 +15,32 @@ * */ -package com.android.systemui.keyguard.ui.binder +package com.android.systemui.deviceentry.ui.binder -import android.content.res.ColorStateList -import android.widget.ImageView +import android.annotation.SuppressLint +import androidx.core.view.isInvisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.keyguard.ui.viewmodel.BackgroundViewModel +import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay +import com.android.systemui.deviceentry.ui.viewmodel.UdfpsAccessibilityOverlayViewModel import com.android.systemui.lifecycle.repeatWhenAttached import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch @ExperimentalCoroutinesApi -object UdfpsBackgroundViewBinder { +object UdfpsAccessibilityOverlayBinder { - /** - * Drives UI for the udfps background view. See [UdfpsAodFingerprintViewBinder] and - * [UdfpsFingerprintViewBinder]. - */ + /** Forwards hover events to the view model to make guided announcements for accessibility. */ + @SuppressLint("ClickableViewAccessibility") @JvmStatic fun bind( - view: ImageView, - viewModel: BackgroundViewModel, + view: UdfpsAccessibilityOverlay, + viewModel: UdfpsAccessibilityOverlayViewModel, ) { - view.alpha = 0f + view.setOnHoverListener { v, event -> viewModel.onHoverEvent(v, event) } view.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - viewModel.transition.collect { - view.alpha = it.alpha - view.scaleX = it.scale - view.scaleY = it.scale - view.imageTintList = ColorStateList.valueOf(it.color) - } - } + // Repeat on CREATED because we update the visibility of the view + repeatOnLifecycle(Lifecycle.State.CREATED) { + viewModel.visible.collect { visible -> view.isInvisible = !visible } } } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/shared/model/NotificationSettingsModel.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt index 7e35360e30ff..7be323073692 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/shared/model/NotificationSettingsModel.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt @@ -12,13 +12,12 @@ * 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.systemui.shared.notifications.shared.model +package com.android.systemui.deviceentry.ui.view + +import android.content.Context +import android.view.View -/** Models notification settings. */ -data class NotificationSettingsModel( - /** Whether notifications are shown on the lock screen. */ - val isShowNotificationsOnLockScreenEnabled: Boolean = false, -) +/** Overlay to handle under-fingerprint sensor accessibility events. */ +class UdfpsAccessibilityOverlay(context: Context?) : View(context) diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt new file mode 100644 index 000000000000..80684b442c5d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.deviceentry.ui.viewmodel + +import android.graphics.Point +import android.view.MotionEvent +import android.view.View +import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor +import com.android.systemui.biometrics.UdfpsUtils +import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor +import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams +import com.android.systemui.keyguard.ui.view.DeviceEntryIconView +import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel +import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf + +/** Models the UI state for the UDFPS accessibility overlay */ +@ExperimentalCoroutinesApi +class UdfpsAccessibilityOverlayViewModel +@Inject +constructor( + udfpsOverlayInteractor: UdfpsOverlayInteractor, + accessibilityInteractor: AccessibilityInteractor, + deviceEntryIconViewModel: DeviceEntryIconViewModel, + deviceEntryFgIconViewModel: DeviceEntryForegroundViewModel, +) { + private val udfpsUtils = UdfpsUtils() + private val udfpsOverlayParams: StateFlow<UdfpsOverlayParams> = + udfpsOverlayInteractor.udfpsOverlayParams + + /** Overlay is only visible if touch exploration is enabled and UDFPS can be used. */ + val visible: Flow<Boolean> = + accessibilityInteractor.isTouchExplorationEnabled.flatMapLatest { touchExplorationEnabled -> + if (touchExplorationEnabled) { + combine( + deviceEntryFgIconViewModel.viewModel, + deviceEntryIconViewModel.deviceEntryViewAlpha, + ) { iconViewModel, alpha -> + iconViewModel.type == DeviceEntryIconView.IconType.FINGERPRINT && + !iconViewModel.useAodVariant && + alpha == 1f + } + } else { + flowOf(false) + } + } + + /** Give directional feedback to help the user authenticate with UDFPS. */ + fun onHoverEvent(v: View, event: MotionEvent): Boolean { + val overlayParams = udfpsOverlayParams.value + val scaledTouch: Point = + udfpsUtils.getTouchInNativeCoordinates(event.getPointerId(0), event, overlayParams) + + if (!udfpsUtils.isWithinSensorArea(event.getPointerId(0), event, overlayParams)) { + // view only receives motionEvents when [visible] which requires touchExplorationEnabled + val announceStr = + udfpsUtils.onTouchOutsideOfSensorArea( + /* touchExplorationEnabled */ true, + v.context, + scaledTouch.x, + scaledTouch.y, + overlayParams, + ) + if (announceStr != null) { + v.announceForAccessibility(announceStr) + } + } + // always let the motion events go through to underlying views + return false + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index 5ec51f4c3dad..15404238099d 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -25,7 +25,9 @@ import com.android.server.notification.Flags.vibrateWhileUnlocked import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor @@ -36,20 +38,28 @@ import javax.inject.Inject class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, handler: Handler) : FlagDependenciesBase(featureFlags, handler) { override fun defineDependencies() { + // Internal notification backend dependencies + crossAppPoliteNotifications dependsOn politeNotifications + vibrateWhileUnlockedToken dependsOn politeNotifications + + // Internal notification frontend dependencies NotificationsLiveDataStoreRefactor.token dependsOn NotificationIconContainerRefactor.token FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token - val keyguardBottomAreaRefactor = FlagToken( - FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor()) + // Internal keyguard dependencies KeyguardShadeMigrationNssl.token dependsOn keyguardBottomAreaRefactor - val crossAppPoliteNotifToken = - FlagToken(FLAG_CROSS_APP_POLITE_NOTIFICATIONS, crossAppPoliteNotifications()) - val politeNotifToken = FlagToken(FLAG_POLITE_NOTIFICATIONS, politeNotifications()) - crossAppPoliteNotifToken dependsOn politeNotifToken - - val vibrateWhileUnlockedToken = - FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked()) - vibrateWhileUnlockedToken dependsOn politeNotifToken + // SceneContainer dependencies + SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta } + SceneContainerFlag.getMainStaticFlag() dependsOn MIGRATE_KEYGUARD_STATUS_BAR_VIEW } + + private inline val politeNotifications + get() = FlagToken(FLAG_POLITE_NOTIFICATIONS, politeNotifications()) + private inline val crossAppPoliteNotifications + get() = FlagToken(FLAG_CROSS_APP_POLITE_NOTIFICATIONS, crossAppPoliteNotifications()) + private inline val vibrateWhileUnlockedToken: FlagToken + get() = FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked()) + private inline val keyguardBottomAreaRefactor + get() = FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor()) } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index b43f54de4b2a..f6db978efadc 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -17,11 +17,11 @@ package com.android.systemui.flags import android.provider.DeviceConfig import com.android.internal.annotations.Keep -import com.android.systemui.res.R import com.android.systemui.flags.FlagsFactory.releasedFlag import com.android.systemui.flags.FlagsFactory.resourceBooleanFlag import com.android.systemui.flags.FlagsFactory.sysPropBooleanFlag import com.android.systemui.flags.FlagsFactory.unreleasedFlag +import com.android.systemui.res.R /** * List of [Flag] objects for use in SystemUI. @@ -483,9 +483,6 @@ object Flags { // TODO(b/264916608): Tracking Bug @JvmField val SCREENSHOT_METADATA = unreleasedFlag("screenshot_metadata") - // TODO(b/266955521): Tracking bug - @JvmField val SCREENSHOT_DETECTION = releasedFlag("screenshot_detection") - // TODO(b/251205791): Tracking Bug @JvmField val SCREENSHOT_APP_CLIPS = releasedFlag("screenshot_app_clips") @@ -607,9 +604,6 @@ object Flags { @JvmField val LOCKSCREEN_WALLPAPER_DREAM_ENABLED = unreleasedFlag("enable_lockscreen_wallpaper_dream") - // TODO(b/283084712): Tracking Bug - @JvmField val IMPROVED_HUN_ANIMATIONS = unreleasedFlag("improved_hun_animations") - // TODO(b/283447257): Tracking bug @JvmField val BIGPICTURE_NOTIFICATION_LAZY_LOADING = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 20da00ee3daf..af5d48d9ae07 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -124,7 +124,7 @@ constructor( indicationAreaHandle = KeyguardIndicationAreaBinder.bind( - notificationShadeWindowView, + notificationShadeWindowView.requireViewById(R.id.keyguard_indication_area), keyguardIndicationAreaViewModel, keyguardRootViewModel, indicationController, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt index de15fd6a958f..c98f63717938 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt @@ -22,6 +22,7 @@ import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository import com.android.systemui.res.R import javax.inject.Inject @@ -48,7 +49,9 @@ constructor( ) { init { - alternateBouncerInteractor.setAlternateBouncerUIAvailable(true, TAG) + if (!DeviceEntryUdfpsRefactor.isEnabled) { + alternateBouncerInteractor.setAlternateBouncerUIAvailable(true, TAG) + } } private val showIndicatorForPrimaryBouncer: Flow<Boolean> = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/adapter/UdfpsKeyguardViewControllerAdapter.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/adapter/UdfpsKeyguardViewControllerAdapter.kt deleted file mode 100644 index ebf1beb132f0..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/adapter/UdfpsKeyguardViewControllerAdapter.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 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. - */ -package com.android.systemui.keyguard.ui.adapter - -/** - * Temporary adapter class while - * [com.android.systemui.biometrics.ui.controller.UdfpsKeyguardViewController] is being refactored - * before [com.android.systemui.biometrics.UdfpsKeyguardViewControllerLegacy] is removed. - * - * TODO (b/278719514): Delete once udfps keyguard view is fully refactored. - */ -interface UdfpsKeyguardViewControllerAdapter diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt index d12d193aa43b..c749818a05e9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt @@ -26,6 +26,7 @@ import com.android.systemui.keyguard.ui.view.DeviceEntryIconView import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel import com.android.systemui.lifecycle.repeatWhenAttached import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch @ExperimentalCoroutinesApi object AlternateBouncerUdfpsViewBinder { @@ -71,10 +72,12 @@ object AlternateBouncerUdfpsViewBinder { bgView.visibility = View.VISIBLE bgView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.bgViewModel.collect { bgViewModel -> - bgView.alpha = bgViewModel.alpha - bgView.imageTintList = ColorStateList.valueOf(bgViewModel.tint) + launch { + viewModel.bgColor.collect { color -> + bgView.imageTintList = ColorStateList.valueOf(color) + } } + launch { viewModel.bgAlpha.collect { alpha -> bgView.alpha = alpha } } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt index dcf4284438bf..a02e8ac84a75 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt @@ -131,10 +131,10 @@ object DeviceEntryIconViewBinder { bgView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { + launch { bgViewModel.alpha.collect { alpha -> bgView.alpha = alpha } } launch { - bgViewModel.viewModel.collect { bgViewModel -> - bgView.alpha = bgViewModel.alpha - bgView.imageTintList = ColorStateList.valueOf(bgViewModel.tint) + bgViewModel.color.collect { color -> + bgView.imageTintList = ColorStateList.valueOf(color) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt index 4efd9ef5f21c..4c33d905b785 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt @@ -54,12 +54,11 @@ object KeyguardIndicationAreaBinder { keyguardRootViewModel: KeyguardRootViewModel, indicationController: KeyguardIndicationController, ): DisposableHandle { - val indicationArea: ViewGroup = view.requireViewById(R.id.keyguard_indication_area) - indicationController.setIndicationArea(indicationArea) + indicationController.setIndicationArea(view) - val indicationText: TextView = indicationArea.requireViewById(R.id.keyguard_indication_text) + val indicationText: TextView = view.requireViewById(R.id.keyguard_indication_text) val indicationTextBottom: TextView = - indicationArea.requireViewById(R.id.keyguard_indication_text_bottom) + view.requireViewById(R.id.keyguard_indication_text_bottom) view.clipChildren = false view.clipToPadding = false @@ -71,7 +70,7 @@ object KeyguardIndicationAreaBinder { launch { if (keyguardBottomAreaRefactor()) { keyguardRootViewModel.alpha.collect { alpha -> - indicationArea.apply { + view.apply { this.importantForAccessibility = if (alpha == 0f) { View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS @@ -83,7 +82,7 @@ object KeyguardIndicationAreaBinder { } } else { viewModel.alpha.collect { alpha -> - indicationArea.apply { + view.apply { this.importantForAccessibility = if (alpha == 0f) { View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS @@ -98,7 +97,7 @@ object KeyguardIndicationAreaBinder { launch { viewModel.indicationAreaTranslationX.collect { translationX -> - indicationArea.translationX = translationX + view.translationX = translationX } } @@ -113,9 +112,7 @@ object KeyguardIndicationAreaBinder { 0 } } - .collect { paddingPx -> - indicationArea.setPadding(paddingPx, 0, paddingPx, 0) - } + .collect { paddingPx -> view.setPadding(paddingPx, 0, paddingPx, 0) } } launch { @@ -124,7 +121,7 @@ object KeyguardIndicationAreaBinder { .flatMapLatest { defaultBurnInOffsetY -> viewModel.indicationAreaTranslationY(defaultBurnInOffsetY) } - .collect { translationY -> indicationArea.translationY = translationY } + .collect { translationY -> view.translationY = translationY } } launch { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 01a1ca3eeb93..362e7e6d4770 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -29,7 +29,6 @@ import android.view.ViewGroup.OnHierarchyChangeListener import android.view.ViewPropertyAnimator import android.view.WindowInsets import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.app.animation.Interpolators import com.android.internal.jank.InteractionJankMonitor @@ -67,6 +66,7 @@ import com.android.systemui.util.ui.value import javax.inject.Provider import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -205,7 +205,6 @@ object KeyguardRootViewBinder { childViews[aodNotificationIconContainerId] ?.setAodNotifIconContainerIsVisible( isVisible, - featureFlags, iconsAppearTranslationPx.value, screenOffAnimationController, ) @@ -359,41 +358,32 @@ object KeyguardRootViewBinder { } } - @JvmStatic - fun bindAodIconVisibility( + suspend fun bindAodNotifIconVisibility( view: View, isVisible: Flow<AnimatedValue<Boolean>>, configuration: ConfigurationState, - featureFlags: FeatureFlagsClassic, screenOffAnimationController: ScreenOffAnimationController, - ): DisposableHandle? { + ) { KeyguardShadeMigrationNssl.assertInLegacyMode() - if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return null - return view.repeatWhenAttached { - lifecycleScope.launch { - val iconAppearTranslationPx = - configuration - .getDimensionPixelSize(R.dimen.shelf_appear_translation) - .stateIn(this) - isVisible.collect { isVisible -> - view.setAodNotifIconContainerIsVisible( - isVisible, - featureFlags, - iconAppearTranslationPx.value, - screenOffAnimationController, - ) - } + if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return + coroutineScope { + val iconAppearTranslationPx = + configuration.getDimensionPixelSize(R.dimen.shelf_appear_translation).stateIn(this) + isVisible.collect { isVisible -> + view.setAodNotifIconContainerIsVisible( + isVisible = isVisible, + iconsAppearTranslationPx = iconAppearTranslationPx.value, + screenOffAnimationController = screenOffAnimationController, + ) } } } private fun View.setAodNotifIconContainerIsVisible( isVisible: AnimatedValue<Boolean>, - featureFlags: FeatureFlagsClassic, iconsAppearTranslationPx: Int, screenOffAnimationController: ScreenOffAnimationController, ) { - val statusViewMigrated = KeyguardShadeMigrationNssl.isEnabled animate().cancel() val animatorListener = object : AnimatorListenerAdapter() { @@ -404,13 +394,13 @@ object KeyguardRootViewBinder { when { !isVisible.isAnimating -> { alpha = 1f - if (!statusViewMigrated) { + if (!KeyguardShadeMigrationNssl.isEnabled) { translationY = 0f } visibility = if (isVisible.value) View.VISIBLE else View.INVISIBLE } newAodTransition() -> { - animateInIconTranslation(statusViewMigrated) + animateInIconTranslation() if (isVisible.value) { CrossFadeHelper.fadeIn(this, animatorListener) } else { @@ -419,7 +409,7 @@ object KeyguardRootViewBinder { } !isVisible.value -> { // Let's make sure the icon are translated to 0, since we cancelled it above - animateInIconTranslation(statusViewMigrated) + animateInIconTranslation() CrossFadeHelper.fadeOut(this, animatorListener) } visibility != View.VISIBLE -> { @@ -429,13 +419,12 @@ object KeyguardRootViewBinder { appearIcons( animate = screenOffAnimationController.shouldAnimateAodIcons(), iconsAppearTranslationPx, - statusViewMigrated, animatorListener, ) } else -> { // Let's make sure the icons are translated to 0, since we cancelled it above - animateInIconTranslation(statusViewMigrated) + animateInIconTranslation() // We were fading out, let's fade in instead CrossFadeHelper.fadeIn(this, animatorListener) } @@ -445,11 +434,10 @@ object KeyguardRootViewBinder { private fun View.appearIcons( animate: Boolean, iconAppearTranslation: Int, - statusViewMigrated: Boolean, animatorListener: Animator.AnimatorListener, ) { if (animate) { - if (!statusViewMigrated) { + if (!KeyguardShadeMigrationNssl.isEnabled) { translationY = -iconAppearTranslation.toFloat() } alpha = 0f @@ -457,19 +445,19 @@ object KeyguardRootViewBinder { .alpha(1f) .setInterpolator(Interpolators.LINEAR) .setDuration(AOD_ICONS_APPEAR_DURATION) - .apply { if (statusViewMigrated) animateInIconTranslation() } + .apply { if (KeyguardShadeMigrationNssl.isEnabled) animateInIconTranslation() } .setListener(animatorListener) .start() } else { alpha = 1.0f - if (!statusViewMigrated) { + if (!KeyguardShadeMigrationNssl.isEnabled) { translationY = 0f } } } - private fun View.animateInIconTranslation(statusViewMigrated: Boolean) { - if (!statusViewMigrated) { + private fun View.animateInIconTranslation() { + if (!KeyguardShadeMigrationNssl.isEnabled) { animate().animateInIconTranslation().setDuration(AOD_ICONS_APPEAR_DURATION).start() } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt deleted file mode 100644 index 52d87d369083..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 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. - * - */ - -package com.android.systemui.keyguard.ui.binder - -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle -import com.airbnb.lottie.LottieAnimationView -import com.android.systemui.keyguard.ui.viewmodel.UdfpsAodViewModel -import com.android.systemui.lifecycle.repeatWhenAttached -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch - -@ExperimentalCoroutinesApi -object UdfpsAodFingerprintViewBinder { - - /** - * Drives UI for the UDFPS aod fingerprint view. See [UdfpsFingerprintViewBinder] and - * [UdfpsBackgroundViewBinder]. - */ - @JvmStatic - fun bind( - view: LottieAnimationView, - viewModel: UdfpsAodViewModel, - ) { - view.alpha = 0f - view.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - viewModel.burnInOffsets.collect { burnInOffsets -> - view.progress = burnInOffsets.progress - view.translationX = burnInOffsets.x.toFloat() - view.translationY = burnInOffsets.y.toFloat() - } - } - - launch { viewModel.alpha.collect { alpha -> view.alpha = alpha } } - - launch { - viewModel.padding.collect { padding -> - view.setPadding(padding, padding, padding, padding) - } - } - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt deleted file mode 100644 index d4621e6e2356..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 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. - * - */ - -package com.android.systemui.keyguard.ui.binder - -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle -import com.airbnb.lottie.LottieAnimationView -import com.airbnb.lottie.LottieProperty -import com.airbnb.lottie.model.KeyPath -import com.android.systemui.keyguard.ui.viewmodel.FingerprintViewModel -import com.android.systemui.lifecycle.repeatWhenAttached -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch - -@ExperimentalCoroutinesApi -object UdfpsFingerprintViewBinder { - private var udfpsIconColor = 0 - - /** - * Drives UI for the UDFPS fingerprint view when it's NOT on aod. See - * [UdfpsAodFingerprintViewBinder] and [UdfpsBackgroundViewBinder]. - */ - @JvmStatic - fun bind( - view: LottieAnimationView, - viewModel: FingerprintViewModel, - ) { - view.alpha = 0f - view.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - viewModel.transition.collect { - view.alpha = it.alpha - view.scaleX = it.scale - view.scaleY = it.scale - if (udfpsIconColor != (it.color)) { - udfpsIconColor = it.color - view.invalidate() - } - } - } - - launch { - viewModel.burnInOffsets.collect { burnInOffsets -> - view.translationX = burnInOffsets.x.toFloat() - view.translationY = burnInOffsets.y.toFloat() - } - } - - launch { - viewModel.dozeAmount.collect { dozeAmount -> - // Lottie progress represents: aod=0 to lockscreen=1 - view.progress = 1f - dozeAmount - } - } - - launch { - viewModel.padding.collect { padding -> - view.setPadding(padding, padding, padding, padding) - } - } - } - } - - // Add a callback that updates the color to `udfpsIconColor` whenever invalidate is called - view.addValueCallback(KeyPath("**"), LottieProperty.COLOR_FILTER) { - PorterDuffColorFilter(udfpsIconColor, PorterDuff.Mode.SRC_ATOP) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt deleted file mode 100644 index aabb3f41e881..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 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. - * - */ - -package com.android.systemui.biometrics.ui.binder - -import android.view.View -import com.android.systemui.res.R -import com.android.systemui.keyguard.ui.binder.UdfpsAodFingerprintViewBinder -import com.android.systemui.keyguard.ui.binder.UdfpsBackgroundViewBinder -import com.android.systemui.keyguard.ui.binder.UdfpsFingerprintViewBinder -import com.android.systemui.keyguard.ui.viewmodel.BackgroundViewModel -import com.android.systemui.keyguard.ui.viewmodel.FingerprintViewModel -import com.android.systemui.keyguard.ui.viewmodel.UdfpsAodViewModel -import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardInternalViewModel -import kotlinx.coroutines.ExperimentalCoroutinesApi - -@ExperimentalCoroutinesApi -object UdfpsKeyguardInternalViewBinder { - - @JvmStatic - fun bind( - view: View, - viewModel: UdfpsKeyguardInternalViewModel, - aodViewModel: UdfpsAodViewModel, - fingerprintViewModel: FingerprintViewModel, - backgroundViewModel: BackgroundViewModel, - ) { - view.accessibilityDelegate = viewModel.accessibilityDelegate - - // bind child views - UdfpsAodFingerprintViewBinder.bind(view.requireViewById(R.id.udfps_aod_fp), aodViewModel) - UdfpsFingerprintViewBinder.bind( - view.requireViewById(R.id.udfps_lockscreen_fp), - fingerprintViewModel - ) - UdfpsBackgroundViewBinder.bind( - view.requireViewById(R.id.udfps_keyguard_fp_bg), - backgroundViewModel - ) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardViewBinder.kt deleted file mode 100644 index 475d26f1db54..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardViewBinder.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 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. - * - */ - -package com.android.systemui.keyguard.ui.binder - -import android.graphics.RectF -import android.view.View -import android.widget.FrameLayout -import androidx.asynclayoutinflater.view.AsyncLayoutInflater -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.biometrics.UdfpsKeyguardView -import com.android.systemui.biometrics.ui.binder.UdfpsKeyguardInternalViewBinder -import com.android.systemui.keyguard.ui.viewmodel.BackgroundViewModel -import com.android.systemui.keyguard.ui.viewmodel.FingerprintViewModel -import com.android.systemui.keyguard.ui.viewmodel.UdfpsAodViewModel -import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardInternalViewModel -import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModel -import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.res.R -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.launch - -@ExperimentalCoroutinesApi -object UdfpsKeyguardViewBinder { - /** - * Drives UI for the keyguard UDFPS view. Inflates child views on a background thread. For view - * binders for its child views, see [UdfpsFingerprintViewBinder], [UdfpsBackgroundViewBinder] & - * [UdfpsAodFingerprintViewBinder]. - */ - @JvmStatic - fun bind( - view: UdfpsKeyguardView, - viewModel: UdfpsKeyguardViewModel, - udfpsKeyguardInternalViewModel: UdfpsKeyguardInternalViewModel, - aodViewModel: UdfpsAodViewModel, - fingerprintViewModel: FingerprintViewModel, - backgroundViewModel: BackgroundViewModel, - ) { - val layoutInflaterFinishListener = - AsyncLayoutInflater.OnInflateFinishedListener { inflatedInternalView, _, parent -> - UdfpsKeyguardInternalViewBinder.bind( - inflatedInternalView, - udfpsKeyguardInternalViewModel, - aodViewModel, - fingerprintViewModel, - backgroundViewModel, - ) - val lp = inflatedInternalView.layoutParams as FrameLayout.LayoutParams - lp.width = viewModel.sensorBounds.width() - lp.height = viewModel.sensorBounds.height() - val relativeToView = - getBoundsRelativeToView( - inflatedInternalView, - RectF(viewModel.sensorBounds), - ) - lp.setMarginsRelative( - relativeToView.left.toInt(), - relativeToView.top.toInt(), - relativeToView.right.toInt(), - relativeToView.bottom.toInt(), - ) - parent!!.addView(inflatedInternalView, lp) - } - val inflater = AsyncLayoutInflater(view.context) - inflater.inflate(R.layout.udfps_keyguard_view_internal, view, layoutInflaterFinishListener) - - view.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - combine(aodViewModel.isVisible, fingerprintViewModel.visible) { - isAodVisible, - isFingerprintVisible -> - isAodVisible || isFingerprintVisible - } - .collect { view.setVisible(it) } - } - } - } - } - - /** - * Converts coordinates of RectF relative to the screen to coordinates relative to this view. - * - * @param bounds RectF based off screen coordinates in current orientation - */ - private fun getBoundsRelativeToView(view: View, bounds: RectF): RectF { - val pos: IntArray = view.locationOnScreen - return RectF( - bounds.left - pos[0], - bounds.top - pos[1], - bounds.right - pos[0], - bounds.bottom - pos[1] - ) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt index 1c6a2abdcbe7..bc9671e65f24 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt @@ -31,18 +31,21 @@ import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopu import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection +import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule.Companion.KEYGUARD_AMBIENT_INDICATION_AREA_SECTION import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection import java.util.Optional import javax.inject.Inject import javax.inject.Named import kotlin.jvm.optionals.getOrNull +import kotlinx.coroutines.ExperimentalCoroutinesApi /** * Positions elements of the lockscreen to the default position. * * This will be the most common use case for phones in portrait mode. */ +@ExperimentalCoroutinesApi @SysUISingleton @JvmSuppressWildcards class DefaultKeyguardBlueprint @@ -62,6 +65,7 @@ constructor( communalTutorialIndicatorSection: CommunalTutorialIndicatorSection, clockSection: ClockSection, smartspaceSection: SmartspaceSection, + udfpsAccessibilityOverlaySection: DefaultUdfpsAccessibilityOverlaySection, ) : KeyguardBlueprint { override val id: String = DEFAULT @@ -79,7 +83,8 @@ constructor( aodBurnInSection, communalTutorialIndicatorSection, clockSection, - defaultDeviceEntrySection, // Add LAST: Intentionally has z-order above other views. + defaultDeviceEntrySection, + udfpsAccessibilityOverlaySection, // Add LAST: Intentionally has z-order above others ) companion object { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt index bf7068220576..9b404338b9e5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt @@ -29,14 +29,17 @@ import com.android.systemui.keyguard.ui.view.layout.sections.DefaultNotification import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopupMenuSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection +import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines import com.android.systemui.util.kotlin.getOrNull import java.util.Optional import javax.inject.Inject import javax.inject.Named +import kotlinx.coroutines.ExperimentalCoroutinesApi /** Vertically aligns the shortcuts with the udfps. */ +@ExperimentalCoroutinesApi @SysUISingleton class ShortcutsBesideUdfpsKeyguardBlueprint @Inject @@ -53,6 +56,7 @@ constructor( defaultNotificationStackScrollLayoutSection: DefaultNotificationStackScrollLayoutSection, aodNotificationIconsSection: AodNotificationIconsSection, aodBurnInSection: AodBurnInSection, + udfpsAccessibilityOverlaySection: DefaultUdfpsAccessibilityOverlaySection, ) : KeyguardBlueprint { override val id: String = SHORTCUTS_BESIDE_UDFPS @@ -68,7 +72,8 @@ constructor( splitShadeGuidelines, aodNotificationIconsSection, aodBurnInSection, - defaultDeviceEntrySection, // Add LAST: Intentionally has z-order above other views. + defaultDeviceEntrySection, + udfpsAccessibilityOverlaySection, // Add LAST: Intentionally has z-order above others ) companion object { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt index 56f717d7e4ef..66c137f7d75e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt @@ -54,7 +54,7 @@ constructor( if (keyguardBottomAreaRefactor()) { indicationAreaHandle = KeyguardIndicationAreaBinder.bind( - constraintLayout, + constraintLayout.requireViewById(R.id.keyguard_indication_area), keyguardIndicationAreaViewModel, keyguardRootViewModel, indicationController, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt new file mode 100644 index 000000000000..e1a33dea2257 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 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. + * + */ + +package com.android.systemui.keyguard.ui.view.layout.sections + +import android.content.Context +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import com.android.systemui.Flags +import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder +import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay +import com.android.systemui.deviceentry.ui.viewmodel.UdfpsAccessibilityOverlayViewModel +import com.android.systemui.keyguard.shared.model.KeyguardSection +import com.android.systemui.res.R +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi + +/** Positions the UDFPS accessibility overlay on the bottom half of the keyguard. */ +@ExperimentalCoroutinesApi +class DefaultUdfpsAccessibilityOverlaySection +@Inject +constructor( + private val context: Context, + private val viewModel: UdfpsAccessibilityOverlayViewModel, +) : KeyguardSection() { + private val viewId = R.id.udfps_accessibility_overlay + private var cachedConstraintLayout: ConstraintLayout? = null + + override fun addViews(constraintLayout: ConstraintLayout) { + cachedConstraintLayout = constraintLayout + constraintLayout.addView(UdfpsAccessibilityOverlay(context).apply { id = viewId }) + } + + override fun bindData(constraintLayout: ConstraintLayout) { + UdfpsAccessibilityOverlayBinder.bind( + constraintLayout.findViewById(viewId)!!, + viewModel, + ) + } + + override fun applyConstraints(constraintSet: ConstraintSet) { + constraintSet.apply { + connect(viewId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) + connect(viewId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) + + create(R.id.udfps_accessibility_overlay_top_guideline, ConstraintSet.HORIZONTAL) + setGuidelinePercent(R.id.udfps_accessibility_overlay_top_guideline, .5f) + connect( + viewId, + ConstraintSet.TOP, + R.id.udfps_accessibility_overlay_top_guideline, + ConstraintSet.BOTTOM, + ) + + if (Flags.keyguardBottomAreaRefactor()) { + connect( + viewId, + ConstraintSet.BOTTOM, + R.id.keyguard_indication_area, + ConstraintSet.TOP, + ) + } else { + connect(viewId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM) + } + } + } + + override fun removeViews(constraintLayout: ConstraintLayout) { + constraintLayout.removeView(viewId) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt index e18893a381f6..f4ae365b2613 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt @@ -43,6 +43,7 @@ constructor( val context: Context, configurationInteractor: ConfigurationInteractor, deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor, + deviceEntryBackgroundViewModel: DeviceEntryBackgroundViewModel, fingerprintPropertyRepository: FingerprintPropertyRepository, ) { private val isSupported: Flow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported @@ -90,26 +91,8 @@ constructor( ) } - private val bgColor: Flow<Int> = - configurationInteractor.onAnyConfigurationChange - .map { - Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorSurface) - } - .onStart { - emit( - Utils.getColorAttrDefaultColor( - context, - com.android.internal.R.attr.colorSurface - ) - ) - } - val bgViewModel: Flow<DeviceEntryBackgroundViewModel.BackgroundViewModel> = - bgColor.map { color -> - DeviceEntryBackgroundViewModel.BackgroundViewModel( - alpha = 1f, - tint = color, - ) - } + val bgColor: Flow<Int> = deviceEntryBackgroundViewModel.color + val bgAlpha: Flow<Float> = flowOf(1f) data class IconLocation( val left: Int, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt index c45caf0b18fd..be9ae1db79e6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt @@ -19,11 +19,10 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.Context import com.android.settingslib.Utils -import com.android.systemui.common.ui.data.repository.ConfigurationRepository +import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart @@ -34,7 +33,7 @@ class DeviceEntryBackgroundViewModel @Inject constructor( val context: Context, - configurationRepository: ConfigurationRepository, // TODO (b/309655554): create & use interactor + configurationInteractor: ConfigurationInteractor, lockscreenToAodTransitionViewModel: LockscreenToAodTransitionViewModel, aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, goneToAodTransitionViewModel: GoneToAodTransitionViewModel, @@ -44,8 +43,8 @@ constructor( dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel, alternateBouncerToAodTransitionViewModel: AlternateBouncerToAodTransitionViewModel, ) { - private val color: Flow<Int> = - configurationRepository.onAnyConfigurationChange + val color: Flow<Int> = + configurationInteractor.onAnyConfigurationChange .map { Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorSurface) } @@ -57,7 +56,7 @@ constructor( ) ) } - private val alpha: Flow<Float> = + val alpha: Flow<Float> = setOf( lockscreenToAodTransitionViewModel.deviceEntryBackgroundViewAlpha, aodToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha, @@ -69,17 +68,4 @@ constructor( alternateBouncerToAodTransitionViewModel.deviceEntryBackgroundViewAlpha, ) .merge() - - val viewModel: Flow<BackgroundViewModel> = - combine(color, alpha) { color, alpha -> - BackgroundViewModel( - alpha = alpha, - tint = color, - ) - } - - data class BackgroundViewModel( - val alpha: Float, - val tint: Int, - ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt new file mode 100644 index 000000000000..d57e569ca7c8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.biometrics.AuthController +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +@SysUISingleton +class LockscreenContentViewModel +@Inject +constructor( + private val interactor: KeyguardBlueprintInteractor, + private val authController: AuthController, +) { + val isUdfpsVisible: Boolean + get() = authController.isUdfpsSupported + + fun blueprintId(scope: CoroutineScope): StateFlow<String> { + return interactor.blueprint + .map { it.id } + .distinctUntilChanged() + .stateIn( + scope = scope, + started = SharingStarted.WhileSubscribed(), + initialValue = interactor.getCurrentBlueprint().id, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt deleted file mode 100644 index 6e77e13e8513..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 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. - */ - -package com.android.systemui.keyguard.ui.viewmodel - -import android.content.Context -import com.android.systemui.keyguard.domain.interactor.Offsets -import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor -import com.android.systemui.res.R -import javax.inject.Inject -import kotlin.math.roundToInt -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -/** View-model for UDFPS AOD view. */ -@ExperimentalCoroutinesApi -class UdfpsAodViewModel -@Inject -constructor( - val interactor: UdfpsKeyguardInteractor, - val context: Context, -) { - val alpha: Flow<Float> = interactor.dozeAmount - val burnInOffsets: Flow<Offsets> = interactor.burnInOffsets - val isVisible: Flow<Boolean> = alpha.map { it != 0f } - - // Padding between the fingerprint icon and its bounding box in pixels. - val padding: Flow<Int> = - interactor.scaleForResolution.map { scale -> - (context.resources.getDimensionPixelSize(R.dimen.lock_icon_padding) * scale) - .roundToInt() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModels.kt deleted file mode 100644 index 098b481491de..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModels.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.android.systemui.keyguard.ui.viewmodel - -import android.graphics.Rect -import com.android.systemui.biometrics.UdfpsKeyguardView -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.ui.binder.UdfpsKeyguardViewBinder -import javax.inject.Inject -import kotlinx.coroutines.ExperimentalCoroutinesApi - -@ExperimentalCoroutinesApi -@SysUISingleton -class UdfpsKeyguardViewModels -@Inject -constructor( - private val viewModel: UdfpsKeyguardViewModel, - private val internalViewModel: UdfpsKeyguardInternalViewModel, - private val aodViewModel: UdfpsAodViewModel, - private val lockscreenFingerprintViewModel: FingerprintViewModel, - private val lockscreenBackgroundViewModel: BackgroundViewModel, -) { - - fun setSensorBounds(sensorBounds: Rect) { - viewModel.sensorBounds = sensorBounds - } - - fun bindViews(view: UdfpsKeyguardView) { - UdfpsKeyguardViewBinder.bind( - view, - viewModel, - internalViewModel, - aodViewModel, - lockscreenFingerprintViewModel, - lockscreenBackgroundViewModel - ) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt deleted file mode 100644 index 642904df21b7..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright (C) 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. - */ - -package com.android.systemui.keyguard.ui.viewmodel - -import android.content.Context -import androidx.annotation.ColorInt -import com.android.settingslib.Utils.getColorAttrDefaultColor -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.domain.interactor.Offsets -import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor -import com.android.systemui.keyguard.shared.model.KeyguardState -import com.android.systemui.keyguard.shared.model.StatusBarState -import com.android.systemui.res.R -import com.android.wm.shell.animation.Interpolators -import javax.inject.Inject -import kotlin.math.roundToInt -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge - -/** View-model for UDFPS lockscreen views. */ -@ExperimentalCoroutinesApi -open class UdfpsLockscreenViewModel( - context: Context, - lockscreenColorResId: Int, - alternateBouncerColorResId: Int, - transitionInteractor: KeyguardTransitionInteractor, - udfpsKeyguardInteractor: UdfpsKeyguardInteractor, - keyguardInteractor: KeyguardInteractor, -) { - private val toLockscreen: Flow<TransitionViewModel> = - transitionInteractor.anyStateToLockscreenTransition.map { - TransitionViewModel( - alpha = - if (it.from == KeyguardState.AOD) { - it.value // animate - } else { - 1f - }, - scale = 1f, - color = getColorAttrDefaultColor(context, lockscreenColorResId), - ) - } - - private val toAlternateBouncer: Flow<TransitionViewModel> = - keyguardInteractor.statusBarState.flatMapLatest { statusBarState -> - transitionInteractor.transitionStepsToState(KeyguardState.ALTERNATE_BOUNCER).map { - TransitionViewModel( - alpha = 1f, - scale = - if (visibleInKeyguardState(it.from, statusBarState)) { - 1f - } else { - Interpolators.FAST_OUT_SLOW_IN.getInterpolation(it.value) - }, - color = getColorAttrDefaultColor(context, alternateBouncerColorResId), - ) - } - } - - private val fadeOut: Flow<TransitionViewModel> = - keyguardInteractor.statusBarState.flatMapLatest { statusBarState -> - merge( - transitionInteractor.anyStateToGoneTransition, - transitionInteractor.anyStateToAodTransition, - transitionInteractor.anyStateToOccludedTransition, - transitionInteractor.anyStateToPrimaryBouncerTransition, - transitionInteractor.anyStateToDreamingTransition, - ) - .map { - TransitionViewModel( - alpha = - if (visibleInKeyguardState(it.from, statusBarState)) { - 1f - it.value - } else { - 0f - }, - scale = 1f, - color = - if (it.from == KeyguardState.ALTERNATE_BOUNCER) { - getColorAttrDefaultColor(context, alternateBouncerColorResId) - } else { - getColorAttrDefaultColor(context, lockscreenColorResId) - }, - ) - } - } - - private fun visibleInKeyguardState( - state: KeyguardState, - statusBarState: StatusBarState - ): Boolean { - return when (state) { - KeyguardState.OFF, - KeyguardState.DOZING, - KeyguardState.DREAMING, - KeyguardState.DREAMING_LOCKSCREEN_HOSTED, - KeyguardState.AOD, - KeyguardState.PRIMARY_BOUNCER, - KeyguardState.GONE, - KeyguardState.OCCLUDED -> false - KeyguardState.LOCKSCREEN -> statusBarState == StatusBarState.KEYGUARD - KeyguardState.ALTERNATE_BOUNCER -> true - } - } - - private val keyguardStateTransition = - merge( - toAlternateBouncer, - toLockscreen, - fadeOut, - ) - - private val dialogHideAffordancesAlphaMultiplier: Flow<Float> = - udfpsKeyguardInteractor.dialogHideAffordancesRequest.map { hideAffordances -> - if (hideAffordances) { - 0f - } else { - 1f - } - } - - private val alphaMultiplier: Flow<Float> = - combine( - transitionInteractor.startedKeyguardState, - dialogHideAffordancesAlphaMultiplier, - udfpsKeyguardInteractor.shadeExpansion, - udfpsKeyguardInteractor.qsProgress, - ) { startedKeyguardState, dialogHideAffordancesAlphaMultiplier, shadeExpansion, qsProgress - -> - if (startedKeyguardState == KeyguardState.ALTERNATE_BOUNCER) { - 1f - } else { - dialogHideAffordancesAlphaMultiplier * (1f - shadeExpansion) * (1f - qsProgress) - } - } - - val transition: Flow<TransitionViewModel> = - combine( - alphaMultiplier, - keyguardStateTransition, - ) { alphaMultiplier, keyguardStateTransition -> - TransitionViewModel( - alpha = keyguardStateTransition.alpha * alphaMultiplier, - scale = keyguardStateTransition.scale, - color = keyguardStateTransition.color, - ) - } - val visible: Flow<Boolean> = transition.map { it.alpha != 0f } -} - -@ExperimentalCoroutinesApi -class FingerprintViewModel -@Inject -constructor( - val context: Context, - transitionInteractor: KeyguardTransitionInteractor, - interactor: UdfpsKeyguardInteractor, - keyguardInteractor: KeyguardInteractor, -) : - UdfpsLockscreenViewModel( - context, - android.R.attr.textColorPrimary, - com.android.internal.R.attr.materialColorOnPrimaryFixed, - transitionInteractor, - interactor, - keyguardInteractor, - ) { - val dozeAmount: Flow<Float> = interactor.dozeAmount - val burnInOffsets: Flow<Offsets> = interactor.burnInOffsets - - // Padding between the fingerprint icon and its bounding box in pixels. - val padding: Flow<Int> = - interactor.scaleForResolution.map { scale -> - (context.resources.getDimensionPixelSize(R.dimen.lock_icon_padding) * scale) - .roundToInt() - } -} - -@ExperimentalCoroutinesApi -class BackgroundViewModel -@Inject -constructor( - val context: Context, - transitionInteractor: KeyguardTransitionInteractor, - interactor: UdfpsKeyguardInteractor, - keyguardInteractor: KeyguardInteractor, -) : - UdfpsLockscreenViewModel( - context, - com.android.internal.R.attr.colorSurface, - com.android.internal.R.attr.materialColorPrimaryFixed, - transitionInteractor, - interactor, - keyguardInteractor, - ) - -data class TransitionViewModel( - val alpha: Float, - val scale: Float, - @ColorInt val color: Int, -) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt index a99c51c2e557..be9393655c5d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt @@ -405,12 +405,15 @@ constructor( } widgetGroupIds.forEach { id -> squishedViewState.widgetStates.get(id)?.let { state -> - state.alpha = - calculateAlpha( - squishFraction, - startPosition / nonsquishedHeight, - endPosition / nonsquishedHeight - ) + // Don't modify alpha for elements that should be invisible (e.g. disabled seekbar) + if (state.alpha != 0f) { + state.alpha = + calculateAlpha( + squishFraction, + startPosition / nonsquishedHeight, + endPosition / nonsquishedHeight + ) + } } } return groupTop // used for the widget group above this group diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaInSceneContainerFlag.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaInSceneContainerFlag.kt index 898298c58b32..77279f225d83 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaInSceneContainerFlag.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaInSceneContainerFlag.kt @@ -17,6 +17,7 @@ package com.android.systemui.media.controls.util import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils /** Helper for reading or using the media_in_scene_container flag state. */ @@ -25,6 +26,10 @@ object MediaInSceneContainerFlag { /** The aconfig flag name */ const val FLAG_NAME = Flags.FLAG_MEDIA_IN_SCENE_CONTAINER + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + /** Is the flag enabled? */ @JvmStatic inline val isEnabled diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index 0e7e69b97d8a..270bfbe4274d 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -263,11 +263,7 @@ constructor( * If the shortcut entry `android:enabled` is set to `true`, the shortcut will be visible in the * Widget Picker to all users. */ - // TODO(b/316332684) - @Suppress("UNREACHABLE_CODE") fun setNoteTaskShortcutEnabled(value: Boolean, user: UserHandle) { - return // shortcut should not be enabled until additional features are implemented. - if (!userManager.isUserUnlocked(user)) { debugLog { "setNoteTaskShortcutEnabled call but user locked: user=$user" } return diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java index 1ab64b76b0dc..ba3357c8b591 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java @@ -17,12 +17,10 @@ package com.android.systemui.qs; import android.content.ComponentName; import android.content.Context; import android.content.res.Resources; -import android.os.Build; import android.provider.Settings; -import com.android.systemui.res.R; import com.android.systemui.plugins.qs.QSTile; -import com.android.systemui.util.leak.GarbageMonitor; +import com.android.systemui.res.R; import java.util.ArrayList; import java.util.Arrays; @@ -44,10 +42,6 @@ public interface QSHost { final String defaultTileList = res.getString(R.string.quick_settings_tiles_default); tiles.addAll(Arrays.asList(defaultTileList.split(","))); - if (Build.IS_DEBUGGABLE - && GarbageMonitor.ADD_MEMORY_TILE_TO_DEFAULT_ON_DEBUGGABLE_BUILDS) { - tiles.add(GarbageMonitor.MemoryTile.TILE_SPEC); - } return tiles; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java index 2af7ae0614ac..47b062430ca5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java @@ -23,7 +23,6 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; -import android.os.Build; import android.provider.Settings; import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; @@ -33,7 +32,6 @@ import android.widget.Button; import androidx.annotation.Nullable; -import com.android.systemui.res.R; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.qs.QSTile; @@ -42,8 +40,8 @@ import com.android.systemui.qs.QSHost; import com.android.systemui.qs.dagger.QSScope; import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon; +import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; -import com.android.systemui.util.leak.GarbageMonitor; import java.util.ArrayList; import java.util.Arrays; @@ -114,9 +112,6 @@ public class TileQueryHelper { possibleTiles.add(spec); } } - if (Build.IS_DEBUGGABLE && !current.contains(GarbageMonitor.MemoryTile.TILE_SPEC)) { - possibleTiles.add(GarbageMonitor.MemoryTile.TILE_SPEC); - } final ArrayList<QSTile> tilesToAdd = new ArrayList<>(); possibleTiles.remove("cell"); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java index 17e6375967fc..bdcbac09a254 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java @@ -14,7 +14,6 @@ package com.android.systemui.qs.tileimpl; -import android.os.Build; import android.util.Log; import androidx.annotation.Nullable; @@ -25,15 +24,14 @@ import com.android.systemui.plugins.qs.QSFactory; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.external.CustomTile; -import com.android.systemui.util.leak.GarbageMonitor; - -import dagger.Lazy; import java.util.Map; import javax.inject.Inject; import javax.inject.Provider; +import dagger.Lazy; + /** * A factory that creates Quick Settings tiles based on a tileSpec * @@ -79,9 +77,7 @@ public class QSFactoryImpl implements QSFactory { @Nullable protected QSTileImpl createTileInternal(String tileSpec) { // Stock tiles. - if (mTileMap.containsKey(tileSpec) - // We should not return a Garbage Monitory Tile if the build is not Debuggable - && (!tileSpec.equals(GarbageMonitor.MemoryTile.TILE_SPEC) || Build.IS_DEBUGGABLE)) { + if (mTileMap.containsKey(tileSpec)) { return mTileMap.get(tileSpec).get(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt index ca5302e13545..c932cee92a30 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt @@ -16,11 +16,15 @@ package com.android.systemui.qs.tiles.impl.custom.data.repository +import android.content.pm.PackageManager +import android.content.pm.ServiceInfo import android.graphics.drawable.Icon import android.os.UserHandle import android.service.quicksettings.Tile +import android.service.quicksettings.TileService import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.qs.external.CustomTileStatePersister +import com.android.systemui.qs.external.PackageManagerAdapter import com.android.systemui.qs.external.TileServiceKey import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tiles.impl.custom.commons.copy @@ -63,6 +67,12 @@ interface CustomTileRepository { */ fun getTile(user: UserHandle): Tile? + /** @see [com.android.systemui.qs.external.TileLifecycleManager.isActiveTile] */ + suspend fun isTileActive(): Boolean + + /** @see [com.android.systemui.qs.external.TileLifecycleManager.isToggleableTile] */ + suspend fun isTileToggleable(): Boolean + /** * Updates tile with the non-null values from [newTile]. Overwrites the current cache when * [user] differs from the cached one. [isPersistable] tile will be persisted to be possibly @@ -92,6 +102,7 @@ class CustomTileRepositoryImpl constructor( private val tileSpec: TileSpec.CustomTileSpec, private val customTileStatePersister: CustomTileStatePersister, + private val packageManagerAdapter: PackageManagerAdapter, @Background private val backgroundContext: CoroutineContext, ) : CustomTileRepository { @@ -149,6 +160,34 @@ constructor( } } + override suspend fun isTileActive(): Boolean = + withContext(backgroundContext) { + try { + val info: ServiceInfo = + packageManagerAdapter.getServiceInfo( + tileSpec.componentName, + META_DATA_QUERY_FLAGS + ) + info.metaData?.getBoolean(TileService.META_DATA_ACTIVE_TILE, false) == true + } catch (e: PackageManager.NameNotFoundException) { + false + } + } + + override suspend fun isTileToggleable(): Boolean = + withContext(backgroundContext) { + try { + val info: ServiceInfo = + packageManagerAdapter.getServiceInfo( + tileSpec.componentName, + META_DATA_QUERY_FLAGS + ) + info.metaData?.getBoolean(TileService.META_DATA_TOGGLEABLE_TILE, false) == true + } catch (e: PackageManager.NameNotFoundException) { + false + } + } + private suspend fun updateTile( user: UserHandle, isPersistable: Boolean, @@ -193,4 +232,12 @@ constructor( private fun UserHandle.getKey() = TileServiceKey(tileSpec.componentName, this.identifier) private data class TileWithUser(val user: UserHandle, val tile: Tile) + + private companion object { + const val META_DATA_QUERY_FLAGS = + (PackageManager.GET_META_DATA or + PackageManager.MATCH_UNINSTALLED_PACKAGES or + PackageManager.MATCH_DIRECT_BOOT_UNAWARE or + PackageManager.MATCH_DIRECT_BOOT_AWARE) + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt index 351bba538463..10b012d30fcd 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt @@ -19,10 +19,9 @@ package com.android.systemui.qs.tiles.impl.custom.domain.interactor import android.os.UserHandle import android.service.quicksettings.Tile import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.qs.external.TileServiceManager import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileDefaultsRepository import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileRepository -import com.android.systemui.qs.tiles.impl.custom.di.bound.CustomTileBoundScope +import com.android.systemui.qs.tiles.impl.di.QSTileScope import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope @@ -35,15 +34,13 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach /** Manages updates of the [Tile] assigned for the current custom tile. */ -@CustomTileBoundScope +@QSTileScope class CustomTileInteractor @Inject constructor( - private val user: UserHandle, private val defaultsRepository: CustomTileDefaultsRepository, private val customTileRepository: CustomTileRepository, - private val tileServiceManager: TileServiceManager, - @CustomTileBoundScope private val boundScope: CoroutineScope, + @QSTileScope private val tileScope: CoroutineScope, @Background private val backgroundContext: CoroutineContext, ) { @@ -51,8 +48,7 @@ constructor( MutableSharedFlow<Tile>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) /** [Tile] updates. [updateTile] to emit a new one. */ - val tiles: Flow<Tile> - get() = customTileRepository.getTiles(user) + fun getTiles(user: UserHandle): Flow<Tile> = customTileRepository.getTiles(user) /** * Current [Tile] @@ -61,10 +57,14 @@ constructor( * the tile hasn't been updated for the current user. Can happen when this is accessed before * [init] returns. */ - val tile: Tile - get() = - customTileRepository.getTile(user) - ?: throw IllegalStateException("Attempt to get a tile for a wrong user") + fun getTile(user: UserHandle): Tile = + customTileRepository.getTile(user) + ?: throw IllegalStateException("Attempt to get a tile for a wrong user") + + /** + * True if the tile is toggleable like a switch and false if it operates as a clickable button. + */ + suspend fun isTileToggleable(): Boolean = customTileRepository.isTileToggleable() /** * Initializes the repository for the current user. Suspends until it's safe to call [tile] @@ -73,36 +73,36 @@ constructor( * - receive tile update in [updateTile]; * - restoration happened for a persisted tile. */ - suspend fun init() { - launchUpdates() - customTileRepository.restoreForTheUserIfNeeded(user, tileServiceManager.isActiveTile) + suspend fun initForUser(user: UserHandle) { + launchUpdates(user) + customTileRepository.restoreForTheUserIfNeeded(user, customTileRepository.isTileActive()) // Suspend to make sure it gets the tile from one of the sources: restoration, defaults, or // tile update. customTileRepository.getTiles(user).firstOrNull() } - private fun launchUpdates() { + private fun launchUpdates(user: UserHandle) { tileUpdates .onEach { customTileRepository.updateWithTile( user, it, - tileServiceManager.isActiveTile, + customTileRepository.isTileActive(), ) } .flowOn(backgroundContext) - .launchIn(boundScope) + .launchIn(tileScope) defaultsRepository .defaults(user) .onEach { customTileRepository.updateWithDefaults( user, it, - tileServiceManager.isActiveTile, + customTileRepository.isTileActive(), ) } .flowOn(backgroundContext) - .launchIn(boundScope) + .launchIn(tileScope) } /** Updates current [Tile]. Emits a new event in [tiles]. */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt index 580c42122a85..ef3df482ea45 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt @@ -22,8 +22,8 @@ import kotlinx.coroutines.flow.StateFlow /** * Represents tiles behaviour logic. This ViewModel is a connection between tile view and data - * layers. All direct inheritors must be added to the [QSTileViewModelInterfaceComplianceTest] class - * to pass compliance tests. + * layers. All direct inheritors must be added to the + * [com.android.systemui.qs.tiles.viewmodel.QSTileViewModelTest] class to pass interface tests. * * All methods of this view model should be considered running on the main thread. This means no * synchronous long running operations are permitted in any method. diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt index 1156250666f3..8c3e4a5d0be2 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt @@ -14,30 +14,94 @@ * limitations under the License. */ +@file:Suppress("NOTHING_TO_INLINE") + package com.android.systemui.scene.shared.flag -import android.content.Context -import androidx.annotation.VisibleForTesting -import com.android.systemui.Flags as AConfigFlags +import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR +import com.android.systemui.Flags.FLAG_SCENE_CONTAINER import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.Flags.sceneContainer import com.android.systemui.compose.ComposeFacade import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.flags.FeatureFlagsClassic -import com.android.systemui.flags.Flag -import com.android.systemui.flags.Flags -import com.android.systemui.flags.ReleasedFlag -import com.android.systemui.flags.ResourceBooleanFlag -import com.android.systemui.flags.UnreleasedFlag +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.Flags.SCENE_CONTAINER_ENABLED +import com.android.systemui.flags.RefactorFlagUtils import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl import com.android.systemui.media.controls.util.MediaInSceneContainerFlag -import com.android.systemui.res.R import dagger.Module import dagger.Provides -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject + +/** Helper for reading or using the scene container flag state. */ +object SceneContainerFlag { + /** The flag description -- not an aconfig flag name */ + const val DESCRIPTION = "SceneContainerFlag" + + inline val isEnabled + get() = + SCENE_CONTAINER_ENABLED && // mainStaticFlag + sceneContainer() && // mainAconfigFlag + keyguardBottomAreaRefactor() && + KeyguardShadeMigrationNssl.isEnabled && + MediaInSceneContainerFlag.isEnabled && + ComposeFacade.isComposeAvailable() + + /** + * The main static flag, SCENE_CONTAINER_ENABLED. This is an explicit static flag check that + * helps with downstream optimizations (like unused code stripping) in builds where aconfig + * flags are still writable. Do not remove! + */ + inline fun getMainStaticFlag() = + FlagToken("Flags.SCENE_CONTAINER_ENABLED", SCENE_CONTAINER_ENABLED) + + /** The main aconfig flag. */ + inline fun getMainAconfigFlag() = FlagToken(FLAG_SCENE_CONTAINER, sceneContainer()) + + /** The set of secondary flags which must be enabled for scene container to work properly */ + inline fun getSecondaryFlags(): Sequence<FlagToken> = + sequenceOf( + FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor()), + KeyguardShadeMigrationNssl.token, + MediaInSceneContainerFlag.token, + ) + + /** The full set of requirements for SceneContainer */ + inline fun getAllRequirements(): Sequence<FlagToken> { + val composeRequirement = + FlagToken("ComposeFacade.isComposeAvailable()", ComposeFacade.isComposeAvailable()) + return sequenceOf(getMainStaticFlag(), getMainAconfigFlag()) + + getSecondaryFlags() + + composeRequirement + } + + /** Return all dependencies of this flag in pairs where [Pair.first] depends on [Pair.second] */ + inline fun getFlagDependencies(): Sequence<Pair<FlagToken, FlagToken>> { + val mainStaticFlag = getMainStaticFlag() + val mainAconfigFlag = getMainAconfigFlag() + return sequence { + // The static and aconfig flags should be equal; make them co-dependent + yield(mainAconfigFlag to mainStaticFlag) + yield(mainStaticFlag to mainAconfigFlag) + // all other flags depend on the static flag for brevity + } + getSecondaryFlags().map { mainStaticFlag to it } + } + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, DESCRIPTION) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, DESCRIPTION) +} /** * Defines interface for classes that can check whether the scene container framework feature is @@ -52,133 +116,25 @@ interface SceneContainerFlags { fun requirementDescription(): String } -class SceneContainerFlagsImpl -@AssistedInject -constructor( - @Application private val context: Context, - private val featureFlagsClassic: FeatureFlagsClassic, - @Assisted private val isComposeAvailable: Boolean, -) : SceneContainerFlags { - - companion object { - @VisibleForTesting - val classicFlagTokens: List<Flag<Boolean>> = - listOf( - Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW, - ) - } - - /** The list of requirements, all must be met for the feature to be enabled. */ - private val requirements = - listOf( - AconfigFlagMustBeEnabled( - flagName = AConfigFlags.FLAG_SCENE_CONTAINER, - flagValue = sceneContainer(), - ), - AconfigFlagMustBeEnabled( - flagName = AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, - flagValue = keyguardBottomAreaRefactor(), - ), - AconfigFlagMustBeEnabled( - flagName = KeyguardShadeMigrationNssl.FLAG_NAME, - flagValue = KeyguardShadeMigrationNssl.isEnabled, - ), - AconfigFlagMustBeEnabled( - flagName = MediaInSceneContainerFlag.FLAG_NAME, - flagValue = MediaInSceneContainerFlag.isEnabled, - ), - ) + - classicFlagTokens.map { flagToken -> FlagMustBeEnabled(flagToken) } + - listOf( - ComposeMustBeAvailable(), - CompileTimeFlagMustBeEnabled(), - ResourceConfigMustBeEnabled() - ) +class SceneContainerFlagsImpl : SceneContainerFlags { override fun isEnabled(): Boolean { - // SCENE_CONTAINER_ENABLED is an explicit static flag check that helps with downstream - // optimizations, e.g., unused code stripping. Do not remove! - return Flags.SCENE_CONTAINER_ENABLED && requirements.all { it.isMet() } + return SceneContainerFlag.isEnabled } override fun requirementDescription(): String { return buildString { - requirements.forEach { requirement -> + SceneContainerFlag.getAllRequirements().forEach { requirement -> append('\n') - append(if (requirement.isMet()) " [MET]" else "[NOT MET]") + append(if (requirement.isEnabled) " [MET]" else "[NOT MET]") append(" ${requirement.name}") } } } - - private interface Requirement { - val name: String - - fun isMet(): Boolean - } - - private inner class ComposeMustBeAvailable : Requirement { - override val name = "Jetpack Compose must be available" - - override fun isMet(): Boolean { - return isComposeAvailable - } - } - - private inner class CompileTimeFlagMustBeEnabled : Requirement { - override val name = "Flags.SCENE_CONTAINER_ENABLED must be enabled in code" - - override fun isMet(): Boolean { - return Flags.SCENE_CONTAINER_ENABLED - } - } - - private inner class FlagMustBeEnabled<FlagType : Flag<*>>( - private val flag: FlagType, - ) : Requirement { - override val name = "Flag ${flag.name} must be enabled" - - override fun isMet(): Boolean { - return when (flag) { - is ResourceBooleanFlag -> featureFlagsClassic.isEnabled(flag) - is ReleasedFlag -> featureFlagsClassic.isEnabled(flag) - is UnreleasedFlag -> featureFlagsClassic.isEnabled(flag) - else -> error("Unsupported flag type ${flag.javaClass}") - } - } - } - - private inner class AconfigFlagMustBeEnabled( - flagName: String, - private val flagValue: Boolean, - ) : Requirement { - override val name: String = "Aconfig flag $flagName must be enabled" - - override fun isMet(): Boolean { - return flagValue - } - } - - private inner class ResourceConfigMustBeEnabled : Requirement { - override val name: String = "R.bool.config_sceneContainerFrameworkEnabled must be true" - - override fun isMet(): Boolean { - return context.resources.getBoolean(R.bool.config_sceneContainerFrameworkEnabled) - } - } - - @AssistedFactory - interface Factory { - fun create(isComposeAvailable: Boolean): SceneContainerFlagsImpl - } } @Module object SceneContainerFlagsModule { - @Provides - @SysUISingleton - fun impl(factory: SceneContainerFlagsImpl.Factory): SceneContainerFlags { - return factory.create(ComposeFacade.isComposeAvailable()) - } + @Provides @SysUISingleton fun impl(): SceneContainerFlags = SceneContainerFlagsImpl() } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt index 5cbea90580a1..7130fa1a1bd3 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt @@ -11,8 +11,6 @@ import android.view.ViewTreeObserver import android.view.animation.AccelerateDecelerateInterpolator import androidx.constraintlayout.widget.Guideline import com.android.systemui.res.R -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import javax.inject.Inject /** @@ -23,7 +21,6 @@ class MessageContainerController constructor( private val workProfileMessageController: WorkProfileMessageController, private val screenshotDetectionController: ScreenshotDetectionController, - private val featureFlags: FeatureFlags, ) { private lateinit var container: ViewGroup private lateinit var guideline: Guideline @@ -63,10 +60,8 @@ constructor( fun onScreenshotTaken(screenshot: ScreenshotData) { val workProfileData = workProfileMessageController.onScreenshotTaken(screenshot.userHandle) - var notifiedApps: List<CharSequence> = listOf() - if (featureFlags.isEnabled(Flags.SCREENSHOT_DETECTION)) { - notifiedApps = screenshotDetectionController.maybeNotifyOfScreenshot(screenshot) - } + var notifiedApps: List<CharSequence> = + screenshotDetectionController.maybeNotifyOfScreenshot(screenshot) // If work profile first run needs to show, bias towards that, otherwise show screenshot // detection notification if needed. diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 95f7c94a235f..878e6faf32e7 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -1096,7 +1096,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } @Override - public void onPulseExpansionChanged(boolean expandingChanged) { + public void onPulseExpansionAmountChanged(boolean expandingChanged) { if (mKeyguardBypassController.getBypassEnabled()) { // Position the notifications while dragging down while pulsing requestScrollerTopPaddingUpdate(false /* animate */); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt index 8d1e8d0ab524..0c67279c1660 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt @@ -20,9 +20,9 @@ import android.util.FloatProperty import android.view.animation.Interpolator import androidx.annotation.VisibleForTesting import androidx.core.animation.ObjectAnimator -import com.android.systemui.Dumpable import com.android.app.animation.Interpolators import com.android.app.animation.InterpolatorsAndroidX +import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -31,6 +31,7 @@ import com.android.systemui.shade.ShadeExpansionListener import com.android.systemui.shade.ShadeViewController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.notification.stack.StackStateAnimator import com.android.systemui.statusbar.phone.DozeParameters @@ -206,8 +207,15 @@ constructor( val nowExpanding = isPulseExpanding() val changed = nowExpanding != pulseExpanding pulseExpanding = nowExpanding - for (listener in wakeUpListeners) { - listener.onPulseExpansionChanged(changed) + if (!NotificationIconContainerRefactor.isEnabled) { + for (listener in wakeUpListeners) { + listener.onPulseExpansionAmountChanged(changed) + } + } + if (changed) { + for (listener in wakeUpListeners) { + listener.onPulseExpandingChanged(pulseExpanding) + } } } } @@ -620,13 +628,20 @@ constructor( * * @param expandingChanged if the user has started or stopped expanding */ - fun onPulseExpansionChanged(expandingChanged: Boolean) {} + @Deprecated( + message = "Use onPulseExpandedChanged instead.", + replaceWith = ReplaceWith("onPulseExpandedChanged"), + ) + fun onPulseExpansionAmountChanged(expandingChanged: Boolean) {} /** * Called when the animator started by [scheduleDelayedDozeAmountAnimation] begins running * after the start delay, or after it ends/is cancelled. */ fun onDelayedDozeAmountAnimationRunning(running: Boolean) {} + + /** Called whenever a pulse has started or stopped expanding. */ + fun onPulseExpandingChanged(isPulseExpanding: Boolean) {} } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt index 3e9c6fbb2ec4..3b48b3922dc5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt @@ -3,10 +3,8 @@ package com.android.systemui.statusbar.notification import android.util.FloatProperty import android.view.View import androidx.annotation.FloatRange -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import com.android.systemui.flags.RefactorFlag import com.android.systemui.res.R +import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation import com.android.systemui.statusbar.notification.stack.AnimationProperties import com.android.systemui.statusbar.notification.stack.StackStateAnimator import kotlin.math.abs @@ -42,13 +40,13 @@ interface Roundable { /** Current top corner in pixel, based on [topRoundness] and [maxRadius] */ val topCornerRadius: Float get() = - if (roundableState.newHeadsUpAnim.isEnabled) roundableState.topCornerRadius + if (NotificationsImprovedHunAnimation.isEnabled) roundableState.topCornerRadius else topRoundness * maxRadius /** Current bottom corner in pixel, based on [bottomRoundness] and [maxRadius] */ val bottomCornerRadius: Float get() = - if (roundableState.newHeadsUpAnim.isEnabled) roundableState.bottomCornerRadius + if (NotificationsImprovedHunAnimation.isEnabled) roundableState.bottomCornerRadius else bottomRoundness * maxRadius /** Get and update the current radii */ @@ -318,13 +316,10 @@ constructor( internal val targetView: View, private val roundable: Roundable, maxRadius: Float, - featureFlags: FeatureFlags? = null ) { internal var maxRadius = maxRadius private set - internal val newHeadsUpAnim = RefactorFlag.forView(Flags.IMPROVED_HUN_ANIMATIONS, featureFlags) - /** Animatable for top roundness */ private val topAnimatable = topAnimatable(roundable) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt index cf03d1c5addc..2cc1403a80a5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt @@ -62,8 +62,8 @@ constructor( override val isPulseExpanding: Flow<Boolean> = conflatedCallbackFlow { val listener = object : NotificationWakeUpCoordinator.WakeUpListener { - override fun onPulseExpansionChanged(expandingChanged: Boolean) { - trySend(expandingChanged) + override fun onPulseExpandingChanged(isPulseExpanding: Boolean) { + trySend(isPulseExpanding) } } trySend(wakeUpCoordinator.isPulseExpanding()) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt index 61f9be54c795..76e5fd3bd4f2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt @@ -112,15 +112,6 @@ class IconManager @Inject constructor( aodIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE aodIcon.setIncreasedSize(true) - // Construct the centered icon view. - val centeredIcon = if (entry.sbn.notification.isMediaNotification) { - iconBuilder.createIconView(entry).apply { - scaleType = ImageView.ScaleType.CENTER_INSIDE - } - } else { - null - } - // Set the icon views' icons val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry) @@ -128,10 +119,7 @@ class IconManager @Inject constructor( setIcon(entry, normalIconDescriptor, sbIcon) setIcon(entry, sensitiveIconDescriptor, shelfIcon) setIcon(entry, sensitiveIconDescriptor, aodIcon) - if (centeredIcon != null) { - setIcon(entry, normalIconDescriptor, centeredIcon) - } - entry.icons = IconPack.buildPack(sbIcon, shelfIcon, aodIcon, centeredIcon, entry.icons) + entry.icons = IconPack.buildPack(sbIcon, shelfIcon, aodIcon, entry.icons) } catch (e: InflationException) { entry.icons = IconPack.buildEmptyPack(entry.icons) throw e @@ -171,11 +159,6 @@ class IconManager @Inject constructor( it.setNotification(entry.sbn, notificationContentDescription) setIcon(entry, sensitiveIconDescriptor, it) } - - entry.icons.centeredIcon?.let { - it.setNotification(entry.sbn, notificationContentDescription) - setIcon(entry, sensitiveIconDescriptor, it) - } } private fun updateIconsSafe(entry: NotificationEntry) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java index 054e381f096a..442c0978fd77 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java @@ -31,7 +31,6 @@ public final class IconPack { @Nullable private final StatusBarIconView mStatusBarIcon; @Nullable private final StatusBarIconView mShelfIcon; @Nullable private final StatusBarIconView mAodIcon; - @Nullable private final StatusBarIconView mCenteredIcon; @Nullable private StatusBarIcon mSmallIconDescriptor; @Nullable private StatusBarIcon mPeopleAvatarDescriptor; @@ -43,7 +42,7 @@ public final class IconPack { * haven't been inflated yet or there was an error while inflating them). */ public static IconPack buildEmptyPack(@Nullable IconPack fromSource) { - return new IconPack(false, null, null, null, null, fromSource); + return new IconPack(false, null, null, null, fromSource); } /** @@ -53,9 +52,8 @@ public final class IconPack { @NonNull StatusBarIconView statusBarIcon, @NonNull StatusBarIconView shelfIcon, @NonNull StatusBarIconView aodIcon, - @Nullable StatusBarIconView centeredIcon, @Nullable IconPack source) { - return new IconPack(true, statusBarIcon, shelfIcon, aodIcon, centeredIcon, source); + return new IconPack(true, statusBarIcon, shelfIcon, aodIcon, source); } private IconPack( @@ -63,12 +61,10 @@ public final class IconPack { @Nullable StatusBarIconView statusBarIcon, @Nullable StatusBarIconView shelfIcon, @Nullable StatusBarIconView aodIcon, - @Nullable StatusBarIconView centeredIcon, @Nullable IconPack source) { mAreIconsAvailable = areIconsAvailable; mStatusBarIcon = statusBarIcon; mShelfIcon = shelfIcon; - mCenteredIcon = centeredIcon; mAodIcon = aodIcon; if (source != null) { mIsImportantConversation = source.mIsImportantConversation; @@ -91,11 +87,6 @@ public final class IconPack { return mShelfIcon; } - @Nullable - public StatusBarIconView getCenteredIcon() { - return mCenteredIcon; - } - /** The version of the icon that's shown when pulsing (in AOD). */ @Nullable public StatusBarIconView getAodIcon() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt new file mode 100644 index 000000000000..d7c29f19fe57 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.statusbar.notification.icon.ui.viewbinder + +import androidx.lifecycle.lifecycleScope +import com.android.systemui.common.ui.ConfigurationState +import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder +import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.notification.collection.NotifCollection +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore +import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel +import com.android.systemui.statusbar.phone.NotificationIconContainer +import com.android.systemui.statusbar.phone.ScreenOffAnimationController +import com.android.systemui.statusbar.ui.SystemBarUtilsState +import javax.inject.Inject +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.launch + +/** Binds a [NotificationIconContainer] to a [NotificationIconContainerAlwaysOnDisplayViewModel]. */ +class NotificationIconContainerAlwaysOnDisplayViewBinder +@Inject +constructor( + private val viewModel: NotificationIconContainerAlwaysOnDisplayViewModel, + private val keyguardRootViewModel: KeyguardRootViewModel, + private val configuration: ConfigurationState, + private val failureTracker: StatusBarIconViewBindingFailureTracker, + private val screenOffAnimationController: ScreenOffAnimationController, + private val systemBarUtilsState: SystemBarUtilsState, + private val viewStore: AlwaysOnDisplayNotificationIconViewStore, +) { + fun bindWhileAttached(view: NotificationIconContainer): DisposableHandle { + return view.repeatWhenAttached { + lifecycleScope.launch { + launch { + NotificationIconContainerViewBinder.bind( + view = view, + viewModel = viewModel, + configuration = configuration, + systemBarUtilsState = systemBarUtilsState, + failureTracker = failureTracker, + viewStore = viewStore, + ) + } + launch { + KeyguardRootViewBinder.bindAodNotifIconVisibility( + view = view, + isVisible = keyguardRootViewModel.isNotifIconContainerVisible, + configuration = configuration, + screenOffAnimationController = screenOffAnimationController, + ) + } + } + } + } +} + +/** [IconViewStore] for the always-on display. */ +class AlwaysOnDisplayNotificationIconViewStore +@Inject +constructor(notifCollection: NotifCollection) : + IconViewStore by (notifCollection.iconViewStoreBy { it.aodIcon }) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerShelfViewBinder.kt new file mode 100644 index 000000000000..783488af3a47 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerShelfViewBinder.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.statusbar.notification.icon.ui.viewbinder + +import com.android.systemui.common.ui.ConfigurationState +import com.android.systemui.statusbar.notification.collection.NotifCollection +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.bindIcons +import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel +import com.android.systemui.statusbar.phone.NotificationIconContainer +import com.android.systemui.statusbar.ui.SystemBarUtilsState +import javax.inject.Inject + +/** Binds a [NotificationIconContainer] to a [NotificationIconContainerShelfViewModel]. */ +class NotificationIconContainerShelfViewBinder +@Inject +constructor( + private val viewModel: NotificationIconContainerShelfViewModel, + private val configuration: ConfigurationState, + private val systemBarUtilsState: SystemBarUtilsState, + private val failureTracker: StatusBarIconViewBindingFailureTracker, + private val viewStore: ShelfNotificationIconViewStore, +) { + suspend fun bind(view: NotificationIconContainer) { + viewModel.icons.bindIcons( + view, + configuration, + systemBarUtilsState, + notifyBindingFailures = { failureTracker.shelfFailures = it }, + viewStore, + ) + } +} + +/** [IconViewStore] for the [com.android.systemui.statusbar.NotificationShelf] */ +class ShelfNotificationIconViewStore @Inject constructor(notifCollection: NotifCollection) : + IconViewStore by (notifCollection.iconViewStoreBy { it.shelfIcon }) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt new file mode 100644 index 000000000000..8e089b1f11fd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.statusbar.notification.icon.ui.viewbinder + +import androidx.lifecycle.lifecycleScope +import com.android.systemui.common.ui.ConfigurationState +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.notification.collection.NotifCollection +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore +import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel +import com.android.systemui.statusbar.phone.NotificationIconContainer +import com.android.systemui.statusbar.ui.SystemBarUtilsState +import javax.inject.Inject +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.launch + +/** Binds a [NotificationIconContainer] to a [NotificationIconContainerStatusBarViewModel]. */ +class NotificationIconContainerStatusBarViewBinder +@Inject +constructor( + private val viewModel: NotificationIconContainerStatusBarViewModel, + private val configuration: ConfigurationState, + private val systemBarUtilsState: SystemBarUtilsState, + private val failureTracker: StatusBarIconViewBindingFailureTracker, + private val viewStore: StatusBarNotificationIconViewStore, +) { + fun bindWhileAttached(view: NotificationIconContainer): DisposableHandle { + return view.repeatWhenAttached { + lifecycleScope.launch { + NotificationIconContainerViewBinder.bind( + view = view, + viewModel = viewModel, + configuration = configuration, + systemBarUtilsState = systemBarUtilsState, + failureTracker = failureTracker, + viewStore = viewStore, + ) + } + } + } +} + +/** [IconViewStore] for the status bar. */ +class StatusBarNotificationIconViewStore @Inject constructor(notifCollection: NotifCollection) : + IconViewStore by (notifCollection.iconViewStoreBy { it.statusBarIcon }) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt index e1e30e1d74f0..8fe00225463b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt @@ -35,7 +35,6 @@ import com.android.systemui.statusbar.notification.icon.IconPack import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconColors import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel -import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconsViewData import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconsViewData.LimitType @@ -45,7 +44,6 @@ import com.android.systemui.util.kotlin.mapValuesNotNullTo import com.android.systemui.util.ui.isAnimating import com.android.systemui.util.ui.stopAnimating import com.android.systemui.util.ui.value -import javax.inject.Inject import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope @@ -56,42 +54,6 @@ import kotlinx.coroutines.launch /** Binds a view-model to a [NotificationIconContainer]. */ object NotificationIconContainerViewBinder { - @JvmStatic - fun bindWhileAttached( - view: NotificationIconContainer, - viewModel: NotificationIconContainerShelfViewModel, - configuration: ConfigurationState, - systemBarUtilsState: SystemBarUtilsState, - failureTracker: StatusBarIconViewBindingFailureTracker, - viewStore: IconViewStore, - ): DisposableHandle { - return view.repeatWhenAttached { - lifecycleScope.launch { - viewModel.icons.bindIcons( - view, - configuration, - systemBarUtilsState, - notifyBindingFailures = { failureTracker.shelfFailures = it }, - viewStore, - ) - } - } - } - - @JvmStatic - fun bindWhileAttached( - view: NotificationIconContainer, - viewModel: NotificationIconContainerStatusBarViewModel, - configuration: ConfigurationState, - systemBarUtilsState: SystemBarUtilsState, - failureTracker: StatusBarIconViewBindingFailureTracker, - viewStore: IconViewStore, - ): DisposableHandle = - view.repeatWhenAttached { - lifecycleScope.launch { - bind(view, viewModel, configuration, systemBarUtilsState, failureTracker, viewStore) - } - } suspend fun bind( view: NotificationIconContainer, @@ -215,7 +177,7 @@ object NotificationIconContainerViewBinder { * given `iconKey`. The parent [Job] of this coroutine will be cancelled automatically when the * view is to be unbound. */ - private suspend fun Flow<NotificationIconsViewData>.bindIcons( + suspend fun Flow<NotificationIconsViewData>.bindIcons( view: NotificationIconContainer, configuration: ConfigurationState, systemBarUtilsState: SystemBarUtilsState, @@ -377,24 +339,14 @@ object NotificationIconContainerViewBinder { } @ColorInt private const val DEFAULT_AOD_ICON_COLOR = Color.WHITE - private const val TAG = "NotifIconContainerViewBinder" + private const val TAG = "NotifIconContainerViewBinder" } -/** [IconViewStore] for the [com.android.systemui.statusbar.NotificationShelf] */ -class ShelfNotificationIconViewStore @Inject constructor(notifCollection: NotifCollection) : - IconViewStore by (notifCollection.iconViewStoreBy { it.shelfIcon }) - -/** [IconViewStore] for the always-on display. */ -class AlwaysOnDisplayNotificationIconViewStore -@Inject -constructor(notifCollection: NotifCollection) : - IconViewStore by (notifCollection.iconViewStoreBy { it.aodIcon }) - -/** [IconViewStore] for the status bar. */ -class StatusBarNotificationIconViewStore @Inject constructor(notifCollection: NotifCollection) : - IconViewStore by (notifCollection.iconViewStoreBy { it.statusBarIcon }) - -private fun NotifCollection.iconViewStoreBy(block: (IconPack) -> StatusBarIconView?) = +/** + * Convenience builder for [IconViewStore] that uses [block] to extract the relevant + * [StatusBarIconView] from an [IconPack] stored inside of the [NotifCollection]. + */ +fun NotifCollection.iconViewStoreBy(block: (IconPack) -> StatusBarIconView?) = IconViewStore { key -> getEntry(key)?.icons?.let(block) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionRefactor.kt index 2624363c7dec..db33f92c1848 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionRefactor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionRefactor.kt @@ -17,12 +17,17 @@ package com.android.systemui.statusbar.notification.interruption import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils /** Helper for reading or using the visual interruptions refactor flag state. */ object VisualInterruptionRefactor { const val FLAG_NAME = Flags.FLAG_VISUAL_INTERRUPTIONS_REFACTOR + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + /** Whether the refactor is enabled */ @JvmStatic inline val isEnabled diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index 4fe05ec9990c..fca527f5fc4d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -31,7 +31,6 @@ import android.view.Choreographer; import android.view.MotionEvent; import android.view.View; import android.view.animation.Interpolator; -import android.view.animation.PathInterpolator; import com.android.app.animation.Interpolators; import com.android.internal.jank.InteractionJankMonitor; @@ -44,6 +43,7 @@ import com.android.systemui.statusbar.notification.FakeShadowView; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; +import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; import com.android.systemui.util.DumpUtilsKt; @@ -67,7 +67,8 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView * The content of the view should start showing at animation progress value of * #ALPHA_APPEAR_START_FRACTION. */ - private static final float ALPHA_APPEAR_START_FRACTION = .4f; + + private static final float ALPHA_APPEAR_START_FRACTION = .7f; /** * The content should show fully with progress at #ALPHA_APPEAR_END_FRACTION * The start of the animation is at #ALPHA_APPEAR_START_FRACTION @@ -86,9 +87,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView */ private boolean mActivated; - private final Interpolator mSlowOutFastInInterpolator; private Interpolator mCurrentAppearInterpolator; - NotificationBackgroundView mBackgroundNormal; private float mAnimationTranslationY; private boolean mDrawingAppearAnimation; @@ -116,7 +115,6 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView public ActivatableNotificationView(Context context, AttributeSet attrs) { super(context, attrs); - mSlowOutFastInInterpolator = new PathInterpolator(0.8f, 0.0f, 0.6f, 1.0f); setClipChildren(false); setClipToPadding(false); updateColors(); @@ -400,12 +398,16 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN; targetValue = 1.0f; } else { - mCurrentAppearInterpolator = mSlowOutFastInInterpolator; + mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE; targetValue = 0.0f; } mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction, targetValue); - mAppearAnimator.setInterpolator(Interpolators.LINEAR); + if (NotificationsImprovedHunAnimation.isEnabled()) { + mAppearAnimator.setInterpolator(mCurrentAppearInterpolator); + } else { + mAppearAnimator.setInterpolator(Interpolators.LINEAR); + } mAppearAnimator.setDuration( (long) (duration * Math.abs(mAppearAnimationFraction - targetValue))); mAppearAnimator.addUpdateListener(animation -> { @@ -502,8 +504,9 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView } private void updateAppearRect() { - float interpolatedFraction = mCurrentAppearInterpolator.getInterpolation( - mAppearAnimationFraction); + float interpolatedFraction = + NotificationsImprovedHunAnimation.isEnabled() ? mAppearAnimationFraction + : mCurrentAppearInterpolator.getInterpolation(mAppearAnimationFraction); mAppearAnimationTranslation = (1.0f - interpolatedFraction) * mAnimationTranslationY; final int actualHeight = getActualHeight(); float bottom = actualHeight * interpolatedFraction; @@ -524,6 +527,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView } private float getInterpolatedAppearAnimationFraction() { + if (mAppearAnimationFraction >= 0) { return mCurrentAppearInterpolator.getInterpolation(mAppearAnimationFraction); } @@ -569,7 +573,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView @Override public float getTopCornerRadius() { - if (mImprovedHunAnimation.isEnabled()) { + if (NotificationsImprovedHunAnimation.isEnabled()) { return super.getTopCornerRadius(); } @@ -579,7 +583,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView @Override public float getBottomCornerRadius() { - if (mImprovedHunAnimation.isEnabled()) { + if (NotificationsImprovedHunAnimation.isEnabled()) { return super.getBottomCornerRadius(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java index 2a3e69b7f4d4..aefd34817fdc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java @@ -28,10 +28,9 @@ import android.util.IndentingPrintWriter; import android.view.View; import android.view.ViewOutlineProvider; -import com.android.systemui.flags.Flags; -import com.android.systemui.flags.RefactorFlag; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.RoundableState; +import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation; import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer; import com.android.systemui.util.DumpUtilsKt; @@ -49,8 +48,6 @@ public abstract class ExpandableOutlineView extends ExpandableView { private float mOutlineAlpha = -1f; private boolean mAlwaysRoundBothCorners; private Path mTmpPath = new Path(); - protected final RefactorFlag mImprovedHunAnimation = - RefactorFlag.forView(Flags.IMPROVED_HUN_ANIMATIONS); /** * {@code false} if the children views of the {@link ExpandableOutlineView} are translated when @@ -126,7 +123,7 @@ public abstract class ExpandableOutlineView extends ExpandableView { return EMPTY_PATH; } float bottomRadius = mAlwaysRoundBothCorners ? getMaxRadius() : getBottomCornerRadius(); - if (!mImprovedHunAnimation.isEnabled() && (topRadius + bottomRadius > height)) { + if (!NotificationsImprovedHunAnimation.isEnabled() && (topRadius + bottomRadius > height)) { float overShoot = topRadius + bottomRadius - height; float currentTopRoundness = getTopRoundness(); float currentBottomRoundness = getBottomRoundness(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/AsyncHybridViewInflation.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/AsyncHybridViewInflation.kt index 24e7f05a9c1d..9556c2e3159c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/AsyncHybridViewInflation.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/AsyncHybridViewInflation.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.row.shared import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils /** Helper for reading or using the async hybrid view inflation flag state. */ @@ -24,6 +25,10 @@ import com.android.systemui.flags.RefactorFlagUtils object AsyncHybridViewInflation { const val FLAG_NAME = Flags.FLAG_NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + /** Is async hybrid (single-line) view inflation enabled */ @JvmStatic inline val isEnabled diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsImprovedHunAnimation.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsImprovedHunAnimation.kt new file mode 100644 index 000000000000..16d35fe9fa68 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsImprovedHunAnimation.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.statusbar.notification.shared + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the notifications improved hun animation flag state. */ +@Suppress("NOTHING_TO_INLINE") +object NotificationsImprovedHunAnimation { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_IMPROVED_HUN_ANIMATION + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.notificationsImprovedHunAnimation() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt index 699e1406bc18..5ab4d4eae929 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt @@ -16,60 +16,38 @@ package com.android.systemui.statusbar.notification.shelf.ui.viewbinder -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.common.ui.ConfigurationState -import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.NotificationShelf -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder import com.android.systemui.statusbar.notification.row.ui.viewbinder.ActivatableNotificationViewBinder import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel import com.android.systemui.statusbar.phone.NotificationIconAreaController -import com.android.systemui.statusbar.ui.SystemBarUtilsState import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch /** Binds a [NotificationShelf] to its [view model][NotificationShelfViewModel]. */ object NotificationShelfViewBinder { - fun bind( + suspend fun bind( shelf: NotificationShelf, viewModel: NotificationShelfViewModel, - configuration: ConfigurationState, - systemBarUtilsState: SystemBarUtilsState, falsingManager: FalsingManager, - iconViewBindingFailureTracker: StatusBarIconViewBindingFailureTracker, + nicBinder: NotificationIconContainerShelfViewBinder, notificationIconAreaController: NotificationIconAreaController, - shelfIconViewStore: ShelfNotificationIconViewStore, - ) { + ): Unit = coroutineScope { ActivatableNotificationViewBinder.bind(viewModel, shelf, falsingManager) shelf.apply { if (NotificationIconContainerRefactor.isEnabled) { - NotificationIconContainerViewBinder.bindWhileAttached( - shelfIcons, - viewModel.icons, - configuration, - systemBarUtilsState, - iconViewBindingFailureTracker, - shelfIconViewStore, - ) + launch { nicBinder.bind(shelfIcons) } } else { notificationIconAreaController.setShelfIcons(shelfIcons) } - repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - viewModel.canModifyColorOfNotifications.collect( - ::setCanModifyColorOfNotifications - ) - } - launch { viewModel.isClickable.collect(::setCanInteract) } - registerViewListenersWhileAttached(shelf, viewModel) - } + launch { + viewModel.canModifyColorOfNotifications.collect(::setCanModifyColorOfNotifications) } + launch { viewModel.isClickable.collect(::setCanInteract) } + registerViewListenersWhileAttached(shelf, viewModel) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt index 64b5b62c4331..5ca8b53d0704 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.shelf.ui.viewmodel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.NotificationShelf -import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModel import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor import javax.inject.Inject @@ -32,7 +31,6 @@ class NotificationShelfViewModel constructor( private val interactor: NotificationShelfInteractor, activatableViewModel: ActivatableNotificationViewModel, - val icons: NotificationIconContainerShelfViewModel, ) : ActivatableNotificationViewModel by activatableViewModel { /** Is the shelf allowed to be clickable when it has content? */ val isClickable: Flow<Boolean> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 3bbdfd164ba7..62fdc294e2d6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -94,9 +94,7 @@ import com.android.systemui.flags.RefactorFlag; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.res.R; -import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.TouchLogger; -import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.StatusBarState; @@ -109,12 +107,12 @@ import com.android.systemui.statusbar.notification.collection.render.GroupExpans import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor; import com.android.systemui.statusbar.notification.footer.ui.view.FooterView; -import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; +import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.phone.HeadsUpTouchHelper; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; @@ -146,8 +144,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private static final String TAG = "StackScroller"; private static final boolean SPEW = Log.isLoggable(TAG, Log.VERBOSE); - // Delay in milli-seconds before shade closes for clear all. - private static final int DELAY_BEFORE_SHADE_CLOSE = 200; private boolean mShadeNeedsToClose = false; @VisibleForTesting @@ -319,7 +315,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } }; private NotificationStackScrollLogger mLogger; - private NotificationsController mNotificationsController; + private Runnable mResetUserExpandedStatesRunnable; private ActivityStarter mActivityStarter; private final int[] mTempInt2 = new int[2]; private final HashSet<Runnable> mAnimationFinishedRunnables = new HashSet<>(); @@ -482,7 +478,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private final Rect mTmpRect = new Rect(); private ClearAllListener mClearAllListener; private ClearAllAnimationListener mClearAllAnimationListener; - private ShadeController mShadeController; + private Runnable mClearAllFinishedWhilePanelExpandedRunnable; private Consumer<Boolean> mOnStackYChanged; private Interpolator mHideXInterpolator = Interpolators.FAST_OUT_SLOW_IN; @@ -3328,8 +3324,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable logHunAnimationSkipped(row, "row has no viewState"); continue; } + boolean shouldHunAppearFromTheBottom = + mStackScrollAlgorithm.shouldHunAppearFromBottom(mAmbientState, viewState); if (isHeadsUp && (mAddedHeadsUpChildren.contains(row) || pinnedAndClosed)) { - if (pinnedAndClosed || shouldHunAppearFromBottom(viewState)) { + if (pinnedAndClosed || shouldHunAppearFromTheBottom) { // Our custom add animation type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR; } else { @@ -3341,6 +3339,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } AnimationEvent event = new AnimationEvent(row, type); event.headsUpFromBottom = onBottom; + if (NotificationsImprovedHunAnimation.isEnabled()) { + // TODO(b/283084712) remove this with the flag and update the HUN filters at + // creation + event.filter.animateHeight = false; + } mAnimationEvents.add(event); if (SPEW) { Log.v(TAG, "Generating HUN animation event: " @@ -3355,11 +3358,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mAddedHeadsUpChildren.clear(); } - private boolean shouldHunAppearFromBottom(ExpandableViewState viewState) { - return viewState.getYTranslation() + viewState.height - >= mAmbientState.getMaxHeadsUpTranslation(); - } - private void generateGroupExpansionEvent() { // Generate a group expansion/collapsing event if there is such a group at all if (mExpandedGroupView != null) { @@ -4114,7 +4112,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mAmbientState.setExpansionChanging(false); if (!mIsExpanded) { resetScrollPosition(); - mNotificationsController.resetUserExpandedStates(); + mResetUserExpandedStatesRunnable.run(); clearTemporaryViews(); clearUserLockedViews(); resetAllSwipeState(); @@ -4316,21 +4314,12 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (mShadeNeedsToClose) { mShadeNeedsToClose = false; if (mIsExpanded) { - collapseShadeDelayed(); + mClearAllFinishedWhilePanelExpandedRunnable.run(); } } } } - private void collapseShadeDelayed() { - postDelayed( - () -> { - mShadeController.animateCollapseShade( - CommandQueue.FLAG_EXCLUDE_NONE); - }, - DELAY_BEFORE_SHADE_CLOSE /* delayMillis */); - } - private void clearHeadsUpDisappearRunning() { for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); @@ -4747,8 +4736,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return max + getStackTranslation(); } - public void setNotificationsController(NotificationsController notificationsController) { - this.mNotificationsController = notificationsController; + public void setResetUserExpandedStatesRunnable(Runnable runnable) { + this.mResetUserExpandedStatesRunnable = runnable; } public void setActivityStarter(ActivityStarter activityStarter) { @@ -4932,7 +4921,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable */ public void setHeadsUpBoundaries(int height, int bottomBarHeight) { mAmbientState.setMaxHeadsUpTranslation(height - bottomBarHeight); + mStackScrollAlgorithm.setHeadsUpAppearHeightBottom(height); mStateAnimator.setHeadsUpAppearHeightBottom(height); + mStateAnimator.setStackTopMargin(mAmbientState.getStackTopMargin()); requestChildrenUpdate(); } @@ -5681,8 +5672,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mFooterClearAllListener = listener; } - void setShadeController(ShadeController shadeController) { - mShadeController = shadeController; + void setClearAllFinishedWhilePanelExpandedRunnable(Runnable runnable) { + mClearAllFinishedWhilePanelExpandedRunnable = runnable; } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index e6315fd159d5..7c7d9431e0fc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -81,6 +81,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeViewController; +import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationLockscreenUserManager.UserChangedListener; @@ -152,6 +153,8 @@ public class NotificationStackScrollLayoutController implements Dumpable { private static final String TAG = "StackScrollerController"; private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG); private static final String HIGH_PRIORITY = "high_priority"; + /** Delay in milli-seconds before shade closes for clear all. */ + private static final int DELAY_BEFORE_SHADE_CLOSE = 200; private final boolean mAllowLongPress; private final NotificationGutsManager mNotificationGutsManager; @@ -754,7 +757,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { mView.setController(this); mView.setLogger(mLogger); mView.setTouchHandler(new TouchHandler()); - mView.setNotificationsController(mNotificationsController); + mView.setResetUserExpandedStatesRunnable(mNotificationsController::resetUserExpandedStates); mView.setActivityStarter(mActivityStarter); mView.setClearAllAnimationListener(this::onAnimationEnd); mView.setClearAllListener((selection) -> mUiEventLogger.log( @@ -770,7 +773,11 @@ public class NotificationStackScrollLayoutController implements Dumpable { mView.setIsRemoteInputActive(active); } }); - mView.setShadeController(mShadeController); + mView.setClearAllFinishedWhilePanelExpandedRunnable(()-> { + final Runnable doCollapseRunnable = () -> + mShadeController.animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NONE); + mView.postDelayed(doCollapseRunnable, /* delayMillis = */ DELAY_BEFORE_SHADE_CLOSE); + }); mDumpManager.registerDumpable(mView); mKeyguardBypassController.registerOnBypassStateChangedListener( @@ -852,7 +859,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { mGroupExpansionManager.registerGroupExpansionChangeListener( (changedRow, expanded) -> mView.onGroupExpandChanged(changedRow, expanded)); - mViewBinder.bind(mView, this); + mViewBinder.bindWhileAttached(mView, this); if (!FooterViewRefactor.isEnabled()) { collectFlow(mView, mKeyguardTransitionRepo.getTransitions(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 06ca9a50bb6d..c4e6b909d023 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -37,6 +37,7 @@ import com.android.systemui.statusbar.notification.footer.ui.view.FooterView; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; +import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation; import java.util.ArrayList; import java.util.List; @@ -66,12 +67,15 @@ public class StackScrollAlgorithm { private boolean mClipNotificationScrollToTop; @VisibleForTesting float mHeadsUpInset; + @VisibleForTesting + float mHeadsUpAppearStartAboveScreen; private int mPinnedZTranslationExtra; private float mNotificationScrimPadding; private int mMarginBottom; private float mQuickQsOffsetHeight; private float mSmallCornerRadius; private float mLargeCornerRadius; + private int mHeadsUpAppearHeightBottom; public StackScrollAlgorithm( Context context, @@ -94,6 +98,8 @@ public class StackScrollAlgorithm { int statusBarHeight = SystemBarUtils.getStatusBarHeight(context); mHeadsUpInset = statusBarHeight + res.getDimensionPixelSize( R.dimen.heads_up_status_bar_padding); + mHeadsUpAppearStartAboveScreen = res.getDimensionPixelSize( + R.dimen.heads_up_appear_y_above_screen); mPinnedZTranslationExtra = res.getDimensionPixelSize( R.dimen.heads_up_pinned_elevation); mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height); @@ -221,6 +227,25 @@ public class StackScrollAlgorithm { return getExpansionFractionWithoutShelf(mTempAlgorithmState, ambientState); } + public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) { + mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom; + } + + /** + * If the QuickSettings is showing full screen, we want to animate the HeadsUp Notifications + * from the bottom of the screen. + * + * @param ambientState Current ambient state. + * @param viewState The state of the HUN that is being queried to appear from the bottom. + * + * @return true if the HeadsUp Notifications should appear from the bottom + */ + public boolean shouldHunAppearFromBottom(AmbientState ambientState, + ExpandableViewState viewState) { + return viewState.getYTranslation() + viewState.height + >= ambientState.getMaxHeadsUpTranslation(); + } + public static void log(String s) { if (DEBUG) { android.util.Log.i(TAG, s); @@ -793,10 +818,16 @@ public class StackScrollAlgorithm { } } if (row.isPinned()) { - // Make sure row yTranslation is at maximum the HUN yTranslation, - // which accounts for AmbientState.stackTopMargin in split-shade. - childState.setYTranslation( - Math.max(childState.getYTranslation(), headsUpTranslation)); + if (NotificationsImprovedHunAnimation.isEnabled()) { + // Make sure row yTranslation is at the HUN yTranslation, + // which accounts for AmbientState.stackTopMargin in split-shade. + childState.setYTranslation(headsUpTranslation); + } else { + // Make sure row yTranslation is at maximum the HUN yTranslation, + // which accounts for AmbientState.stackTopMargin in split-shade. + childState.setYTranslation( + Math.max(childState.getYTranslation(), headsUpTranslation)); + } childState.height = Math.max(row.getIntrinsicHeight(), childState.height); childState.hidden = false; ExpandableViewState topState = @@ -819,10 +850,22 @@ public class StackScrollAlgorithm { } } if (row.isHeadsUpAnimatingAway()) { - // Make sure row yTranslation is at maximum the HUN yTranslation, - // which accounts for AmbientState.stackTopMargin in split-shade. - childState.setYTranslation( - Math.max(childState.getYTranslation(), headsUpTranslation)); + if (NotificationsImprovedHunAnimation.isEnabled()) { + if (shouldHunAppearFromBottom(ambientState, childState)) { + // move to the bottom of the screen + childState.setYTranslation( + mHeadsUpAppearHeightBottom + mHeadsUpAppearStartAboveScreen); + } else { + // move to the top of the screen + childState.setYTranslation(-ambientState.getStackTopMargin() + - mHeadsUpAppearStartAboveScreen); + } + } else { + // Make sure row yTranslation is at maximum the HUN yTranslation, + // which accounts for AmbientState.stackTopMargin in split-shade. + childState.setYTranslation( + Math.max(childState.getYTranslation(), headsUpTranslation)); + } // keep it visible for the animation childState.hidden = false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java index e94258f416ac..a3e09417b34c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.stack; +import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK; @@ -26,6 +27,7 @@ import android.util.Property; import android.view.View; import com.android.app.animation.Interpolators; +import com.android.internal.annotations.VisibleForTesting; import com.android.keyguard.KeyguardSliceView; import com.android.systemui.res.R; import com.android.systemui.shared.clocks.AnimatableClockView; @@ -33,6 +35,7 @@ import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; +import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation; import java.util.ArrayList; import java.util.HashSet; @@ -68,6 +71,8 @@ public class StackStateAnimator { private final int mGoToFullShadeAppearingTranslation; private final int mPulsingAppearingTranslation; + @VisibleForTesting + float mHeadsUpAppearStartAboveScreen; private final ExpandableViewState mTmpState = new ExpandableViewState(); private final AnimationProperties mAnimationProperties; public NotificationStackScrollLayout mHostLayout; @@ -85,21 +90,23 @@ public class StackStateAnimator { private ValueAnimator mTopOverScrollAnimator; private ValueAnimator mBottomOverScrollAnimator; private int mHeadsUpAppearHeightBottom; + private int mStackTopMargin; private boolean mShadeExpanded; private ArrayList<ExpandableView> mTransientViewsToRemove = new ArrayList<>(); private NotificationShelf mShelf; - private float mStatusBarIconLocation; - private int[] mTmpLocation = new int[2]; private StackStateLogger mLogger; public StackStateAnimator(NotificationStackScrollLayout hostLayout) { mHostLayout = hostLayout; + // TODO(b/317061579) reload on configuration changes mGoToFullShadeAppearingTranslation = hostLayout.getContext().getResources().getDimensionPixelSize( R.dimen.go_to_full_shade_appearing_translation); mPulsingAppearingTranslation = hostLayout.getContext().getResources().getDimensionPixelSize( R.dimen.pulsing_notification_appear_translation); + mHeadsUpAppearStartAboveScreen = hostLayout.getContext().getResources() + .getDimensionPixelSize(R.dimen.heads_up_appear_y_above_screen); mAnimationProperties = new AnimationProperties() { @Override public AnimationFilter getAnimationFilter() { @@ -455,8 +462,37 @@ public class StackStateAnimator { .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) { ExpandableNotificationRow row = (ExpandableNotificationRow) event.mChangingView; row.prepareExpansionChanged(); - } else if (event.animationType == NotificationStackScrollLayout - .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) { + } else if (NotificationsImprovedHunAnimation.isEnabled() + && (event.animationType == ANIMATION_TYPE_HEADS_UP_APPEAR)) { + mHeadsUpAppearChildren.add(changingView); + + mTmpState.copyFrom(changingView.getViewState()); + if (event.headsUpFromBottom) { + // start from the bottom of the screen + mTmpState.setYTranslation( + mHeadsUpAppearHeightBottom + mHeadsUpAppearStartAboveScreen); + } else { + // start from the top of the screen + mTmpState.setYTranslation( + -mStackTopMargin - mHeadsUpAppearStartAboveScreen); + } + // set the height and the initial position + mTmpState.applyToView(changingView); + mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y, + Interpolators.FAST_OUT_SLOW_IN); + + Runnable onAnimationEnd = null; + if (loggable) { + // This only captures HEADS_UP_APPEAR animations, but HUNs can appear with + // normal ADD animations, which would not be logged here. + String finalKey = key; + mLogger.logHUNViewAppearing(key); + onAnimationEnd = () -> mLogger.appearAnimationEnded(finalKey); + } + changingView.performAddAnimation(0, ANIMATION_DURATION_HEADS_UP_APPEAR, + /* isHeadsUpAppear= */ true, onAnimationEnd); + } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_APPEAR) { + NotificationsImprovedHunAnimation.assertInLegacyMode(); // This item is added, initialize its properties. ExpandableViewState viewState = changingView.getViewState(); mTmpState.copyFrom(viewState); @@ -536,6 +572,10 @@ public class StackStateAnimator { changingView.setInRemovalAnimation(true); }; } + if (NotificationsImprovedHunAnimation.isEnabled()) { + mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y, + Interpolators.FAST_OUT_SLOW_IN_REVERSE); + } long removeAnimationDelay = changingView.performRemoveAnimation( ANIMATION_DURATION_HEADS_UP_DISAPPEAR, 0, 0.0f, true /* isHeadsUpAppear */, @@ -601,6 +641,10 @@ public class StackStateAnimator { mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom; } + public void setStackTopMargin(int stackTopMargin) { + mStackTopMargin = stackTopMargin; + } + public void setShadeExpanded(boolean shadeExpanded) { mShadeExpanded = shadeExpanded; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/DisplaySwitchNotificationsHiderFlag.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/DisplaySwitchNotificationsHiderFlag.kt index 98c173402109..3cd9a8f598a4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/DisplaySwitchNotificationsHiderFlag.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/DisplaySwitchNotificationsHiderFlag.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.stack.shared import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils /** Helper for reading or using the DisplaySwitchNotificationsHider flag state. */ @@ -24,6 +25,10 @@ import com.android.systemui.flags.RefactorFlagUtils object DisplaySwitchNotificationsHiderFlag { const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_HIDE_ON_DISPLAY_SWITCH + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + /** Is the hiding enabled? */ @JvmStatic inline val isEnabled diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt index 274bf94566cc..910b40f594f6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt @@ -16,29 +16,22 @@ package com.android.systemui.statusbar.notification.stack.ui.viewbinder import androidx.core.view.doOnDetach -import androidx.lifecycle.lifecycleScope -import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel -import kotlinx.coroutines.launch /** * Binds a [NotificationStackScrollLayoutController] to its [view model][NotificationListViewModel]. */ object HideNotificationsBinder { - fun bindHideList( + suspend fun bindHideList( viewController: NotificationStackScrollLayoutController, viewModel: NotificationListViewModel ) { - viewController.view.repeatWhenAttached { - lifecycleScope.launch { - viewModel.hideListViewModel.shouldHideListForPerformance.collect { shouldHide -> - viewController.bindHideState(shouldHide) - } - } - } - viewController.view.doOnDetach { viewController.bindHideState(shouldHide = false) } + + viewModel.hideListViewModel.shouldHideListForPerformance.collect { shouldHide -> + viewController.bindHideState(shouldHide) + } } private fun NotificationStackScrollLayoutController.bindHideState(shouldHide: Boolean) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt index 9373d497ffa7..1b3666078b27 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt @@ -32,15 +32,14 @@ import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.footer.ui.view.FooterView import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel import com.android.systemui.statusbar.phone.NotificationIconAreaController -import com.android.systemui.statusbar.ui.SystemBarUtilsState +import com.android.systemui.util.kotlin.getOrNull import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.combine @@ -55,25 +54,27 @@ constructor( private val configuration: ConfigurationState, private val falsingManager: FalsingManager, private val iconAreaController: NotificationIconAreaController, - private val iconViewBindingFailureTracker: StatusBarIconViewBindingFailureTracker, private val metricsLogger: MetricsLogger, - private val shelfIconViewStore: ShelfNotificationIconViewStore, - private val systemBarUtilsState: SystemBarUtilsState, + private val nicBinder: NotificationIconContainerShelfViewBinder, ) { - fun bind( + fun bindWhileAttached( view: NotificationStackScrollLayout, viewController: NotificationStackScrollLayoutController ) { - bindShelf(view) - bindHideList(viewController, viewModel) + val shelf = + LayoutInflater.from(view.context) + .inflate(R.layout.status_bar_notification_shelf, view, false) as NotificationShelf + view.setShelf(shelf) - if (FooterViewRefactor.isEnabled) { - bindFooter(view) - bindEmptyShade(view) + view.repeatWhenAttached { + lifecycleScope.launch { + launch { bindShelf(shelf) } + launch { bindHideList(viewController, viewModel) } - view.repeatWhenAttached { - lifecycleScope.launch { + if (FooterViewRefactor.isEnabled) { + launch { bindFooter(view) } + launch { bindEmptyShade(view) } viewModel.isImportantForAccessibility.collect { isImportantForAccessibility -> view.setImportantForAccessibilityYesNo(isImportantForAccessibility) } @@ -82,73 +83,57 @@ constructor( } } - private fun bindShelf(parentView: NotificationStackScrollLayout) { - val shelf = - LayoutInflater.from(parentView.context) - .inflate(R.layout.status_bar_notification_shelf, parentView, false) - as NotificationShelf + private suspend fun bindShelf(shelf: NotificationShelf) { NotificationShelfViewBinder.bind( shelf, viewModel.shelf, - configuration, - systemBarUtilsState, falsingManager, - iconViewBindingFailureTracker, + nicBinder, iconAreaController, - shelfIconViewStore, ) - parentView.setShelf(shelf) } - private fun bindFooter(parentView: NotificationStackScrollLayout) { - viewModel.footer.ifPresent { footerViewModel -> + private suspend fun bindFooter(parentView: NotificationStackScrollLayout) { + viewModel.footer.getOrNull()?.let { footerViewModel -> // The footer needs to be re-inflated every time the theme or the font size changes. - parentView.repeatWhenAttached { - configuration.reinflateAndBindLatest( - R.layout.status_bar_notification_footer, - parentView, - attachToRoot = false, - backgroundDispatcher, - ) { footerView: FooterView -> - traceSection("bind FooterView") { - val disposableHandle = - FooterViewBinder.bind( - footerView, - footerViewModel, - clearAllNotifications = { - metricsLogger.action( - MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES - ) - parentView.clearAllNotifications() - }, - ) - parentView.setFooterView(footerView) - return@reinflateAndBindLatest disposableHandle - } + configuration.reinflateAndBindLatest( + R.layout.status_bar_notification_footer, + parentView, + attachToRoot = false, + backgroundDispatcher, + ) { footerView: FooterView -> + traceSection("bind FooterView") { + val disposableHandle = + FooterViewBinder.bind( + footerView, + footerViewModel, + clearAllNotifications = { + metricsLogger.action( + MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES + ) + parentView.clearAllNotifications() + }, + ) + parentView.setFooterView(footerView) + return@reinflateAndBindLatest disposableHandle } } } } - private fun bindEmptyShade( - parentView: NotificationStackScrollLayout, - ) { - parentView.repeatWhenAttached { - lifecycleScope.launch { - combine( - viewModel.shouldShowEmptyShadeView, - viewModel.areNotificationsHiddenInShade, - viewModel.hasFilteredOutSeenNotifications, - ::Triple - ) - .collect { (shouldShow, areNotifsHidden, hasFilteredNotifs) -> - parentView.updateEmptyShadeView( - shouldShow, - areNotifsHidden, - hasFilteredNotifs, - ) - } + private suspend fun bindEmptyShade(parentView: NotificationStackScrollLayout) { + combine( + viewModel.shouldShowEmptyShadeView, + viewModel.areNotificationsHiddenInShade, + viewModel.hasFilteredOutSeenNotifications, + ::Triple + ) + .collect { (shouldShow, areNotifsHidden, hasFilteredNotifs) -> + parentView.updateEmptyShadeView( + shouldShow, + areNotifsHidden, + hasFilteredNotifs, + ) } - } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index 145dbff81144..7cc08887ae52 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -196,7 +196,7 @@ public class KeyguardStatusBarView extends RelativeLayout { } private void updateKeyguardStatusBarHeight() { - MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams(); + ViewGroup.LayoutParams lp = (ViewGroup.LayoutParams) getLayoutParams(); lp.height = getStatusBarHeaderHeightKeyguard(mContext); setLayoutParams(lp); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java index a62a1ed9f0c0..e79f3ff19031 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java @@ -608,7 +608,7 @@ public class LegacyNotificationIconAreaControllerImpl implements } @Override - public void onPulseExpansionChanged(boolean expandingChanged) { + public void onPulseExpansionAmountChanged(boolean expandingChanged) { if (expandingChanged) { updateAodIconsVisibility(true /* animate */, false /* force */); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index 0dabafbdecb0..f34a44a5c4b0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -284,11 +284,22 @@ public class NotificationIconContainer extends ViewGroup { @Override public String toString() { - return "NotificationIconContainer(" - + "dozing=" + mDozing + " onLockScreen=" + mOnLockScreen - + " overrideIconColor=" + mOverrideIconColor - + " speedBumpIndex=" + mSpeedBumpIndex - + " themedTextColorPrimary=#" + Integer.toHexString(mThemedTextColorPrimary) + ')'; + if (NotificationIconContainerRefactor.isEnabled()) { + return super.toString() + + " {" + + " overrideIconColor=" + mOverrideIconColor + + ", maxIcons=" + mMaxIcons + + ", isStaticLayout=" + mIsStaticLayout + + ", themedTextColorPrimary=#" + Integer.toHexString(mThemedTextColorPrimary) + + " }"; + } else { + return "NotificationIconContainer(" + + "dozing=" + mDozing + " onLockScreen=" + mOnLockScreen + + " overrideIconColor=" + mOverrideIconColor + + " speedBumpIndex=" + mSpeedBumpIndex + + " themedTextColorPrimary=#" + Integer.toHexString(mThemedTextColorPrimary) + + ')'; + } } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java index cd999349d055..2740cc6682a6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java @@ -38,7 +38,6 @@ import com.android.app.animation.Interpolators; import com.android.app.animation.InterpolatorsAndroidX; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.Dumpable; -import com.android.systemui.common.ui.ConfigurationState; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoModeController; @@ -54,10 +53,7 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.disableflags.DisableFlagsLogger.DisableState; import com.android.systemui.statusbar.events.SystemStatusAnimationCallback; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder; -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker; -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarNotificationIconViewStore; -import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel; +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder; import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; import com.android.systemui.statusbar.phone.NotificationIconAreaController; import com.android.systemui.statusbar.phone.NotificationIconContainer; @@ -75,7 +71,6 @@ import com.android.systemui.statusbar.pipeline.shared.ui.binder.CollapsedStatusB import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarVisibilityChangeListener; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModel; import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.statusbar.ui.SystemBarUtilsState; import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.statusbar.window.StatusBarWindowStateListener; import com.android.systemui.util.CarrierConfigTracker; @@ -95,6 +90,8 @@ import javax.inject.Inject; import kotlin.Unit; +import kotlinx.coroutines.DisposableHandle; + /** * Contains the collapsed status bar and handles hiding/showing based on disable flags * and keyguard state. Also manages lifecycle to make sure the views it contains are being @@ -151,10 +148,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private final DumpManager mDumpManager; private final StatusBarWindowStateController mStatusBarWindowStateController; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; - private final NotificationIconContainerStatusBarViewModel mStatusBarIconsViewModel; - private final ConfigurationState mConfigurationState; - private final SystemBarUtilsState mSystemBarUtilsState; - private final StatusBarNotificationIconViewStore mStatusBarIconViewStore; + private final NotificationIconContainerStatusBarViewBinder mNicViewBinder; private final DemoModeController mDemoModeController; private List<String> mBlockedIcons = new ArrayList<>(); @@ -216,7 +210,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mWaitingForWindowStateChangeAfterCameraLaunch = false; mTransitionFromLockscreenToDreamStarted = false; }; - private final StatusBarIconViewBindingFailureTracker mIconViewBindingFailureTracker; + private DisposableHandle mNicBindingDisposable; @Inject public CollapsedStatusBarFragment( @@ -234,7 +228,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue KeyguardStateController keyguardStateController, ShadeViewController shadeViewController, StatusBarStateController statusBarStateController, - StatusBarIconViewBindingFailureTracker iconViewBindingFailureTracker, + NotificationIconContainerStatusBarViewBinder nicViewBinder, CommandQueue commandQueue, CarrierConfigTracker carrierConfigTracker, CollapsedStatusBarFragmentLogger collapsedStatusBarFragmentLogger, @@ -244,10 +238,6 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue DumpManager dumpManager, StatusBarWindowStateController statusBarWindowStateController, KeyguardUpdateMonitor keyguardUpdateMonitor, - NotificationIconContainerStatusBarViewModel statusBarIconsViewModel, - ConfigurationState configurationState, - SystemBarUtilsState systemBarUtilsState, - StatusBarNotificationIconViewStore statusBarIconViewStore, DemoModeController demoModeController) { mStatusBarFragmentComponentFactory = statusBarFragmentComponentFactory; mOngoingCallController = ongoingCallController; @@ -263,7 +253,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mKeyguardStateController = keyguardStateController; mShadeViewController = shadeViewController; mStatusBarStateController = statusBarStateController; - mIconViewBindingFailureTracker = iconViewBindingFailureTracker; + mNicViewBinder = nicViewBinder; mCommandQueue = commandQueue; mCarrierConfigTracker = carrierConfigTracker; mCollapsedStatusBarFragmentLogger = collapsedStatusBarFragmentLogger; @@ -273,10 +263,6 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mDumpManager = dumpManager; mStatusBarWindowStateController = statusBarWindowStateController; mKeyguardUpdateMonitor = keyguardUpdateMonitor; - mStatusBarIconsViewModel = statusBarIconsViewModel; - mConfigurationState = configurationState; - mSystemBarUtilsState = systemBarUtilsState; - mStatusBarIconViewStore = statusBarIconViewStore; mDemoModeController = demoModeController; } @@ -455,6 +441,12 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mStartableStates.put(startable, Startable.State.STOPPED); } mDumpManager.unregisterDumpable(getClass().getSimpleName()); + if (NotificationIconContainerRefactor.isEnabled()) { + if (mNicBindingDisposable != null) { + mNicBindingDisposable.dispose(); + mNicBindingDisposable = null; + } + } } /** Initializes views related to the notification icon area. */ @@ -466,13 +458,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue .inflate(R.layout.notification_icon_area, notificationIconArea, true); NotificationIconContainer notificationIcons = notificationIconArea.requireViewById(R.id.notificationIcons); - NotificationIconContainerViewBinder.bindWhileAttached( - notificationIcons, - mStatusBarIconsViewModel, - mConfigurationState, - mSystemBarUtilsState, - mIconViewBindingFailureTracker, - mStatusBarIconViewStore); + mNicBindingDisposable = mNicViewBinder.bindWhileAttached(notificationIcons); } else { mNotificationIconAreaInner = mNotificationIconAreaController.getNotificationInnerAreaView(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index 4864fb8ca634..5bced934be7a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -303,8 +303,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mEntry.mRemoteEditImeVisible = editTextRootWindowInsets != null && editTextRootWindowInsets.isVisible(WindowInsets.Type.ime()); if (!mEntry.mRemoteEditImeVisible && !mEditText.mShowImeOnInputConnection) { - // Pass null to ensure all inputs are cleared for this entry b/227115380 - mController.removeRemoteInput(mEntry, null, + mController.removeRemoteInput(mEntry, mToken, /* reason= */"RemoteInputView$WindowInsetAnimation#onEnd"); } } @@ -536,6 +535,11 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene if (mEntry.getRow().isChangingPosition() || isTemporarilyDetached()) { return; } + // RemoteInputView can be detached from window before IME close event in some cases like + // remote input view removal with notification update. As a result of this, RemoteInputView + // will stop ime animation updates, which results in never removing remote input. That's why + // we have to set mRemoteEditImeAnimatingAway false on detach to remove remote input. + mEntry.mRemoteEditImeAnimatingAway = false; mController.removeRemoteInput(mEntry, mToken, /* reason= */"RemoteInputView#onDetachedFromWindow"); mController.removeSpinning(mEntry.getKey(), mToken); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java index fa0cb5c939ab..66bf527f5047 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.policy; import android.app.AlarmManager; +import android.app.Flags; import android.app.NotificationManager; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -191,7 +192,11 @@ public class ZenModeControllerImpl implements ZenModeController, Dumpable { @Override public void setZen(int zen, Uri conditionId, String reason) { - mNoMan.setZenMode(zen, conditionId, reason); + if (Flags.modesApi()) { + mNoMan.setZenMode(zen, conditionId, reason, /* fromUser= */ true); + } else { + mNoMan.setZenMode(zen, conditionId, reason); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt index e0d205fc4b6a..c170eb567344 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt @@ -37,6 +37,7 @@ import com.android.internal.logging.UiEventLogger import com.android.internal.util.UserIcons import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.systemui.Flags.switchUserOnBg import com.android.systemui.SystemUISecondaryUserService import com.android.systemui.animation.Expandable import com.android.systemui.broadcast.BroadcastDispatcher @@ -44,6 +45,7 @@ import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -100,6 +102,7 @@ constructor( broadcastDispatcher: BroadcastDispatcher, keyguardUpdateMonitor: KeyguardUpdateMonitor, @Background private val backgroundDispatcher: CoroutineDispatcher, + @Main private val mainDispatcher: CoroutineDispatcher, private val activityManager: ActivityManager, private val refreshUsersScheduler: RefreshUsersScheduler, private val guestUserInteractor: GuestUserInteractor, @@ -339,7 +342,11 @@ constructor( } .launchIn(applicationScope) restartSecondaryService(repository.getSelectedUserInfo().id) - keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) + applicationScope.launch { + withContext(mainDispatcher) { + keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) + } + } } fun addCallback(callback: UserCallback) { @@ -593,10 +600,18 @@ constructor( private fun switchUser(userId: Int) { // TODO(b/246631653): track jank and latency like in the old impl. refreshUsersScheduler.pause() - try { - activityManager.switchUser(userId) - } catch (e: RemoteException) { - Log.e(TAG, "Couldn't switch user.", e) + val runnable = Runnable { + try { + activityManager.switchUser(userId) + } catch (e: RemoteException) { + Log.e(TAG, "Couldn't switch user.", e) + } + } + + if (switchUserOnBg()) { + applicationScope.launch { withContext(backgroundDispatcher) { runnable.run() } } + } else { + runnable.run() } } diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java b/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java deleted file mode 100644 index 82153600e473..000000000000 --- a/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright (C) 2017 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.systemui.util.leak; - -import android.content.ClipData; -import android.content.ClipDescription; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Build; -import android.util.Log; - -import androidx.core.content.FileProvider; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -/** - * Utility class for dumping, compressing, sending, and serving heap dump files. - * - * <p>Unlike the Internet, this IS a big truck you can dump something on. - */ -public class DumpTruck { - private static final String FILEPROVIDER_AUTHORITY = "com.android.systemui.fileprovider"; - private static final String FILEPROVIDER_PATH = "leak"; - - private static final String TAG = "DumpTruck"; - private static final int BUFSIZ = 1024 * 1024; // 1MB - - private final Context context; - private final GarbageMonitor mGarbageMonitor; - private Uri hprofUri; - private long rss; - final StringBuilder body = new StringBuilder(); - - public DumpTruck(Context context, GarbageMonitor garbageMonitor) { - this.context = context; - mGarbageMonitor = garbageMonitor; - } - - /** - * Capture memory for the given processes and zip them up for sharing. - * - * @param pids - * @return this, for chaining - */ - public DumpTruck captureHeaps(List<Long> pids) { - final File dumpDir = new File(context.getCacheDir(), FILEPROVIDER_PATH); - dumpDir.mkdirs(); - hprofUri = null; - - body.setLength(0); - body.append("Build: ").append(Build.DISPLAY).append("\n\nProcesses:\n"); - - final ArrayList<String> paths = new ArrayList<String>(); - final int myPid = android.os.Process.myPid(); - - for (Long pidL : pids) { - final int pid = pidL.intValue(); - body.append(" pid ").append(pid); - GarbageMonitor.ProcessMemInfo info = mGarbageMonitor.getMemInfo(pid); - if (info != null) { - body.append(":") - .append(" up=") - .append(info.getUptime()) - .append(" rss=") - .append(info.currentRss); - rss = info.currentRss; - } - if (pid == myPid) { - final String path = - new File(dumpDir, String.format("heap-%d.ahprof", pid)).getPath(); - Log.v(TAG, "Dumping memory info for process " + pid + " to " + path); - try { - android.os.Debug.dumpHprofData(path); // will block - paths.add(path); - body.append(" (hprof attached)"); - } catch (IOException e) { - Log.e(TAG, "error dumping memory:", e); - body.append("\n** Could not dump heap: \n").append(e.toString()).append("\n"); - } - } - body.append("\n"); - } - - try { - final String zipfile = - new File(dumpDir, String.format("hprof-%d.zip", System.currentTimeMillis())) - .getCanonicalPath(); - if (DumpTruck.zipUp(zipfile, paths)) { - final File pathFile = new File(zipfile); - hprofUri = FileProvider.getUriForFile(context, FILEPROVIDER_AUTHORITY, pathFile); - Log.v(TAG, "Heap dump accessible at URI: " + hprofUri); - } - } catch (IOException e) { - Log.e(TAG, "unable to zip up heapdumps", e); - body.append("\n** Could not zip up files: \n").append(e.toString()).append("\n"); - } - - return this; - } - - /** - * Get the Uri of the current heap dump. Be sure to call captureHeaps first. - * - * @return Uri to the dump served by the SystemUI file provider - */ - public Uri getDumpUri() { - return hprofUri; - } - - /** - * Get an ACTION_SEND intent suitable for startActivity() or attaching to a Notification. - * - * @return share intent - */ - public Intent createShareIntent() { - Intent shareIntent = new Intent(Intent.ACTION_SEND_MULTIPLE); - shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - shareIntent.putExtra(Intent.EXTRA_SUBJECT, - String.format("SystemUI memory dump (rss=%dM)", rss / 1024)); - - shareIntent.putExtra(Intent.EXTRA_TEXT, body.toString()); - - if (hprofUri != null) { - final ArrayList<Uri> uriList = new ArrayList<>(); - uriList.add(hprofUri); - shareIntent.setType("application/zip"); - shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList); - - // Include URI in ClipData also, so that grantPermission picks it up. - // We don't use setData here because some apps interpret this as "to:". - ClipData clipdata = new ClipData(new ClipDescription("content", - new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}), - new ClipData.Item(hprofUri)); - shareIntent.setClipData(clipdata); - shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - return shareIntent; - } - - private static boolean zipUp(String zipfilePath, ArrayList<String> paths) { - try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipfilePath))) { - final byte[] buf = new byte[BUFSIZ]; - - for (String filename : paths) { - try (InputStream is = new BufferedInputStream(new FileInputStream(filename))) { - ZipEntry entry = new ZipEntry(filename); - zos.putNextEntry(entry); - int len; - while (0 < (len = is.read(buf, 0, BUFSIZ))) { - zos.write(buf, 0, len); - } - zos.closeEntry(); - } - } - return true; - } catch (IOException e) { - Log.e(TAG, "error zipping up profile data", e); - } - return false; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java deleted file mode 100644 index de392d3f444f..000000000000 --- a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java +++ /dev/null @@ -1,617 +0,0 @@ -/* - * Copyright (C) 2017 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.systemui.util.leak; - -import static android.service.quicksettings.Tile.STATE_ACTIVE; -import static android.telephony.ims.feature.ImsFeature.STATE_UNAVAILABLE; - -import static com.android.internal.logging.MetricsLogger.VIEW_UNKNOWN; - -import android.annotation.Nullable; -import android.app.ActivityManager; -import android.content.Context; -import android.content.Intent; -import android.content.res.ColorStateList; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.ColorFilter; -import android.graphics.Paint; -import android.graphics.PixelFormat; -import android.graphics.PorterDuff; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.os.Process; -import android.os.SystemProperties; -import android.provider.Settings; -import android.text.format.DateUtils; -import android.util.Log; -import android.util.LongSparseArray; -import android.view.View; - -import com.android.internal.logging.MetricsLogger; -import com.android.systemui.CoreStartable; -import com.android.systemui.Dumpable; -import com.android.systemui.res.R; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.plugins.qs.QSTile; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.qs.QSHost; -import com.android.systemui.qs.QsEventLogger; -import com.android.systemui.qs.logging.QSLogger; -import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; -import com.android.systemui.qs.tileimpl.QSTileImpl; -import com.android.systemui.util.concurrency.DelayableExecutor; -import com.android.systemui.util.concurrency.MessageRouter; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; - -import javax.inject.Inject; - -/** - * Suite of tools to periodically inspect the System UI heap and possibly prompt the user to - * capture heap dumps and report them. Includes the implementation of the "Dump SysUI Heap" - * quick settings tile. - */ -@SysUISingleton -public class GarbageMonitor implements Dumpable { - // Feature switches - // ================ - - // Whether to use TrackedGarbage to trigger LeakReporter. Off by default unless you set the - // appropriate sysprop on a userdebug device. - public static final boolean LEAK_REPORTING_ENABLED = Build.IS_DEBUGGABLE - && SystemProperties.getBoolean("debug.enable_leak_reporting", false); - public static final String FORCE_ENABLE_LEAK_REPORTING = "sysui_force_enable_leak_reporting"; - - // Heap tracking: watch the current memory levels and update the MemoryTile if available. - // On for all userdebug devices. - public static final boolean HEAP_TRACKING_ENABLED = Build.IS_DEBUGGABLE; - - // Tell QSTileHost.java to toss this into the default tileset? - public static final boolean ADD_MEMORY_TILE_TO_DEFAULT_ON_DEBUGGABLE_BUILDS = true; - - // whether to use ActivityManager.setHeapLimit (and post a notification to the user asking - // to dump the heap). Off by default unless you set the appropriate sysprop on userdebug - private static final boolean ENABLE_AM_HEAP_LIMIT = Build.IS_DEBUGGABLE - && SystemProperties.getBoolean("debug.enable_sysui_heap_limit", false); - - // Tuning params - // ============= - - // threshold for setHeapLimit(), in KB (overrides R.integer.watch_heap_limit) - private static final String SETTINGS_KEY_AM_HEAP_LIMIT = "systemui_am_heap_limit"; - - private static final long GARBAGE_INSPECTION_INTERVAL = - 15 * DateUtils.MINUTE_IN_MILLIS; // 15 min - private static final long HEAP_TRACK_INTERVAL = 1 * DateUtils.MINUTE_IN_MILLIS; // 1 min - private static final int HEAP_TRACK_HISTORY_LEN = 720; // 12 hours - - private static final int DO_GARBAGE_INSPECTION = 1000; - private static final int DO_HEAP_TRACK = 3000; - - static final int GARBAGE_ALLOWANCE = 5; - - private static final String TAG = "GarbageMonitor"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - - private final MessageRouter mMessageRouter; - private final TrackedGarbage mTrackedGarbage; - private final LeakReporter mLeakReporter; - private final Context mContext; - private final DelayableExecutor mDelayableExecutor; - private MemoryTile mQSTile; - private final DumpTruck mDumpTruck; - - private final LongSparseArray<ProcessMemInfo> mData = new LongSparseArray<>(); - private final ArrayList<Long> mPids = new ArrayList<>(); - - private long mHeapLimit; - - /** - */ - @Inject - public GarbageMonitor( - Context context, - @Background DelayableExecutor delayableExecutor, - @Background MessageRouter messageRouter, - LeakDetector leakDetector, - LeakReporter leakReporter, - DumpManager dumpManager) { - mContext = context.getApplicationContext(); - - mDelayableExecutor = delayableExecutor; - mMessageRouter = messageRouter; - mMessageRouter.subscribeTo(DO_GARBAGE_INSPECTION, this::doGarbageInspection); - mMessageRouter.subscribeTo(DO_HEAP_TRACK, this::doHeapTrack); - - mTrackedGarbage = leakDetector.getTrackedGarbage(); - mLeakReporter = leakReporter; - - mDumpTruck = new DumpTruck(mContext, this); - - dumpManager.registerDumpable(getClass().getSimpleName(), this); - - if (ENABLE_AM_HEAP_LIMIT) { - mHeapLimit = Settings.Global.getInt(context.getContentResolver(), - SETTINGS_KEY_AM_HEAP_LIMIT, - mContext.getResources().getInteger(R.integer.watch_heap_limit)); - } - } - - public void startLeakMonitor() { - if (mTrackedGarbage == null) { - return; - } - - mMessageRouter.sendMessage(DO_GARBAGE_INSPECTION); - } - - public void startHeapTracking() { - startTrackingProcess( - android.os.Process.myPid(), mContext.getPackageName(), System.currentTimeMillis()); - mMessageRouter.sendMessage(DO_HEAP_TRACK); - } - - private boolean gcAndCheckGarbage() { - if (mTrackedGarbage.countOldGarbage() > GARBAGE_ALLOWANCE) { - Runtime.getRuntime().gc(); - return true; - } - return false; - } - - void reinspectGarbageAfterGc() { - int count = mTrackedGarbage.countOldGarbage(); - if (count > GARBAGE_ALLOWANCE) { - mLeakReporter.dumpLeak(count); - } - } - - public ProcessMemInfo getMemInfo(int pid) { - return mData.get(pid); - } - - public List<Long> getTrackedProcesses() { - return mPids; - } - - public void startTrackingProcess(long pid, String name, long start) { - synchronized (mPids) { - if (mPids.contains(pid)) return; - - mPids.add(pid); - logPids(); - - mData.put(pid, new ProcessMemInfo(pid, name, start)); - } - } - - private void logPids() { - if (DEBUG) { - StringBuffer sb = new StringBuffer("Now tracking processes: "); - for (int i = 0; i < mPids.size(); i++) { - final int p = mPids.get(i).intValue(); - sb.append(" "); - } - Log.v(TAG, sb.toString()); - } - } - - private void update() { - synchronized (mPids) { - for (int i = 0; i < mPids.size(); i++) { - final int pid = mPids.get(i).intValue(); - // rssValues contains [VmRSS, RssFile, RssAnon, VmSwap]. - long[] rssValues = Process.getRss(pid); - if (rssValues == null && rssValues.length == 0) { - if (DEBUG) Log.e(TAG, "update: Process.getRss() didn't provide any values."); - break; - } - long rss = rssValues[0]; - final ProcessMemInfo info = mData.get(pid); - info.rss[info.head] = info.currentRss = rss; - info.head = (info.head + 1) % info.rss.length; - if (info.currentRss > info.max) info.max = info.currentRss; - if (info.currentRss == 0) { - if (DEBUG) Log.v(TAG, "update: pid " + pid + " has rss=0, it probably died"); - mData.remove(pid); - } - } - for (int i = mPids.size() - 1; i >= 0; i--) { - final long pid = mPids.get(i).intValue(); - if (mData.get(pid) == null) { - mPids.remove(i); - logPids(); - } - } - } - if (mQSTile != null) mQSTile.update(); - } - - private void setTile(MemoryTile tile) { - mQSTile = tile; - if (tile != null) tile.update(); - } - - private static String formatBytes(long b) { - String[] SUFFIXES = {"B", "K", "M", "G", "T"}; - int i; - for (i = 0; i < SUFFIXES.length; i++) { - if (b < 1024) break; - b /= 1024; - } - return b + SUFFIXES[i]; - } - - private Intent dumpHprofAndGetShareIntent() { - return mDumpTruck.captureHeaps(getTrackedProcesses()).createShareIntent(); - } - - @Override - public void dump(PrintWriter pw, @Nullable String[] args) { - pw.println("GarbageMonitor params:"); - pw.println(String.format(" mHeapLimit=%d KB", mHeapLimit)); - pw.println(String.format(" GARBAGE_INSPECTION_INTERVAL=%d (%.1f mins)", - GARBAGE_INSPECTION_INTERVAL, - (float) GARBAGE_INSPECTION_INTERVAL / DateUtils.MINUTE_IN_MILLIS)); - final float htiMins = HEAP_TRACK_INTERVAL / DateUtils.MINUTE_IN_MILLIS; - pw.println(String.format(" HEAP_TRACK_INTERVAL=%d (%.1f mins)", - HEAP_TRACK_INTERVAL, - htiMins)); - pw.println(String.format(" HEAP_TRACK_HISTORY_LEN=%d (%.1f hr total)", - HEAP_TRACK_HISTORY_LEN, - (float) HEAP_TRACK_HISTORY_LEN * htiMins / 60f)); - - pw.println("GarbageMonitor tracked processes:"); - - for (long pid : mPids) { - final ProcessMemInfo pmi = mData.get(pid); - if (pmi != null) { - pmi.dump(pw, args); - } - } - } - - - private static class MemoryIconDrawable extends Drawable { - long rss, limit; - final Drawable baseIcon; - final Paint paint = new Paint(); - final float dp; - - MemoryIconDrawable(Context context) { - baseIcon = context.getDrawable(R.drawable.ic_memory).mutate(); - dp = context.getResources().getDisplayMetrics().density; - paint.setColor(Color.WHITE); - } - - public void setRss(long rss) { - if (rss != this.rss) { - this.rss = rss; - invalidateSelf(); - } - } - - public void setLimit(long limit) { - if (limit != this.limit) { - this.limit = limit; - invalidateSelf(); - } - } - - @Override - public void draw(Canvas canvas) { - baseIcon.draw(canvas); - - if (limit > 0 && rss > 0) { - float frac = Math.min(1f, (float) rss / limit); - - final Rect bounds = getBounds(); - canvas.translate(bounds.left + 8 * dp, bounds.top + 5 * dp); - //android:pathData="M16.0,5.0l-8.0,0.0l0.0,14.0l8.0,0.0z" - canvas.drawRect(0, 14 * dp * (1 - frac), 8 * dp + 1, 14 * dp + 1, paint); - } - } - - @Override - public void setBounds(int left, int top, int right, int bottom) { - super.setBounds(left, top, right, bottom); - baseIcon.setBounds(left, top, right, bottom); - } - - @Override - public int getIntrinsicHeight() { - return baseIcon.getIntrinsicHeight(); - } - - @Override - public int getIntrinsicWidth() { - return baseIcon.getIntrinsicWidth(); - } - - @Override - public void setAlpha(int i) { - baseIcon.setAlpha(i); - } - - @Override - public void setColorFilter(ColorFilter colorFilter) { - baseIcon.setColorFilter(colorFilter); - paint.setColorFilter(colorFilter); - } - - @Override - public void setTint(int tint) { - super.setTint(tint); - baseIcon.setTint(tint); - } - - @Override - public void setTintList(ColorStateList tint) { - super.setTintList(tint); - baseIcon.setTintList(tint); - } - - @Override - public void setTintMode(PorterDuff.Mode tintMode) { - super.setTintMode(tintMode); - baseIcon.setTintMode(tintMode); - } - - @Override - public int getOpacity() { - return PixelFormat.TRANSLUCENT; - } - } - - private static class MemoryGraphIcon extends QSTile.Icon { - long rss, limit; - - public void setRss(long rss) { - this.rss = rss; - } - - public void setHeapLimit(long limit) { - this.limit = limit; - } - - @Override - public Drawable getDrawable(Context context) { - final MemoryIconDrawable drawable = new MemoryIconDrawable(context); - drawable.setRss(rss); - drawable.setLimit(limit); - return drawable; - } - } - - public static class MemoryTile extends QSTileImpl<QSTile.State> { - public static final String TILE_SPEC = "dbg:mem"; - - private final GarbageMonitor gm; - private ProcessMemInfo pmi; - private boolean dumpInProgress; - private final PanelInteractor mPanelInteractor; - - @Inject - public MemoryTile( - QSHost host, - QsEventLogger uiEventLogger, - @Background Looper backgroundLooper, - @Main Handler mainHandler, - FalsingManager falsingManager, - MetricsLogger metricsLogger, - StatusBarStateController statusBarStateController, - ActivityStarter activityStarter, - QSLogger qsLogger, - GarbageMonitor monitor, - PanelInteractor panelInteractor - ) { - super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, - statusBarStateController, activityStarter, qsLogger); - gm = monitor; - mPanelInteractor = panelInteractor; - } - - @Override - public State newTileState() { - return new QSTile.State(); - } - - @Override - public Intent getLongClickIntent() { - return new Intent(); - } - - @Override - protected void handleClick(@Nullable View view) { - if (dumpInProgress) return; - - dumpInProgress = true; - refreshState(); - new Thread("HeapDumpThread") { - @Override - public void run() { - try { - // wait for animations & state changes - Thread.sleep(500); - } catch (InterruptedException ignored) { } - final Intent shareIntent = gm.dumpHprofAndGetShareIntent(); - mHandler.post(() -> { - dumpInProgress = false; - refreshState(); - mPanelInteractor.collapsePanels(); - mActivityStarter.postStartActivityDismissingKeyguard(shareIntent, 0); - }); - } - }.start(); - } - - @Override - public int getMetricsCategory() { - return VIEW_UNKNOWN; - } - - @Override - public void handleSetListening(boolean listening) { - super.handleSetListening(listening); - if (gm != null) gm.setTile(listening ? this : null); - - final ActivityManager am = mContext.getSystemService(ActivityManager.class); - if (listening && gm.mHeapLimit > 0) { - am.setWatchHeapLimit(1024 * gm.mHeapLimit); // why is this in bytes? - } else { - am.clearWatchHeapLimit(); - } - } - - @Override - public CharSequence getTileLabel() { - return getState().label; - } - - @Override - protected void handleUpdateState(State state, Object arg) { - pmi = gm.getMemInfo(Process.myPid()); - final MemoryGraphIcon icon = new MemoryGraphIcon(); - icon.setHeapLimit(gm.mHeapLimit); - state.state = dumpInProgress ? STATE_UNAVAILABLE : STATE_ACTIVE; - state.label = dumpInProgress - ? "Dumping..." - : mContext.getString(R.string.heap_dump_tile_name); - if (pmi != null) { - icon.setRss(pmi.currentRss); - state.secondaryLabel = - String.format( - "rss: %s / %s", - formatBytes(pmi.currentRss * 1024), - formatBytes(gm.mHeapLimit * 1024)); - } else { - icon.setRss(0); - state.secondaryLabel = null; - } - state.icon = icon; - } - - public void update() { - refreshState(); - } - - public long getRss() { - return pmi != null ? pmi.currentRss : 0; - } - - public long getHeapLimit() { - return gm != null ? gm.mHeapLimit : 0; - } - } - - /** */ - public static class ProcessMemInfo implements Dumpable { - public long pid; - public String name; - public long startTime; - public long currentRss; - public long[] rss = new long[HEAP_TRACK_HISTORY_LEN]; - public long max = 1; - public int head = 0; - - public ProcessMemInfo(long pid, String name, long start) { - this.pid = pid; - this.name = name; - this.startTime = start; - } - - public long getUptime() { - return System.currentTimeMillis() - startTime; - } - - @Override - public void dump(PrintWriter pw, @Nullable String[] args) { - pw.print("{ \"pid\": "); - pw.print(pid); - pw.print(", \"name\": \""); - pw.print(name.replace('"', '-')); - pw.print("\", \"start\": "); - pw.print(startTime); - pw.print(", \"rss\": ["); - // write rss values starting from the oldest, which is rss[head], wrapping around to - // rss[(head-1) % rss.length] - for (int i = 0; i < rss.length; i++) { - if (i > 0) pw.print(","); - pw.print(rss[(head + i) % rss.length]); - } - pw.println("] }"); - } - } - - /** */ - @SysUISingleton - public static class Service implements CoreStartable, Dumpable { - private final Context mContext; - private final GarbageMonitor mGarbageMonitor; - - @Inject - public Service(Context context, GarbageMonitor garbageMonitor) { - mContext = context; - mGarbageMonitor = garbageMonitor; - } - - @Override - public void start() { - boolean forceEnable = - Settings.Secure.getInt( - mContext.getContentResolver(), FORCE_ENABLE_LEAK_REPORTING, 0) - != 0; - if (LEAK_REPORTING_ENABLED || forceEnable) { - mGarbageMonitor.startLeakMonitor(); - } - if (HEAP_TRACKING_ENABLED || forceEnable) { - mGarbageMonitor.startHeapTracking(); - } - } - - @Override - public void dump(PrintWriter pw, @Nullable String[] args) { - if (mGarbageMonitor != null) mGarbageMonitor.dump(pw, args); - } - } - - private void doGarbageInspection(int id) { - if (gcAndCheckGarbage()) { - mDelayableExecutor.executeDelayed(this::reinspectGarbageAfterGc, 100); - } - - mMessageRouter.cancelMessages(DO_GARBAGE_INSPECTION); - mMessageRouter.sendMessageDelayed(DO_GARBAGE_INSPECTION, GARBAGE_INSPECTION_INTERVAL); - } - - private void doHeapTrack(int id) { - update(); - mMessageRouter.cancelMessages(DO_HEAP_TRACK); - mMessageRouter.sendMessageDelayed(DO_HEAP_TRACK, HEAP_TRACK_INTERVAL); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitorModule.kt b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitorModule.kt deleted file mode 100644 index e975200e4d52..000000000000 --- a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitorModule.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.systemui.util.leak - -import com.android.systemui.CoreStartable -import com.android.systemui.qs.tileimpl.QSTileImpl -import dagger.Binds -import dagger.Module -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap -import dagger.multibindings.StringKey - -@Module -interface GarbageMonitorModule { - /** Inject into GarbageMonitor.Service. */ - @Binds - @IntoMap - @ClassKey(GarbageMonitor::class) - fun bindGarbageMonitorService(sysui: GarbageMonitor.Service): CoreStartable - - @Binds - @IntoMap - @StringKey(GarbageMonitor.MemoryTile.TILE_SPEC) - fun bindMemoryTile(memoryTile: GarbageMonitor.MemoryTile): QSTileImpl<*> -} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java index 88f63ad9c8cb..a2499615e18c 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java @@ -34,14 +34,12 @@ import android.widget.LinearLayout; import android.widget.RelativeLayout; import com.android.systemui.SysuiTestCase; -import com.android.systemui.common.ui.ConfigurationState; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory; import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager; -import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel; import com.android.systemui.log.LogBuffer; import com.android.systemui.plugins.clocks.ClockAnimations; import com.android.systemui.plugins.clocks.ClockController; @@ -56,14 +54,9 @@ import com.android.systemui.shared.clocks.AnimatableClockView; import com.android.systemui.shared.clocks.ClockRegistry; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController; -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore; -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker; -import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel; -import com.android.systemui.statusbar.phone.DozeParameters; +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerAlwaysOnDisplayViewBinder; import com.android.systemui.statusbar.phone.NotificationIconAreaController; import com.android.systemui.statusbar.phone.NotificationIconContainer; -import com.android.systemui.statusbar.phone.ScreenOffAnimationController; -import com.android.systemui.statusbar.ui.SystemBarUtilsState; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.util.time.FakeSystemClock; @@ -185,9 +178,7 @@ public class KeyguardClockSwitchControllerBaseTest extends SysuiTestCase { mKeyguardSliceViewController, mNotificationIconAreaController, mSmartspaceController, - mock(SystemBarUtilsState.class), - mock(ScreenOffAnimationController.class), - mock(StatusBarIconViewBindingFailureTracker.class), + mock(NotificationIconContainerAlwaysOnDisplayViewBinder.class), mKeyguardUnlockAnimationController, mSecureSettings, mExecutor, @@ -195,11 +186,6 @@ public class KeyguardClockSwitchControllerBaseTest extends SysuiTestCase { mDumpManager, mClockEventController, mLogBuffer, - mock(NotificationIconContainerAlwaysOnDisplayViewModel.class), - mock(KeyguardRootViewModel.class), - mock(ConfigurationState.class), - mock(DozeParameters.class), - mock(AlwaysOnDisplayNotificationIconViewStore.class), KeyguardInteractorFactory.create(mFakeFeatureFlags).getKeyguardInteractor(), mKeyguardClockInteractor, mFakeFeatureFlags, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelTest.kt index fd5a584935ef..1b6aaabd4fd6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelTest.kt @@ -22,17 +22,13 @@ import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepositor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic -import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition -import com.android.systemui.keyguard.ui.viewmodel.deviceEntryIconViewModelTransitionsMock +import com.android.systemui.keyguard.ui.viewmodel.fakeDeviceEntryIconViewModelTransition import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.phone.SystemUIDialogManager import com.android.systemui.statusbar.phone.systemUIDialogManager import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -48,25 +44,14 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(JUnit4::class) class DeviceEntryUdfpsTouchOverlayViewModelTest : SysuiTestCase() { - val kosmos = + private val kosmos = testKosmos().apply { fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, true) } } - val testScope = kosmos.testScope - - private val testDeviceEntryIconTransitionAlpha = MutableStateFlow(0f) - private val testDeviceEntryIconTransition: DeviceEntryIconTransition - get() = - object : DeviceEntryIconTransition { - override val deviceEntryParentViewAlpha: Flow<Float> = - testDeviceEntryIconTransitionAlpha.asStateFlow() - } - - init { - kosmos.deviceEntryIconViewModelTransitionsMock.add(testDeviceEntryIconTransition) - } private val systemUIDialogManager = kosmos.systemUIDialogManager private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository + private val testScope = kosmos.testScope + private val deviceEntryIconViewModelTransition = kosmos.fakeDeviceEntryIconViewModelTransition private val underTest = kosmos.deviceEntryUdfpsTouchOverlayViewModel @Captor @@ -82,7 +67,7 @@ class DeviceEntryUdfpsTouchOverlayViewModelTest : SysuiTestCase() { testScope.runTest { val shouldHandleTouches by collectLastValue(underTest.shouldHandleTouches) - testDeviceEntryIconTransitionAlpha.value = 1f + deviceEntryIconViewModelTransition.setDeviceEntryParentViewAlpha(1f) runCurrent() verify(systemUIDialogManager).registerListener(sysuiDialogListenerCaptor.capture()) @@ -96,7 +81,7 @@ class DeviceEntryUdfpsTouchOverlayViewModelTest : SysuiTestCase() { testScope.runTest { val shouldHandleTouches by collectLastValue(underTest.shouldHandleTouches) - testDeviceEntryIconTransitionAlpha.value = .3f + deviceEntryIconViewModelTransition.setDeviceEntryParentViewAlpha(.3f) runCurrent() verify(systemUIDialogManager).registerListener(sysuiDialogListenerCaptor.capture()) @@ -110,7 +95,7 @@ class DeviceEntryUdfpsTouchOverlayViewModelTest : SysuiTestCase() { testScope.runTest { val shouldHandleTouches by collectLastValue(underTest.shouldHandleTouches) - testDeviceEntryIconTransitionAlpha.value = 1f + deviceEntryIconViewModelTransition.setDeviceEntryParentViewAlpha(1f) runCurrent() verify(systemUIDialogManager).registerListener(sysuiDialogListenerCaptor.capture()) @@ -124,7 +109,7 @@ class DeviceEntryUdfpsTouchOverlayViewModelTest : SysuiTestCase() { testScope.runTest { val shouldHandleTouches by collectLastValue(underTest.shouldHandleTouches) - testDeviceEntryIconTransitionAlpha.value = 0f + deviceEntryIconViewModelTransition.setDeviceEntryParentViewAlpha(0f) runCurrent() bouncerRepository.setAlternateVisible(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt new file mode 100644 index 000000000000..93ce86a2959e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt @@ -0,0 +1,157 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.deviceentry.domain.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.accessibility.data.repository.fakeAccessibilityRepository +import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.data.ui.viewmodel.udfpsAccessibilityOverlayViewModel +import com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.ui.viewmodel.fakeDeviceEntryIconViewModelTransition +import com.android.systemui.kosmos.testScope +import com.android.systemui.shade.data.repository.fakeShadeRepository +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +class UdfpsAccessibilityOverlayViewModelTest : SysuiTestCase() { + private val kosmos = + testKosmos().apply { + fakeFeatureFlagsClassic.apply { set(FULL_SCREEN_USER_SWITCHER, false) } + } + private val deviceEntryIconTransition = kosmos.fakeDeviceEntryIconViewModelTransition + private val testScope = kosmos.testScope + private val accessibilityRepository = kosmos.fakeAccessibilityRepository + private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository + private val deviceEntryFingerprintAuthRepository = kosmos.deviceEntryFingerprintAuthRepository + private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository + private val shadeRepository = kosmos.fakeShadeRepository + private val underTest = kosmos.udfpsAccessibilityOverlayViewModel + + @Test + fun visible() = + testScope.runTest { + val visible by collectLastValue(underTest.visible) + setupVisibleStateOnLockscreen() + assertThat(visible).isTrue() + } + + @Test + fun touchExplorationNotEnabled_overlayNotVisible() = + testScope.runTest { + val visible by collectLastValue(underTest.visible) + setupVisibleStateOnLockscreen() + accessibilityRepository.isTouchExplorationEnabled.value = false + assertThat(visible).isFalse() + } + + @Test + fun deviceEntryFgIconViewModelAod_overlayNotVisible() = + testScope.runTest { + val visible by collectLastValue(underTest.visible) + setupVisibleStateOnLockscreen() + + // AOD + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + value = 0f, + transitionState = TransitionState.STARTED, + ) + ) + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + value = 1f, + transitionState = TransitionState.FINISHED, + ) + ) + assertThat(visible).isFalse() + } + + @Test + fun deviceUnlocked_overlayNotVisible() = + testScope.runTest { + val visible by collectLastValue(underTest.visible) + setupVisibleStateOnLockscreen() + deviceEntryRepository.setUnlocked(true) + assertThat(visible).isFalse() + } + + @Test + fun deviceEntryViewAlpha0_overlayNotVisible() = + testScope.runTest { + val visible by collectLastValue(underTest.visible) + setupVisibleStateOnLockscreen() + deviceEntryIconTransition.setDeviceEntryParentViewAlpha(0f) + assertThat(visible).isFalse() + } + + private suspend fun setupVisibleStateOnLockscreen() { + // A11y enabled + accessibilityRepository.isTouchExplorationEnabled.value = true + + // Transition alpha is 1f + deviceEntryIconTransition.setDeviceEntryParentViewAlpha(1f) + + // Listening for UDFPS + fingerprintPropertyRepository.supportsUdfps() + deviceEntryFingerprintAuthRepository.setIsRunning(true) + deviceEntryRepository.setUnlocked(false) + + // Lockscreen + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + value = 0f, + transitionState = TransitionState.STARTED, + ) + ) + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + value = 1f, + transitionState = TransitionState.FINISHED, + ) + ) + + // Shade not expanded + shadeRepository.qsExpansion.value = 0f + shadeRepository.lockscreenShadeExpansion.value = 0f + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt index 3109e761e423..ad86ee9f07d2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt @@ -37,6 +37,7 @@ import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopu import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection +import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines import com.android.systemui.util.mockito.whenever @@ -70,7 +71,8 @@ class DefaultKeyguardBlueprintTest : SysuiTestCase() { @Mock private lateinit var communalTutorialIndicatorSection: CommunalTutorialIndicatorSection @Mock private lateinit var clockSection: ClockSection @Mock private lateinit var smartspaceSection: SmartspaceSection - + @Mock + private lateinit var udfpsAccessibilityOverlaySection: DefaultUdfpsAccessibilityOverlaySection @Before fun setup() { MockitoAnnotations.initMocks(this) @@ -90,6 +92,7 @@ class DefaultKeyguardBlueprintTest : SysuiTestCase() { communalTutorialIndicatorSection, clockSection, smartspaceSection, + udfpsAccessibilityOverlaySection, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt deleted file mode 100644 index 6512290bf556..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (C) 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. - */ - -package com.android.systemui.keyguard.ui.viewmodel - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository -import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.doze.util.BurnInHelperWrapper -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.domain.interactor.BurnInInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory -import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor -import com.android.systemui.shade.data.repository.FakeShadeRepository -import com.android.systemui.statusbar.phone.SystemUIDialogManager -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations - -@ExperimentalCoroutinesApi -@SmallTest -@RunWith(AndroidJUnit4::class) -class UdfpsAodViewModelTest : SysuiTestCase() { - private val defaultPadding = 12 - private lateinit var underTest: UdfpsAodViewModel - - private lateinit var testScope: TestScope - private lateinit var configRepository: FakeConfigurationRepository - private lateinit var bouncerRepository: KeyguardBouncerRepository - private lateinit var keyguardRepository: FakeKeyguardRepository - private lateinit var shadeRepository: FakeShadeRepository - private lateinit var keyguardInteractor: KeyguardInteractor - - @Mock private lateinit var dialogManager: SystemUIDialogManager - @Mock private lateinit var burnInHelper: BurnInHelperWrapper - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - overrideResource(com.android.systemui.res.R.dimen.lock_icon_padding, defaultPadding) - testScope = TestScope() - shadeRepository = FakeShadeRepository() - KeyguardInteractorFactory.create().also { - keyguardInteractor = it.keyguardInteractor - keyguardRepository = it.repository - configRepository = it.configurationRepository - bouncerRepository = it.bouncerRepository - } - val udfpsKeyguardInteractor = - UdfpsKeyguardInteractor( - configRepository, - BurnInInteractor( - context, - burnInHelper, - testScope.backgroundScope, - configRepository, - keyguardInteractor, - ), - keyguardInteractor, - shadeRepository, - dialogManager, - ) - - underTest = - UdfpsAodViewModel( - udfpsKeyguardInteractor, - context, - ) - } - - @Test - fun alphaAndVisibleUpdates_onDozeAmountChanges() = - testScope.runTest { - val alpha by collectLastValue(underTest.alpha) - val visible by collectLastValue(underTest.isVisible) - - keyguardRepository.setDozeAmount(0f) - runCurrent() - assertThat(alpha).isEqualTo(0f) - assertThat(visible).isFalse() - - keyguardRepository.setDozeAmount(.65f) - runCurrent() - assertThat(alpha).isEqualTo(.65f) - assertThat(visible).isTrue() - - keyguardRepository.setDozeAmount(.23f) - runCurrent() - assertThat(alpha).isEqualTo(.23f) - assertThat(visible).isTrue() - - keyguardRepository.setDozeAmount(1f) - runCurrent() - assertThat(alpha).isEqualTo(1f) - assertThat(visible).isTrue() - } - - @Test - fun paddingUpdates_onScaleForResolutionChanges() = - testScope.runTest { - val padding by collectLastValue(underTest.padding) - - configRepository.setScaleForResolution(1f) - runCurrent() - assertThat(padding).isEqualTo(defaultPadding) - - configRepository.setScaleForResolution(2f) - runCurrent() - assertThat(padding).isEqualTo(defaultPadding * 2) - - configRepository.setScaleForResolution(.5f) - runCurrent() - assertThat(padding).isEqualTo((defaultPadding * .5f).toInt()) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt deleted file mode 100644 index 95b2fe554f0c..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (C) 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. - */ - -package com.android.systemui.keyguard.ui.viewmodel - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository -import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository -import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.doze.util.BurnInHelperWrapper -import com.android.systemui.keyguard.data.repository.FakeCommandQueue -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository -import com.android.systemui.keyguard.domain.interactor.BurnInInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory -import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor -import com.android.systemui.shade.data.repository.FakeShadeRepository -import com.android.systemui.statusbar.phone.SystemUIDialogManager -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations - -/** Tests UdfpsFingerprintViewModel specific flows. */ -@ExperimentalCoroutinesApi -@SmallTest -@RunWith(AndroidJUnit4::class) -class UdfpsFingerprintViewModelTest : SysuiTestCase() { - private val defaultPadding = 12 - private lateinit var underTest: FingerprintViewModel - - private lateinit var testScope: TestScope - private lateinit var configRepository: FakeConfigurationRepository - private lateinit var bouncerRepository: KeyguardBouncerRepository - private lateinit var keyguardRepository: FakeKeyguardRepository - private lateinit var fakeCommandQueue: FakeCommandQueue - private lateinit var transitionRepository: FakeKeyguardTransitionRepository - private lateinit var shadeRepository: FakeShadeRepository - - @Mock private lateinit var burnInHelper: BurnInHelperWrapper - @Mock private lateinit var dialogManager: SystemUIDialogManager - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - overrideResource(com.android.systemui.res.R.dimen.lock_icon_padding, defaultPadding) - testScope = TestScope() - configRepository = FakeConfigurationRepository() - keyguardRepository = FakeKeyguardRepository() - bouncerRepository = FakeKeyguardBouncerRepository() - fakeCommandQueue = FakeCommandQueue() - bouncerRepository = FakeKeyguardBouncerRepository() - transitionRepository = FakeKeyguardTransitionRepository() - shadeRepository = FakeShadeRepository() - val keyguardInteractor = - KeyguardInteractorFactory.create( - repository = keyguardRepository, - ) - .keyguardInteractor - - val transitionInteractor = - KeyguardTransitionInteractorFactory.create( - scope = testScope.backgroundScope, - repository = transitionRepository, - keyguardInteractor = keyguardInteractor, - ) - .keyguardTransitionInteractor - - underTest = - FingerprintViewModel( - context, - transitionInteractor, - UdfpsKeyguardInteractor( - configRepository, - BurnInInteractor( - context, - burnInHelper, - testScope.backgroundScope, - configRepository, - keyguardInteractor, - ), - keyguardInteractor, - shadeRepository, - dialogManager, - ), - keyguardInteractor, - ) - } - - @Test - fun paddingUpdates_onScaleForResolutionChanges() = - testScope.runTest { - val padding by collectLastValue(underTest.padding) - - configRepository.setScaleForResolution(1f) - runCurrent() - assertThat(padding).isEqualTo(defaultPadding) - - configRepository.setScaleForResolution(2f) - runCurrent() - assertThat(padding).isEqualTo(defaultPadding * 2) - - configRepository.setScaleForResolution(.5f) - runCurrent() - assertThat(padding).isEqualTo((defaultPadding * .5).toInt()) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt deleted file mode 100644 index 848a94b2a5d2..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt +++ /dev/null @@ -1,749 +0,0 @@ -/* - * Copyright (C) 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. - */ - -package com.android.systemui.keyguard.ui.viewmodel - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.settingslib.Utils -import com.android.systemui.SysuiTestCase -import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository -import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository -import com.android.systemui.keyguard.domain.interactor.BurnInInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory -import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor -import com.android.systemui.keyguard.shared.model.KeyguardState -import com.android.systemui.keyguard.shared.model.StatusBarState -import com.android.systemui.keyguard.shared.model.TransitionState -import com.android.systemui.keyguard.shared.model.TransitionStep -import com.android.systemui.shade.data.repository.FakeShadeRepository -import com.android.systemui.statusbar.phone.SystemUIDialogManager -import com.android.systemui.util.mockito.argumentCaptor -import com.android.systemui.util.mockito.mock -import com.android.wm.shell.animation.Interpolators -import com.google.common.collect.Range -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito -import org.mockito.MockitoAnnotations - -/** Tests UDFPS lockscreen view model transitions. */ -@ExperimentalCoroutinesApi -@SmallTest -@RunWith(AndroidJUnit4::class) -class UdfpsLockscreenViewModelTest : SysuiTestCase() { - private val lockscreenColorResId = android.R.attr.textColorPrimary - private val alternateBouncerResId = com.android.internal.R.attr.materialColorOnPrimaryFixed - private val lockscreenColor = Utils.getColorAttrDefaultColor(context, lockscreenColorResId) - private val alternateBouncerColor = - Utils.getColorAttrDefaultColor(context, alternateBouncerResId) - - @Mock private lateinit var dialogManager: SystemUIDialogManager - - private lateinit var underTest: UdfpsLockscreenViewModel - private lateinit var testScope: TestScope - private lateinit var transitionRepository: FakeKeyguardTransitionRepository - private lateinit var configRepository: FakeConfigurationRepository - private lateinit var keyguardRepository: FakeKeyguardRepository - private lateinit var keyguardInteractor: KeyguardInteractor - private lateinit var bouncerRepository: FakeKeyguardBouncerRepository - private lateinit var shadeRepository: FakeShadeRepository - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - testScope = TestScope() - transitionRepository = FakeKeyguardTransitionRepository() - shadeRepository = FakeShadeRepository() - KeyguardInteractorFactory.create().also { - keyguardInteractor = it.keyguardInteractor - keyguardRepository = it.repository - configRepository = it.configurationRepository - bouncerRepository = it.bouncerRepository - } - - val transitionInteractor = - KeyguardTransitionInteractorFactory.create( - scope = testScope.backgroundScope, - repository = transitionRepository, - keyguardInteractor = keyguardInteractor, - ) - .keyguardTransitionInteractor - - underTest = - UdfpsLockscreenViewModel( - context, - lockscreenColorResId, - alternateBouncerResId, - transitionInteractor, - UdfpsKeyguardInteractor( - configRepository, - BurnInInteractor( - context, - burnInHelperWrapper = mock(), - testScope.backgroundScope, - configRepository, - keyguardInteractor, - ), - keyguardInteractor, - shadeRepository, - dialogManager, - ), - keyguardInteractor, - ) - } - - @Test - fun goneToAodTransition() = - testScope.runTest { - val transition by collectLastValue(underTest.transition) - val visible by collectLastValue(underTest.visible) - - // TransitionState.STARTED: gone -> AOD - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.GONE, - to = KeyguardState.AOD, - value = 0f, - transitionState = TransitionState.STARTED, - ownerName = "goneToAodTransition", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(0f) - assertThat(visible).isFalse() - - // TransitionState.RUNNING: gone -> AOD - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.GONE, - to = KeyguardState.AOD, - value = .6f, - transitionState = TransitionState.RUNNING, - ownerName = "goneToAodTransition", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(0f) - assertThat(visible).isFalse() - - // TransitionState.FINISHED: gone -> AOD - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.GONE, - to = KeyguardState.AOD, - value = 1f, - transitionState = TransitionState.FINISHED, - ownerName = "goneToAodTransition", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(0f) - assertThat(visible).isFalse() - } - - @Test - fun lockscreenToAod() = - testScope.runTest { - val transition by collectLastValue(underTest.transition) - val visible by collectLastValue(underTest.visible) - keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD) - - // TransitionState.STARTED: lockscreen -> AOD - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.AOD, - value = 0f, - transitionState = TransitionState.STARTED, - ownerName = "lockscreenToAod", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(1f) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(lockscreenColor) - assertThat(visible).isTrue() - - // TransitionState.RUNNING: lockscreen -> AOD - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.AOD, - value = .6f, - transitionState = TransitionState.RUNNING, - ownerName = "lockscreenToAod", - ) - ) - runCurrent() - assertThat(transition?.alpha).isIn(Range.closed(.39f, .41f)) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(lockscreenColor) - assertThat(visible).isTrue() - - // TransitionState.FINISHED: lockscreen -> AOD - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.AOD, - value = 1f, - transitionState = TransitionState.FINISHED, - ownerName = "lockscreenToAod", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(0f) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(lockscreenColor) - assertThat(visible).isFalse() - } - - @Test - fun lockscreenShadeLockedToAod() = - testScope.runTest { - val transition by collectLastValue(underTest.transition) - val visible by collectLastValue(underTest.visible) - keyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED) - - // TransitionState.STARTED: lockscreen -> AOD - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.AOD, - value = 0f, - transitionState = TransitionState.STARTED, - ownerName = "lockscreenToAod", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(0f) - assertThat(visible).isFalse() - - // TransitionState.RUNNING: lockscreen -> AOD - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.AOD, - value = .6f, - transitionState = TransitionState.RUNNING, - ownerName = "lockscreenToAod", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(0f) - assertThat(visible).isFalse() - - // TransitionState.FINISHED: lockscreen -> AOD - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.AOD, - value = 1f, - transitionState = TransitionState.FINISHED, - ownerName = "lockscreenToAod", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(0f) - assertThat(visible).isFalse() - } - - @Test - fun aodToLockscreen() = - testScope.runTest { - val transition by collectLastValue(underTest.transition) - val visible by collectLastValue(underTest.visible) - - // TransitionState.STARTED: AOD -> lockscreen - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.AOD, - to = KeyguardState.LOCKSCREEN, - value = 0f, - transitionState = TransitionState.STARTED, - ownerName = "aodToLockscreen", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(0f) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(lockscreenColor) - assertThat(visible).isFalse() - - // TransitionState.RUNNING: AOD -> lockscreen - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.AOD, - to = KeyguardState.LOCKSCREEN, - value = .6f, - transitionState = TransitionState.RUNNING, - ownerName = "aodToLockscreen", - ) - ) - runCurrent() - assertThat(transition?.alpha).isIn(Range.closed(.59f, .61f)) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(lockscreenColor) - assertThat(visible).isTrue() - - // TransitionState.FINISHED: AOD -> lockscreen - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.AOD, - to = KeyguardState.LOCKSCREEN, - value = 1f, - transitionState = TransitionState.FINISHED, - ownerName = "aodToLockscreen", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(1f) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(lockscreenColor) - assertThat(visible).isTrue() - } - - @Test - fun lockscreenToAlternateBouncer() = - testScope.runTest { - val transition by collectLastValue(underTest.transition) - val visible by collectLastValue(underTest.visible) - keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD) - - // TransitionState.STARTED: lockscreen -> alternate bouncer - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.ALTERNATE_BOUNCER, - value = 0f, - transitionState = TransitionState.STARTED, - ownerName = "lockscreenToAlternateBouncer", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(1f) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(alternateBouncerColor) - assertThat(visible).isTrue() - - // TransitionState.RUNNING: lockscreen -> alternate bouncer - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.ALTERNATE_BOUNCER, - value = .6f, - transitionState = TransitionState.RUNNING, - ownerName = "lockscreenToAlternateBouncer", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(1f) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(alternateBouncerColor) - assertThat(visible).isTrue() - - // TransitionState.FINISHED: lockscreen -> alternate bouncer - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.ALTERNATE_BOUNCER, - value = 1f, - transitionState = TransitionState.FINISHED, - ownerName = "lockscreenToAlternateBouncer", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(1f) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(alternateBouncerColor) - assertThat(visible).isTrue() - } - - fun alternateBouncerToPrimaryBouncer() = - testScope.runTest { - val transition by collectLastValue(underTest.transition) - val visible by collectLastValue(underTest.visible) - - // TransitionState.STARTED: alternate bouncer -> primary bouncer - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.ALTERNATE_BOUNCER, - to = KeyguardState.PRIMARY_BOUNCER, - value = 0f, - transitionState = TransitionState.STARTED, - ownerName = "alternateBouncerToPrimaryBouncer", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(1f) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(alternateBouncerColor) - assertThat(visible).isTrue() - - // TransitionState.RUNNING: alternate bouncer -> primary bouncer - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.ALTERNATE_BOUNCER, - to = KeyguardState.PRIMARY_BOUNCER, - value = .6f, - transitionState = TransitionState.RUNNING, - ownerName = "alternateBouncerToPrimaryBouncer", - ) - ) - runCurrent() - assertThat(transition?.alpha).isIn(Range.closed(.59f, .61f)) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(alternateBouncerColor) - assertThat(visible).isTrue() - - // TransitionState.FINISHED: alternate bouncer -> primary bouncer - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.ALTERNATE_BOUNCER, - to = KeyguardState.PRIMARY_BOUNCER, - value = 1f, - transitionState = TransitionState.FINISHED, - ownerName = "alternateBouncerToPrimaryBouncer", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(0f) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(alternateBouncerColor) - assertThat(visible).isFalse() - } - - fun alternateBouncerToAod() = - testScope.runTest { - val transition by collectLastValue(underTest.transition) - val visible by collectLastValue(underTest.visible) - - // TransitionState.STARTED: alternate bouncer -> aod - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.ALTERNATE_BOUNCER, - to = KeyguardState.AOD, - value = 0f, - transitionState = TransitionState.STARTED, - ownerName = "alternateBouncerToAod", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(1f) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(alternateBouncerColor) - assertThat(visible).isTrue() - - // TransitionState.RUNNING: alternate bouncer -> aod - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.ALTERNATE_BOUNCER, - to = KeyguardState.AOD, - value = .6f, - transitionState = TransitionState.RUNNING, - ownerName = "alternateBouncerToAod", - ) - ) - runCurrent() - assertThat(transition?.alpha).isIn(Range.closed(.39f, .41f)) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(alternateBouncerColor) - assertThat(visible).isTrue() - - // TransitionState.FINISHED: alternate bouncer -> aod - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.ALTERNATE_BOUNCER, - to = KeyguardState.AOD, - value = 1f, - transitionState = TransitionState.FINISHED, - ownerName = "alternateBouncerToAod", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(0f) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(alternateBouncerColor) - assertThat(visible).isFalse() - } - - @Test - fun lockscreenToOccluded() = - testScope.runTest { - val transition by collectLastValue(underTest.transition) - val visible by collectLastValue(underTest.visible) - keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD) - - // TransitionState.STARTED: lockscreen -> occluded - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.OCCLUDED, - value = 0f, - transitionState = TransitionState.STARTED, - ownerName = "lockscreenToOccluded", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(1f) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(lockscreenColor) - assertThat(visible).isTrue() - - // TransitionState.RUNNING: lockscreen -> occluded - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.OCCLUDED, - value = .6f, - transitionState = TransitionState.RUNNING, - ownerName = "lockscreenToOccluded", - ) - ) - runCurrent() - assertThat(transition?.alpha).isIn(Range.closed(.39f, .41f)) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(lockscreenColor) - assertThat(visible).isTrue() - - // TransitionState.FINISHED: lockscreen -> occluded - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.OCCLUDED, - value = 1f, - transitionState = TransitionState.FINISHED, - ownerName = "lockscreenToOccluded", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(0f) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(lockscreenColor) - assertThat(visible).isFalse() - } - - @Test - fun occludedToLockscreen() = - testScope.runTest { - val transition by collectLastValue(underTest.transition) - val visible by collectLastValue(underTest.visible) - - // TransitionState.STARTED: occluded -> lockscreen - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.OCCLUDED, - to = KeyguardState.LOCKSCREEN, - value = 0f, - transitionState = TransitionState.STARTED, - ownerName = "occludedToLockscreen", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(1f) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(lockscreenColor) - assertThat(visible).isTrue() - - // TransitionState.RUNNING: occluded -> lockscreen - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.OCCLUDED, - to = KeyguardState.LOCKSCREEN, - value = .6f, - transitionState = TransitionState.RUNNING, - ownerName = "occludedToLockscreen", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(1f) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(lockscreenColor) - assertThat(visible).isTrue() - - // TransitionState.FINISHED: occluded -> lockscreen - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.OCCLUDED, - to = KeyguardState.LOCKSCREEN, - value = 1f, - transitionState = TransitionState.FINISHED, - ownerName = "occludedToLockscreen", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(1f) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(lockscreenColor) - assertThat(visible).isTrue() - } - - @Test - fun qsProgressChange() = - testScope.runTest { - val transition by collectLastValue(underTest.transition) - val visible by collectLastValue(underTest.visible) - givenTransitionToLockscreenFinished() - - // qsExpansion = 0f - shadeRepository.setQsExpansion(0f) - runCurrent() - assertThat(transition?.alpha).isEqualTo(1f) - assertThat(visible).isEqualTo(true) - - // qsExpansion = .25 - shadeRepository.setQsExpansion(.2f) - runCurrent() - assertThat(transition?.alpha).isEqualTo(.6f) - assertThat(visible).isEqualTo(true) - - // qsExpansion = .5 - shadeRepository.setQsExpansion(.5f) - runCurrent() - assertThat(transition?.alpha).isEqualTo(0f) - assertThat(visible).isEqualTo(false) - - // qsExpansion = 1 - shadeRepository.setQsExpansion(1f) - runCurrent() - assertThat(transition?.alpha).isEqualTo(0f) - assertThat(visible).isEqualTo(false) - } - - @Test - fun shadeExpansionChanged() = - testScope.runTest { - val transition by collectLastValue(underTest.transition) - val visible by collectLastValue(underTest.visible) - givenTransitionToLockscreenFinished() - - // shadeExpansion = 0f - shadeRepository.setUdfpsTransitionToFullShadeProgress(0f) - runCurrent() - assertThat(transition?.alpha).isEqualTo(1f) - assertThat(visible).isEqualTo(true) - - // shadeExpansion = .2 - shadeRepository.setUdfpsTransitionToFullShadeProgress(.2f) - runCurrent() - assertThat(transition?.alpha).isEqualTo(.8f) - assertThat(visible).isEqualTo(true) - - // shadeExpansion = .5 - shadeRepository.setUdfpsTransitionToFullShadeProgress(.5f) - runCurrent() - assertThat(transition?.alpha).isEqualTo(.5f) - assertThat(visible).isEqualTo(true) - - // shadeExpansion = 1 - shadeRepository.setUdfpsTransitionToFullShadeProgress(1f) - runCurrent() - assertThat(transition?.alpha).isEqualTo(0f) - assertThat(visible).isEqualTo(false) - } - - @Test - fun dialogHideAffordancesRequestChanged() = - testScope.runTest { - val transition by collectLastValue(underTest.transition) - givenTransitionToLockscreenFinished() - runCurrent() - val captor = argumentCaptor<SystemUIDialogManager.Listener>() - Mockito.verify(dialogManager).registerListener(captor.capture()) - - captor.value.shouldHideAffordances(true) - assertThat(transition?.alpha).isEqualTo(0f) - - captor.value.shouldHideAffordances(false) - assertThat(transition?.alpha).isEqualTo(1f) - } - - @Test - fun occludedToAlternateBouncer() = - testScope.runTest { - val transition by collectLastValue(underTest.transition) - val visible by collectLastValue(underTest.visible) - - // TransitionState.STARTED: occluded -> alternate bouncer - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.OCCLUDED, - to = KeyguardState.ALTERNATE_BOUNCER, - value = 0f, - transitionState = TransitionState.STARTED, - ownerName = "occludedToAlternateBouncer", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(1f) - assertThat(transition?.scale).isEqualTo(0f) - assertThat(transition?.color).isEqualTo(alternateBouncerColor) - assertThat(visible).isTrue() - - // TransitionState.RUNNING: occluded -> alternate bouncer - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.OCCLUDED, - to = KeyguardState.ALTERNATE_BOUNCER, - value = .6f, - transitionState = TransitionState.RUNNING, - ownerName = "occludedToAlternateBouncer", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(1f) - assertThat(transition?.scale) - .isEqualTo(Interpolators.FAST_OUT_SLOW_IN.getInterpolation(.6f)) - assertThat(transition?.color).isEqualTo(alternateBouncerColor) - assertThat(visible).isTrue() - - // TransitionState.FINISHED: occluded -> alternate bouncer - transitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.OCCLUDED, - to = KeyguardState.ALTERNATE_BOUNCER, - value = 1f, - transitionState = TransitionState.FINISHED, - ownerName = "occludedToAlternateBouncer", - ) - ) - runCurrent() - assertThat(transition?.alpha).isEqualTo(1f) - assertThat(transition?.scale).isEqualTo(1f) - assertThat(transition?.color).isEqualTo(alternateBouncerColor) - assertThat(visible).isTrue() - } - - private suspend fun givenTransitionToLockscreenFinished() { - transitionRepository.sendTransitionSteps( - from = KeyguardState.AOD, - to = KeyguardState.LOCKSCREEN, - testScope - ) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt index 1f99303f10ed..0a464e6047d3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt @@ -22,11 +22,11 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View import androidx.test.filters.SmallTest -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.res.R import com.android.systemui.util.animation.MeasurementInput import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.animation.TransitionViewState @@ -37,6 +37,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.floatThat import org.mockito.Mock +import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever @@ -183,10 +184,12 @@ class MediaViewControllerTest : SysuiTestCase() { // detail widgets occupy [90, 100] whenever(detailWidgetState.y).thenReturn(90F) whenever(detailWidgetState.height).thenReturn(10) + whenever(detailWidgetState.alpha).thenReturn(1F) // control widgets occupy [150, 170] whenever(controlWidgetState.y).thenReturn(150F) whenever(controlWidgetState.height).thenReturn(20) - // in current beizer, when the progress reach 0.38, the result will be 0.5 + whenever(controlWidgetState.alpha).thenReturn(1F) + // in current bezier, when the progress reach 0.38, the result will be 0.5 mediaViewController.squishViewState(mockViewState, 181.4F / 200F) verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } @@ -196,6 +199,34 @@ class MediaViewControllerTest : SysuiTestCase() { } @Test + fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_invisibleElements() { + whenever(mockViewState.copy()).thenReturn(mockCopiedState) + whenever(mockCopiedState.widgetStates) + .thenReturn( + mutableMapOf( + R.id.media_progress_bar to controlWidgetState, + R.id.header_artist to detailWidgetState + ) + ) + whenever(mockCopiedState.measureHeight).thenReturn(200) + // detail widgets occupy [90, 100] + whenever(detailWidgetState.y).thenReturn(90F) + whenever(detailWidgetState.height).thenReturn(10) + whenever(detailWidgetState.alpha).thenReturn(0F) + // control widgets occupy [150, 170] + whenever(controlWidgetState.y).thenReturn(150F) + whenever(controlWidgetState.height).thenReturn(20) + whenever(controlWidgetState.alpha).thenReturn(0F) + // Verify that alpha remains 0 throughout squishing + mediaViewController.squishViewState(mockViewState, 181.4F / 200F) + verify(controlWidgetState, never()).alpha = floatThat { it > 0 } + verify(detailWidgetState, never()).alpha = floatThat { it > 0 } + mediaViewController.squishViewState(mockViewState, 200F / 200F) + verify(controlWidgetState, never()).alpha = floatThat { it > 0 } + verify(detailWidgetState, never()).alpha = floatThat { it > 0 } + } + + @Test fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forRecommendation() { whenever(mockViewState.copy()).thenReturn(mockCopiedState) whenever(mockCopiedState.widgetStates) @@ -210,12 +241,15 @@ class MediaViewControllerTest : SysuiTestCase() { // media container widgets occupy [20, 300] whenever(mediaContainerWidgetState.y).thenReturn(20F) whenever(mediaContainerWidgetState.height).thenReturn(280) + whenever(mediaContainerWidgetState.alpha).thenReturn(1F) // media title widgets occupy [320, 330] whenever(mediaTitleWidgetState.y).thenReturn(320F) whenever(mediaTitleWidgetState.height).thenReturn(10) + whenever(mediaTitleWidgetState.alpha).thenReturn(1F) // media subtitle widgets occupy [340, 350] whenever(mediaSubTitleWidgetState.y).thenReturn(340F) whenever(mediaSubTitleWidgetState.height).thenReturn(10) + whenever(mediaSubTitleWidgetState.alpha).thenReturn(1F) // in current beizer, when the progress reach 0.38, the result will be 0.5 mediaViewController.squishViewState(mockViewState, 307.6F / 360F) diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt index 4d423242893a..b7618d290f53 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -75,7 +75,6 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runCurrent import org.junit.Before -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt @@ -463,7 +462,6 @@ internal class NoteTaskControllerTest : SysuiTestCase() { // region setNoteTaskShortcutEnabled @Test - @Ignore("b/316332684") fun setNoteTaskShortcutEnabled_setTrue() { createNoteTaskController().setNoteTaskShortcutEnabled(value = true, userTracker.userHandle) @@ -480,7 +478,6 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } @Test - @Ignore("b/316332684") fun setNoteTaskShortcutEnabled_setFalse() { createNoteTaskController().setNoteTaskShortcutEnabled(value = false, userTracker.userHandle) @@ -497,7 +494,6 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } @Test - @Ignore("b/316332684") fun setNoteTaskShortcutEnabled_workProfileUser_setTrue() { whenever(context.createContextAsUser(eq(workUserInfo.userHandle), any())) .thenReturn(workProfileContext) @@ -519,7 +515,6 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } @Test - @Ignore("b/316332684") fun setNoteTaskShortcutEnabled_workProfileUser_setFalse() { whenever(context.createContextAsUser(eq(workUserInfo.userHandle), any())) .thenReturn(workProfileContext) @@ -738,7 +733,6 @@ internal class NoteTaskControllerTest : SysuiTestCase() { // region internalUpdateNoteTaskAsUser @Test - @Ignore("b/316332684") fun updateNoteTaskAsUserInternal_withNotesRole_withShortcuts_shouldUpdateShortcuts() { createNoteTaskController(isEnabled = true) .launchUpdateNoteTaskAsUser(userTracker.userHandle) @@ -772,7 +766,6 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } @Test - @Ignore("b/316332684") fun updateNoteTaskAsUserInternal_noNotesRole_shouldDisableShortcuts() { whenever(roleManager.getRoleHoldersAsUser(ROLE_NOTES, userTracker.userHandle)) .thenReturn(emptyList()) @@ -796,7 +789,6 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } @Test - @Ignore("b/316332684") fun updateNoteTaskAsUserInternal_flagDisabled_shouldDisableShortcuts() { createNoteTaskController(isEnabled = false) .launchUpdateNoteTaskAsUser(userTracker.userHandle) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java index 5e2423a8b373..ef7798e545d3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java @@ -447,10 +447,6 @@ public class QSTileHostTest extends SysuiTestCase { mContext.getOrCreateTestableResources() .addOverride(R.string.quick_settings_tiles_default, "spec1,spec1"); List<String> specs = QSTileHost.loadTileSpecs(mContext, "default"); - - // Remove spurious tiles, like dbg:mem - specs.removeIf(spec -> !"spec1".equals(spec)); - assertEquals(1, specs.size()); } @Test @@ -458,10 +454,6 @@ public class QSTileHostTest extends SysuiTestCase { mContext.getOrCreateTestableResources() .addOverride(R.string.quick_settings_tiles_default, "spec1"); List<String> specs = QSTileHost.loadTileSpecs(mContext, "default,spec1"); - - // Remove spurious tiles, like dbg:mem - specs.removeIf(spec -> !"spec1".equals(spec)); - assertEquals(1, specs.size()); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt index 067218a9c983..5201e5d9ccf7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt @@ -50,7 +50,6 @@ import com.android.systemui.qs.tiles.RotationLockTile import com.android.systemui.qs.tiles.ScreenRecordTile import com.android.systemui.qs.tiles.UiModeNightTile import com.android.systemui.qs.tiles.WorkModeTile -import com.android.systemui.util.leak.GarbageMonitor import com.android.systemui.util.mockito.any import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -117,7 +116,6 @@ class QSFactoryImplTest : SysuiTestCase() { @Mock private lateinit var dataSaverTile: DataSaverTile @Mock private lateinit var nightDisplayTile: NightDisplayTile @Mock private lateinit var nfcTile: NfcTile - @Mock private lateinit var memoryTile: GarbageMonitor.MemoryTile @Mock private lateinit var darkModeTile: UiModeNightTile @Mock private lateinit var screenRecordTile: ScreenRecordTile @Mock private lateinit var reduceBrightColorsTile: ReduceBrightColorsTile diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt index b90ccc0e3d7e..994166172aff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Android Open Source Project + * Copyright (C) 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. @@ -16,35 +16,23 @@ package com.android.systemui.scene.shared.flag -import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.FakeFeatureFlagsImpl -import com.android.systemui.Flags as AconfigFlags import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FakeFeatureFlagsClassic +import com.android.systemui.compose.ComposeFacade import com.android.systemui.flags.Flags -import com.android.systemui.flags.ReleasedFlag -import com.android.systemui.flags.ResourceBooleanFlag -import com.android.systemui.flags.UnreleasedFlag +import com.android.systemui.flags.setFlagValue import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl import com.android.systemui.media.controls.util.MediaInSceneContainerFlag -import com.android.systemui.res.R -import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth +import org.junit.Assume import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.junit.runners.Parameterized @SmallTest -@RunWith(Parameterized::class) -internal class SceneContainerFlagsTest( - private val testCase: TestCase, -) : SysuiTestCase() { - - @Rule @JvmField val setFlagsRule: SetFlagsRule = SetFlagsRule() - - private lateinit var underTest: SceneContainerFlags +@RunWith(AndroidJUnit4::class) +internal class SceneContainerFlagsTest : SysuiTestCase() { @Before fun setUp() { @@ -52,83 +40,39 @@ internal class SceneContainerFlagsTest( // Flags.SCENE_CONTAINER_ENABLED is no longer needed. val field = Flags::class.java.getField("SCENE_CONTAINER_ENABLED") field.isAccessible = true - field.set(null, true) - - val featureFlags = - FakeFeatureFlagsClassic().apply { - SceneContainerFlagsImpl.classicFlagTokens.forEach { flagToken -> - when (flagToken) { - is ResourceBooleanFlag -> set(flagToken, testCase.areAllFlagsSet) - is ReleasedFlag -> set(flagToken, testCase.areAllFlagsSet) - is UnreleasedFlag -> set(flagToken, testCase.areAllFlagsSet) - else -> error("Unsupported flag type ${flagToken.javaClass}") - } - } - } - // TODO(b/306421592): get the aconfig FeatureFlags from the SetFlagsRule. - val aconfigFlags = FakeFeatureFlagsImpl() + field.set(null, true) // note: this does not work with multivalent tests + } + private fun setAconfigFlagsEnabled(enabled: Boolean) { listOf( - AconfigFlags.FLAG_SCENE_CONTAINER, - AconfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, + com.android.systemui.Flags.FLAG_SCENE_CONTAINER, + com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, KeyguardShadeMigrationNssl.FLAG_NAME, MediaInSceneContainerFlag.FLAG_NAME, ) - .forEach { flagToken -> - setFlagsRule.enableFlags(flagToken) - aconfigFlags.setFlag(flagToken, testCase.areAllFlagsSet) - overrideResource( - R.bool.config_sceneContainerFrameworkEnabled, - testCase.isResourceConfigEnabled - ) - } - - underTest = - SceneContainerFlagsImpl( - context = context, - featureFlagsClassic = featureFlags, - isComposeAvailable = testCase.isComposeAvailable, - ) + .forEach { flagName -> mSetFlagsRule.setFlagValue(flagName, enabled) } } @Test - fun isEnabled() { - assertThat(underTest.isEnabled()).isEqualTo(testCase.expectedEnabled) + fun isNotEnabled_withoutAconfigFlags() { + setAconfigFlagsEnabled(false) + Truth.assertThat(SceneContainerFlag.isEnabled).isEqualTo(false) + Truth.assertThat(SceneContainerFlagsImpl().isEnabled()).isEqualTo(false) } - internal data class TestCase( - val isComposeAvailable: Boolean, - val areAllFlagsSet: Boolean, - val isResourceConfigEnabled: Boolean, - val expectedEnabled: Boolean, - ) { - override fun toString(): String { - return "(compose=$isComposeAvailable + flags=$areAllFlagsSet) + XML" + - " config=$isResourceConfigEnabled -> expected=$expectedEnabled" - } + @Test + fun isEnabled_withAconfigFlags_withCompose() { + Assume.assumeTrue(ComposeFacade.isComposeAvailable()) + setAconfigFlagsEnabled(true) + Truth.assertThat(SceneContainerFlag.isEnabled).isEqualTo(true) + Truth.assertThat(SceneContainerFlagsImpl().isEnabled()).isEqualTo(true) } - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun testCases() = buildList { - repeat(8) { combination -> - val isComposeAvailable = combination and 0b100 != 0 - val areAllFlagsSet = combination and 0b010 != 0 - val isResourceConfigEnabled = combination and 0b001 != 0 - - val expectedEnabled = - isComposeAvailable && areAllFlagsSet && isResourceConfigEnabled - - add( - TestCase( - isComposeAvailable = isComposeAvailable, - areAllFlagsSet = areAllFlagsSet, - expectedEnabled = expectedEnabled, - isResourceConfigEnabled = isResourceConfigEnabled, - ) - ) - } - } + @Test + fun isNotEnabled_withAconfigFlags_withoutCompose() { + Assume.assumeFalse(ComposeFacade.isComposeAvailable()) + setAconfigFlagsEnabled(true) + Truth.assertThat(SceneContainerFlag.isEnabled).isEqualTo(false) + Truth.assertThat(SceneContainerFlagsImpl().isEnabled()).isEqualTo(false) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt index d4e8d37b3b44..72fc65b7c8d4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt @@ -10,8 +10,6 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.Guideline import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever @@ -39,7 +37,6 @@ class MessageContainerControllerTest : SysuiTestCase() { lateinit var detectionNoticeView: ViewGroup lateinit var container: FrameLayout - var featureFlags = FakeFeatureFlags() lateinit var screenshotView: ViewGroup val userHandle = UserHandle.of(5) @@ -55,7 +52,6 @@ class MessageContainerControllerTest : SysuiTestCase() { MessageContainerController( workProfileMessageController, screenshotDetectionController, - featureFlags ) screenshotView = ConstraintLayout(mContext) workProfileData = WorkProfileMessageController.WorkProfileFirstRunData(appName, icon) @@ -105,8 +101,6 @@ class MessageContainerControllerTest : SysuiTestCase() { @Test fun testOnScreenshotTakenScreenshotData_nothingToShow() { - featureFlags.set(Flags.SCREENSHOT_DETECTION, true) - messageContainer.onScreenshotTaken(screenshotData) verify(workProfileMessageController, never()).populateView(any(), any(), any()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt new file mode 100644 index 000000000000..80f8cf1559a6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.shared.notifications.data.repository + +import android.provider.Settings +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.shared.settings.data.repository.FakeSecureSettingsRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class NotificationSettingsRepositoryTest : SysuiTestCase() { + + private lateinit var underTest: NotificationSettingsRepository + + private lateinit var testScope: TestScope + private lateinit var secureSettingsRepository: FakeSecureSettingsRepository + + @Before + fun setUp() { + val testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) + secureSettingsRepository = FakeSecureSettingsRepository() + + underTest = + NotificationSettingsRepository( + scope = testScope.backgroundScope, + backgroundDispatcher = testDispatcher, + secureSettingsRepository = secureSettingsRepository, + ) + } + + @Test + fun testGetIsShowNotificationsOnLockscreenEnabled() = + testScope.runTest { + val showNotifs by collectLastValue(underTest.isShowNotificationsOnLockScreenEnabled) + + secureSettingsRepository.set( + name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, + value = 1, + ) + assertThat(showNotifs).isEqualTo(true) + + secureSettingsRepository.set( + name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, + value = 0, + ) + assertThat(showNotifs).isEqualTo(false) + } + + @Test + fun testSetIsShowNotificationsOnLockscreenEnabled() = + testScope.runTest { + val showNotifs by collectLastValue(underTest.isShowNotificationsOnLockScreenEnabled) + + underTest.setShowNotificationsOnLockscreenEnabled(true) + assertThat(showNotifs).isEqualTo(true) + + underTest.setShowNotificationsOnLockscreenEnabled(false) + assertThat(showNotifs).isEqualTo(false) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt index a56fb2c515a8..7d8cf3657ba1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt @@ -1,11 +1,10 @@ package com.android.systemui.statusbar.notification +import android.platform.test.annotations.EnableFlags import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags +import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import org.junit.Assert.assertEquals @@ -20,8 +19,7 @@ import org.mockito.Mockito.verify @RunWith(JUnit4::class) class RoundableTest : SysuiTestCase() { private val targetView: View = mock() - private val featureFlags = FakeFeatureFlags() - private val roundable = FakeRoundable(targetView = targetView, featureFlags = featureFlags) + private val roundable = FakeRoundable(targetView = targetView) @Test fun defaultConfig_shouldNotHaveRoundedCorner() { @@ -150,36 +148,36 @@ class RoundableTest : SysuiTestCase() { } @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) fun getCornerRadii_radius_maxed_to_height() { whenever(targetView.height).thenReturn(10) - featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true) roundable.requestRoundness(1f, 1f, SOURCE1) assertCornerRadiiEquals(5f, 5f) } @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) fun getCornerRadii_topRadius_maxed_to_height() { whenever(targetView.height).thenReturn(5) - featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true) roundable.requestRoundness(1f, 0f, SOURCE1) assertCornerRadiiEquals(5f, 0f) } @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) fun getCornerRadii_bottomRadius_maxed_to_height() { whenever(targetView.height).thenReturn(5) - featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true) roundable.requestRoundness(0f, 1f, SOURCE1) assertCornerRadiiEquals(0f, 5f) } @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) fun getCornerRadii_radii_kept() { whenever(targetView.height).thenReturn(100) - featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true) roundable.requestRoundness(1f, 1f, SOURCE1) assertCornerRadiiEquals(MAX_RADIUS, MAX_RADIUS) @@ -193,14 +191,12 @@ class RoundableTest : SysuiTestCase() { class FakeRoundable( targetView: View, radius: Float = MAX_RADIUS, - featureFlags: FeatureFlags ) : Roundable { override val roundableState = RoundableState( targetView = targetView, roundable = this, maxRadius = radius, - featureFlags = featureFlags ) override val clipHeight: Int diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt index f3094cdd4faf..170f651aed91 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt @@ -80,7 +80,7 @@ class NotificationsKeyguardViewStateRepositoryTest : SysuiTestCase() { assertThat(isPulseExpanding).isFalse() withArgCaptor { verify(mockWakeUpCoordinator).addListener(capture()) } - .onPulseExpansionChanged(true) + .onPulseExpandingChanged(true) runCurrent() assertThat(isPulseExpanding).isTrue() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index cb731082b89b..0cd834ded638 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -57,7 +57,6 @@ import com.android.systemui.TestableDependency; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -568,9 +567,6 @@ public class NotificationTestHelper { NotificationEntry entry, @InflationFlag int extraInflationFlags) throws Exception { - // NOTE: This flag is read when the ExpandableNotificationRow is inflated, so it needs to be - // set, but we do not want to override an existing value that is needed by a specific test. - mFeatureFlags.setDefault(Flags.IMPROVED_HUN_ANIMATIONS); LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( mContext.LAYOUT_INFLATER_SERVICE); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt index c8dbdc505aba..2df6e46d630f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt @@ -28,8 +28,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations +import org.mockito.Mockito.`when` as whenever /** Tests for {@link NotificationShelf}. */ @SmallTest @@ -53,7 +53,6 @@ open class NotificationShelfTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) mDependency.injectTestDependency(FeatureFlags::class.java, flags) flags.set(Flags.SENSITIVE_REVEAL_ANIM, useSensitiveReveal) - flags.setDefault(Flags.IMPROVED_HUN_ANIMATIONS) val root = FrameLayout(context) shelf = LayoutInflater.from(root.context) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index ba5ba2c31a42..ad7dee33b6d8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -82,7 +82,6 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.footer.ui.view.FooterView; -import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -116,7 +115,6 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { private AmbientState mAmbientState; private TestableResources mTestableResources; @Rule public MockitoRule mockito = MockitoJUnit.rule(); - @Mock private NotificationsController mNotificationsController; @Mock private SysuiStatusBarStateController mBarState; @Mock private GroupMembershipManager mGroupMembershipManger; @Mock private GroupExpansionManager mGroupExpansionManager; @@ -193,7 +191,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { mStackScrollerInternal.initView(getContext(), mNotificationSwipeHelper, mNotificationStackSizeCalculator); mStackScroller = spy(mStackScrollerInternal); - mStackScroller.setNotificationsController(mNotificationsController); + mStackScroller.setResetUserExpandedStatesRunnable(()->{}); mStackScroller.setEmptyShadeView(mEmptyShadeView); when(mStackScrollLayoutController.isHistoryEnabled()).thenReturn(true); when(mStackScrollLayoutController.getNotificationRoundnessManager()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt index 08ef47765174..f266f039958f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt @@ -2,21 +2,28 @@ package com.android.systemui.statusbar.notification.stack import android.annotation.DimenRes import android.content.pm.PackageManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ShadeInterpolation.getContentAlpha import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.res.R import com.android.systemui.shade.transition.LargeScreenShadeInterpolator import com.android.systemui.statusbar.EmptyShadeView import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.notification.RoundableState +import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.footer.ui.view.FooterView import com.android.systemui.statusbar.notification.footer.ui.view.FooterView.FooterViewState import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView +import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.util.mockito.mock import com.google.common.truth.Expect @@ -24,6 +31,7 @@ import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Assume import org.junit.Before import org.junit.Rule @@ -37,22 +45,26 @@ import org.mockito.Mockito.`when` as whenever @SmallTest class StackScrollAlgorithmTest : SysuiTestCase() { - @JvmField @Rule - var expect: Expect = Expect.create() + @JvmField @Rule var expect: Expect = Expect.create() private val largeScreenShadeInterpolator = mock<LargeScreenShadeInterpolator>() private val hostView = FrameLayout(context) private val stackScrollAlgorithm = StackScrollAlgorithm(context, hostView) private val notificationRow = mock<ExpandableNotificationRow>() + private val notificationEntry = mock<NotificationEntry>() private val dumpManager = mock<DumpManager>() + @OptIn(ExperimentalCoroutinesApi::class) private val mStatusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>() private val notificationShelf = mock<NotificationShelf>() - private val emptyShadeView = EmptyShadeView(context, /* attrs= */ null).apply { - layout(/* l= */ 0, /* t= */ 0, /* r= */ 100, /* b= */ 100) - } - private val footerView = FooterView(context, /*attrs=*/null) - private val ambientState = AmbientState( + private val emptyShadeView = + EmptyShadeView(context, /* attrs= */ null).apply { + layout(/* l= */ 0, /* t= */ 0, /* r= */ 100, /* b= */ 100) + } + private val footerView = FooterView(context, /*attrs=*/ null) + @OptIn(ExperimentalCoroutinesApi::class) + private val ambientState = + AmbientState( context, dumpManager, /* sectionProvider */ { _, _ -> false }, @@ -62,13 +74,14 @@ class StackScrollAlgorithmTest : SysuiTestCase() { ) private val testableResources = mContext.getOrCreateTestableResources() + private val featureFlags = mock<FeatureFlagsClassic>() private val maxPanelHeight = mContext.resources.displayMetrics.heightPixels - - px(R.dimen.notification_panel_margin_top) - - px(R.dimen.notification_panel_margin_bottom) + px(R.dimen.notification_panel_margin_top) - + px(R.dimen.notification_panel_margin_bottom) private fun px(@DimenRes id: Int): Float = - testableResources.resources.getDimensionPixelSize(id).toFloat() + testableResources.resources.getDimensionPixelSize(id).toFloat() private val bigGap = px(R.dimen.notification_section_divider_height) private val smallGap = px(R.dimen.notification_section_divider_height_lockscreen) @@ -76,9 +89,12 @@ class StackScrollAlgorithmTest : SysuiTestCase() { @Before fun setUp() { Assume.assumeFalse(isTv()) - + mDependency.injectTestDependency(FeatureFlags::class.java, featureFlags) whenever(notificationShelf.viewState).thenReturn(ExpandableViewState()) whenever(notificationRow.viewState).thenReturn(ExpandableViewState()) + whenever(notificationRow.entry).thenReturn(notificationEntry) + whenever(notificationRow.roundableState) + .thenReturn(RoundableState(notificationRow, notificationRow, 0f)) ambientState.isSmallScreen = true hostView.addView(notificationRow) @@ -92,7 +108,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { fun resetViewStates_defaultHun_yTranslationIsInset() { whenever(notificationRow.isPinned).thenReturn(true) whenever(notificationRow.isHeadsUp).thenReturn(true) - resetViewStates_hunYTranslationIsInset() + resetViewStates_hunYTranslationIs(stackScrollAlgorithm.mHeadsUpInset) } @Test @@ -103,18 +119,87 @@ class StackScrollAlgorithmTest : SysuiTestCase() { } @Test + @DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) fun resetViewStates_hunAnimatingAway_yTranslationIsInset() { whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) - resetViewStates_hunYTranslationIsInset() + resetViewStates_hunYTranslationIs(stackScrollAlgorithm.mHeadsUpInset) } @Test + @DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) fun resetViewStates_hunAnimatingAway_StackMarginChangesHunYTranslation() { whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) resetViewStates_stackMargin_changesHunYTranslation() } @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) + fun resetViewStates_defaultHun_newHeadsUpAnim_yTranslationIsInset() { + whenever(notificationRow.isPinned).thenReturn(true) + whenever(notificationRow.isHeadsUp).thenReturn(true) + resetViewStates_hunYTranslationIs(stackScrollAlgorithm.mHeadsUpInset) + } + + @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) + fun resetViewStates_defaultHunWithStackMargin_newHeadsUpAnim_changesHunYTranslation() { + whenever(notificationRow.isPinned).thenReturn(true) + whenever(notificationRow.isHeadsUp).thenReturn(true) + resetViewStates_stackMargin_changesHunYTranslation() + } + + @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) + fun resetViewStates_defaultHun_showingQS_newHeadsUpAnim_hunTranslatedToMax() { + // Given: the shade is open and scrolled to the bottom to show the QuickSettings + val maxHunTranslation = 2000f + ambientState.maxHeadsUpTranslation = maxHunTranslation + ambientState.setLayoutMinHeight(2500) // Mock the height of shade + ambientState.stackY = 2500f // Scroll over the max translation + stackScrollAlgorithm.setIsExpanded(true) // Mark the shade open + whenever(notificationRow.mustStayOnScreen()).thenReturn(true) + whenever(notificationRow.isHeadsUp).thenReturn(true) + whenever(notificationRow.isAboveShelf).thenReturn(true) + + resetViewStates_hunYTranslationIs(maxHunTranslation) + } + + @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) + fun resetViewStates_hunAnimatingAway_showingQS_newHeadsUpAnim_hunTranslatedToBottomOfScreen() { + // Given: the shade is open and scrolled to the bottom to show the QuickSettings + val bottomOfScreen = 2600f + val maxHunTranslation = 2000f + ambientState.maxHeadsUpTranslation = maxHunTranslation + ambientState.setLayoutMinHeight(2500) // Mock the height of shade + ambientState.stackY = 2500f // Scroll over the max translation + stackScrollAlgorithm.setIsExpanded(true) // Mark the shade open + stackScrollAlgorithm.setHeadsUpAppearHeightBottom(bottomOfScreen.toInt()) + whenever(notificationRow.mustStayOnScreen()).thenReturn(true) + whenever(notificationRow.isHeadsUp).thenReturn(true) + whenever(notificationRow.isAboveShelf).thenReturn(true) + whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) + + resetViewStates_hunYTranslationIs( + expected = bottomOfScreen + stackScrollAlgorithm.mHeadsUpAppearStartAboveScreen + ) + } + + @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) + fun resetViewStates_hunAnimatingAway_newHeadsUpAnim_hunTranslatedToTopOfScreen() { + val topMargin = 100f + ambientState.maxHeadsUpTranslation = 2000f + ambientState.stackTopMargin = topMargin.toInt() + whenever(notificationRow.intrinsicHeight).thenReturn(100) + whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) + + resetViewStates_hunYTranslationIs( + expected = -topMargin - stackScrollAlgorithm.mHeadsUpAppearStartAboveScreen + ) + } + + @Test fun resetViewStates_hunAnimatingAway_bottomNotClipped() { whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) @@ -136,6 +221,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { } @Test + @DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) fun resetViewStates_hunsOverlappingAndBottomHunAnimatingAway_bottomHunClipped() { val topHun = mockExpandableNotificationRow() val bottomHun = mockExpandableNotificationRow() @@ -156,7 +242,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0) val marginBottom = - context.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom) + context.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom) val fullHeight = ambientState.layoutMaxHeight + marginBottom - ambientState.stackY val centeredY = ambientState.stackY + fullHeight / 2f - emptyShadeView.height / 2f assertThat(emptyShadeView.viewState.yTranslation).isEqualTo(centeredY) @@ -174,33 +260,37 @@ class StackScrollAlgorithmTest : SysuiTestCase() { assertThat(notificationRow.viewState.alpha).isEqualTo(1f) } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun resetViewStates_expansionChanging_notificationBecomesTransparent() { whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(false) resetViewStates_expansionChanging_notificationAlphaUpdated( - expansionFraction = 0.25f, - expectedAlpha = 0.0f + expansionFraction = 0.25f, + expectedAlpha = 0.0f ) } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun resetViewStates_expansionChangingWhileBouncerInTransit_viewBecomesTransparent() { whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(true) resetViewStates_expansionChanging_notificationAlphaUpdated( - expansionFraction = 0.85f, - expectedAlpha = 0.0f + expansionFraction = 0.85f, + expectedAlpha = 0.0f ) } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun resetViewStates_expansionChanging_notificationAlphaUpdated() { whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(false) resetViewStates_expansionChanging_notificationAlphaUpdated( - expansionFraction = 0.6f, - expectedAlpha = getContentAlpha(0.6f) + expansionFraction = 0.6f, + expectedAlpha = getContentAlpha(0.6f) ) } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun resetViewStates_largeScreen_expansionChanging_alphaUpdated_largeScreenValue() { val expansionFraction = 0.6f @@ -216,13 +306,14 @@ class StackScrollAlgorithmTest : SysuiTestCase() { ) } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun expansionChanging_largeScreen_bouncerInTransit_alphaUpdated_bouncerValues() { ambientState.isSmallScreen = false whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(true) resetViewStates_expansionChanging_notificationAlphaUpdated( - expansionFraction = 0.95f, - expectedAlpha = aboutToShowBouncerProgress(0.95f), + expansionFraction = 0.95f, + expectedAlpha = aboutToShowBouncerProgress(0.95f), ) } @@ -235,10 +326,8 @@ class StackScrollAlgorithmTest : SysuiTestCase() { stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0) - verify(notificationShelf).updateState( - /* algorithmState= */any(), - /* ambientState= */eq(ambientState) - ) + verify(notificationShelf) + .updateState(/* algorithmState= */ any(), /* ambientState= */ eq(ambientState)) } @Test @@ -397,22 +486,31 @@ class StackScrollAlgorithmTest : SysuiTestCase() { @Test fun getGapForLocation_onLockscreen_returnsSmallGap() { - val gap = stackScrollAlgorithm.getGapForLocation( - /* fractionToShade= */ 0f, /* onKeyguard= */ true) + val gap = + stackScrollAlgorithm.getGapForLocation( + /* fractionToShade= */ 0f, + /* onKeyguard= */ true + ) assertThat(gap).isEqualTo(smallGap) } @Test fun getGapForLocation_goingToShade_interpolatesGap() { - val gap = stackScrollAlgorithm.getGapForLocation( - /* fractionToShade= */ 0.5f, /* onKeyguard= */ true) + val gap = + stackScrollAlgorithm.getGapForLocation( + /* fractionToShade= */ 0.5f, + /* onKeyguard= */ true + ) assertThat(gap).isEqualTo(smallGap * 0.5f + bigGap * 0.5f) } @Test fun getGapForLocation_notOnLockscreen_returnsBigGap() { - val gap = stackScrollAlgorithm.getGapForLocation( - /* fractionToShade= */ 0f, /* onKeyguard= */ false) + val gap = + stackScrollAlgorithm.getGapForLocation( + /* fractionToShade= */ 0f, + /* onKeyguard= */ false + ) assertThat(gap).isEqualTo(bigGap) } @@ -469,12 +567,14 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val expandableViewState = ExpandableViewState() expandableViewState.headsUpIsVisible = false - stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState, - /* isShadeExpanded= */ true, - /* mustStayOnScreen= */ true, - /* isViewEndVisible= */ true, - /* viewEnd= */ 0f, - /* maxHunY= */ 10f) + stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible( + expandableViewState, + /* isShadeExpanded= */ true, + /* mustStayOnScreen= */ true, + /* isViewEndVisible= */ true, + /* viewEnd= */ 0f, + /* maxHunY= */ 10f + ) assertTrue(expandableViewState.headsUpIsVisible) } @@ -484,12 +584,14 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val expandableViewState = ExpandableViewState() expandableViewState.headsUpIsVisible = true - stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState, - /* isShadeExpanded= */ true, - /* mustStayOnScreen= */ true, - /* isViewEndVisible= */ true, - /* viewEnd= */ 10f, - /* maxHunY= */ 0f) + stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible( + expandableViewState, + /* isShadeExpanded= */ true, + /* mustStayOnScreen= */ true, + /* isViewEndVisible= */ true, + /* viewEnd= */ 10f, + /* maxHunY= */ 0f + ) assertFalse(expandableViewState.headsUpIsVisible) } @@ -499,12 +601,14 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val expandableViewState = ExpandableViewState() expandableViewState.headsUpIsVisible = true - stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState, - /* isShadeExpanded= */ false, - /* mustStayOnScreen= */ true, - /* isViewEndVisible= */ true, - /* viewEnd= */ 10f, - /* maxHunY= */ 1f) + stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible( + expandableViewState, + /* isShadeExpanded= */ false, + /* mustStayOnScreen= */ true, + /* isViewEndVisible= */ true, + /* viewEnd= */ 10f, + /* maxHunY= */ 1f + ) assertTrue(expandableViewState.headsUpIsVisible) } @@ -514,12 +618,14 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val expandableViewState = ExpandableViewState() expandableViewState.headsUpIsVisible = true - stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState, - /* isShadeExpanded= */ true, - /* mustStayOnScreen= */ false, - /* isViewEndVisible= */ true, - /* viewEnd= */ 10f, - /* maxHunY= */ 1f) + stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible( + expandableViewState, + /* isShadeExpanded= */ true, + /* mustStayOnScreen= */ false, + /* isViewEndVisible= */ true, + /* viewEnd= */ 10f, + /* maxHunY= */ 1f + ) assertTrue(expandableViewState.headsUpIsVisible) } @@ -529,12 +635,14 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val expandableViewState = ExpandableViewState() expandableViewState.headsUpIsVisible = true - stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState, - /* isShadeExpanded= */ true, - /* mustStayOnScreen= */ true, - /* isViewEndVisible= */ false, - /* viewEnd= */ 10f, - /* maxHunY= */ 1f) + stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible( + expandableViewState, + /* isShadeExpanded= */ true, + /* mustStayOnScreen= */ true, + /* isViewEndVisible= */ false, + /* viewEnd= */ 10f, + /* maxHunY= */ 1f + ) assertTrue(expandableViewState.headsUpIsVisible) } @@ -544,9 +652,12 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val expandableViewState = ExpandableViewState() expandableViewState.yTranslation = 50f - stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f, - /* stackTranslation= */ 0f, - /* collapsedHeight= */ 1f, expandableViewState) + stackScrollAlgorithm.clampHunToTop( + /* quickQsOffsetHeight= */ 10f, + /* stackTranslation= */ 0f, + /* collapsedHeight= */ 1f, + expandableViewState + ) // qqs (10 + 0) < viewY (50) assertEquals(50f, expandableViewState.yTranslation) @@ -557,9 +668,12 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val expandableViewState = ExpandableViewState() expandableViewState.yTranslation = -10f - stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f, - /* stackTranslation= */ 0f, - /* collapsedHeight= */ 1f, expandableViewState) + stackScrollAlgorithm.clampHunToTop( + /* quickQsOffsetHeight= */ 10f, + /* stackTranslation= */ 0f, + /* collapsedHeight= */ 1f, + expandableViewState + ) // qqs (10 + 0) > viewY (-10) assertEquals(10f, expandableViewState.yTranslation) @@ -571,9 +685,12 @@ class StackScrollAlgorithmTest : SysuiTestCase() { expandableViewState.height = 20 expandableViewState.yTranslation = -100f - stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f, - /* stackTranslation= */ 0f, - /* collapsedHeight= */ 10f, expandableViewState) + stackScrollAlgorithm.clampHunToTop( + /* quickQsOffsetHeight= */ 10f, + /* stackTranslation= */ 0f, + /* collapsedHeight= */ 10f, + expandableViewState + ) // newTranslation = max(10, -100) = 10 // distToRealY = 10 - (-100f) = 110 @@ -587,9 +704,12 @@ class StackScrollAlgorithmTest : SysuiTestCase() { expandableViewState.height = 20 expandableViewState.yTranslation = 5f - stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f, - /* stackTranslation= */ 0f, - /* collapsedHeight= */ 10f, expandableViewState) + stackScrollAlgorithm.clampHunToTop( + /* quickQsOffsetHeight= */ 10f, + /* stackTranslation= */ 0f, + /* collapsedHeight= */ 10f, + expandableViewState + ) // newTranslation = max(10, 5) = 10 // distToRealY = 10 - 5 = 5 @@ -599,41 +719,49 @@ class StackScrollAlgorithmTest : SysuiTestCase() { @Test fun computeCornerRoundnessForPinnedHun_stackBelowScreen_round() { - val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun( + val currentRoundness = + stackScrollAlgorithm.computeCornerRoundnessForPinnedHun( /* hostViewHeight= */ 100f, /* stackY= */ 110f, /* viewMaxHeight= */ 20f, - /* originalCornerRoundness= */ 0f) + /* originalCornerRoundness= */ 0f + ) assertEquals(1f, currentRoundness) } @Test fun computeCornerRoundnessForPinnedHun_stackAboveScreenBelowPinPoint_halfRound() { - val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun( + val currentRoundness = + stackScrollAlgorithm.computeCornerRoundnessForPinnedHun( /* hostViewHeight= */ 100f, /* stackY= */ 90f, /* viewMaxHeight= */ 20f, - /* originalCornerRoundness= */ 0f) + /* originalCornerRoundness= */ 0f + ) assertEquals(0.5f, currentRoundness) } @Test fun computeCornerRoundnessForPinnedHun_stackAbovePinPoint_notRound() { - val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun( + val currentRoundness = + stackScrollAlgorithm.computeCornerRoundnessForPinnedHun( /* hostViewHeight= */ 100f, /* stackY= */ 0f, /* viewMaxHeight= */ 20f, - /* originalCornerRoundness= */ 0f) + /* originalCornerRoundness= */ 0f + ) assertEquals(0f, currentRoundness) } @Test fun computeCornerRoundnessForPinnedHun_originallyRoundAndStackAbovePinPoint_round() { - val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun( + val currentRoundness = + stackScrollAlgorithm.computeCornerRoundnessForPinnedHun( /* hostViewHeight= */ 100f, /* stackY= */ 0f, /* viewMaxHeight= */ 20f, - /* originalCornerRoundness= */ 1f) + /* originalCornerRoundness= */ 1f + ) assertEquals(1f, currentRoundness) } @@ -642,23 +770,20 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // Given: shade is opened, yTranslation of HUN is 0, // the height of HUN equals to the height of QQS Panel, // and HUN fully overlaps with QQS Panel - ambientState.stackTranslation = px(R.dimen.qqs_layout_margin_top) + - px(R.dimen.qqs_layout_padding_bottom) - val childHunView = createHunViewMock( - isShadeOpen = true, - fullyVisible = false, - headerVisibleAmount = 1f - ) + ambientState.stackTranslation = + px(R.dimen.qqs_layout_margin_top) + px(R.dimen.qqs_layout_padding_bottom) + val childHunView = + createHunViewMock(isShadeOpen = true, fullyVisible = false, headerVisibleAmount = 1f) val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState() algorithmState.visibleChildren.add(childHunView) // When: updateChildZValue() is called for the top HUN stackScrollAlgorithm.updateChildZValue( - /* i= */ 0, - /* childrenOnTop= */ 0.0f, - /* StackScrollAlgorithmState= */ algorithmState, - /* ambientState= */ ambientState, - /* shouldElevateHun= */ true + /* i= */ 0, + /* childrenOnTop= */ 0.0f, + /* StackScrollAlgorithmState= */ algorithmState, + /* ambientState= */ ambientState, + /* shouldElevateHun= */ true ) // Then: full shadow would be applied @@ -670,13 +795,10 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // Given: shade is opened, yTranslation of HUN is greater than 0, // the height of HUN is equal to the height of QQS Panel, // and HUN partially overlaps with QQS Panel - ambientState.stackTranslation = px(R.dimen.qqs_layout_margin_top) + - px(R.dimen.qqs_layout_padding_bottom) - val childHunView = createHunViewMock( - isShadeOpen = true, - fullyVisible = false, - headerVisibleAmount = 1f - ) + ambientState.stackTranslation = + px(R.dimen.qqs_layout_margin_top) + px(R.dimen.qqs_layout_padding_bottom) + val childHunView = + createHunViewMock(isShadeOpen = true, fullyVisible = false, headerVisibleAmount = 1f) // Use half of the HUN's height as overlap childHunView.viewState.yTranslation = (childHunView.viewState.height + 1 shr 1).toFloat() val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState() @@ -684,17 +806,17 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // When: updateChildZValue() is called for the top HUN stackScrollAlgorithm.updateChildZValue( - /* i= */ 0, - /* childrenOnTop= */ 0.0f, - /* StackScrollAlgorithmState= */ algorithmState, - /* ambientState= */ ambientState, - /* shouldElevateHun= */ true + /* i= */ 0, + /* childrenOnTop= */ 0.0f, + /* StackScrollAlgorithmState= */ algorithmState, + /* ambientState= */ ambientState, + /* shouldElevateHun= */ true ) // Then: HUN should have shadow, but not as full size assertThat(childHunView.viewState.zTranslation).isGreaterThan(0.0f) assertThat(childHunView.viewState.zTranslation) - .isLessThan(px(R.dimen.heads_up_pinned_elevation)) + .isLessThan(px(R.dimen.heads_up_pinned_elevation)) } @Test @@ -702,28 +824,25 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // Given: shade is opened, yTranslation of HUN is equal to QQS Panel's height, // the height of HUN is equal to the height of QQS Panel, // and HUN doesn't overlap with QQS Panel - ambientState.stackTranslation = px(R.dimen.qqs_layout_margin_top) + - px(R.dimen.qqs_layout_padding_bottom) + ambientState.stackTranslation = + px(R.dimen.qqs_layout_margin_top) + px(R.dimen.qqs_layout_padding_bottom) // Mock the height of shade ambientState.setLayoutMinHeight(1000) - val childHunView = createHunViewMock( - isShadeOpen = true, - fullyVisible = true, - headerVisibleAmount = 1f - ) + val childHunView = + createHunViewMock(isShadeOpen = true, fullyVisible = true, headerVisibleAmount = 1f) // HUN doesn't overlap with QQS Panel - childHunView.viewState.yTranslation = ambientState.topPadding + - ambientState.stackTranslation + childHunView.viewState.yTranslation = + ambientState.topPadding + ambientState.stackTranslation val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState() algorithmState.visibleChildren.add(childHunView) // When: updateChildZValue() is called for the top HUN stackScrollAlgorithm.updateChildZValue( - /* i= */ 0, - /* childrenOnTop= */ 0.0f, - /* StackScrollAlgorithmState= */ algorithmState, - /* ambientState= */ ambientState, - /* shouldElevateHun= */ true + /* i= */ 0, + /* childrenOnTop= */ 0.0f, + /* StackScrollAlgorithmState= */ algorithmState, + /* ambientState= */ ambientState, + /* shouldElevateHun= */ true ) // Then: HUN should not have shadow @@ -737,11 +856,8 @@ class StackScrollAlgorithmTest : SysuiTestCase() { ambientState.stackTranslation = -ambientState.topPadding // Mock the height of shade ambientState.setLayoutMinHeight(1000) - val childHunView = createHunViewMock( - isShadeOpen = false, - fullyVisible = false, - headerVisibleAmount = 0f - ) + val childHunView = + createHunViewMock(isShadeOpen = false, fullyVisible = false, headerVisibleAmount = 0f) childHunView.viewState.yTranslation = 0f // Shade is closed, thus childHunView's headerVisibleAmount is 0 childHunView.headerVisibleAmount = 0f @@ -750,11 +866,11 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // When: updateChildZValue() is called for the top HUN stackScrollAlgorithm.updateChildZValue( - /* i= */ 0, - /* childrenOnTop= */ 0.0f, - /* StackScrollAlgorithmState= */ algorithmState, - /* ambientState= */ ambientState, - /* shouldElevateHun= */ true + /* i= */ 0, + /* childrenOnTop= */ 0.0f, + /* StackScrollAlgorithmState= */ algorithmState, + /* ambientState= */ ambientState, + /* shouldElevateHun= */ true ) // Then: HUN should have full shadow @@ -768,11 +884,8 @@ class StackScrollAlgorithmTest : SysuiTestCase() { ambientState.stackTranslation = -ambientState.topPadding // Mock the height of shade ambientState.setLayoutMinHeight(1000) - val childHunView = createHunViewMock( - isShadeOpen = false, - fullyVisible = false, - headerVisibleAmount = 0.5f - ) + val childHunView = + createHunViewMock(isShadeOpen = false, fullyVisible = false, headerVisibleAmount = 0.5f) childHunView.viewState.yTranslation = 0f // Shade is being opened, thus childHunView's headerVisibleAmount is between 0 and 1 // use 0.5 as headerVisibleAmount here @@ -782,17 +895,17 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // When: updateChildZValue() is called for the top HUN stackScrollAlgorithm.updateChildZValue( - /* i= */ 0, - /* childrenOnTop= */ 0.0f, - /* StackScrollAlgorithmState= */ algorithmState, - /* ambientState= */ ambientState, - /* shouldElevateHun= */ true + /* i= */ 0, + /* childrenOnTop= */ 0.0f, + /* StackScrollAlgorithmState= */ algorithmState, + /* ambientState= */ ambientState, + /* shouldElevateHun= */ true ) // Then: HUN should have shadow, but not as full size assertThat(childHunView.viewState.zTranslation).isGreaterThan(0.0f) assertThat(childHunView.viewState.zTranslation) - .isLessThan(px(R.dimen.heads_up_pinned_elevation)) + .isLessThan(px(R.dimen.heads_up_pinned_elevation)) } @Test @@ -862,134 +975,174 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // stackScrollAlgorithm.resetViewStates is called. ambientState.dozeAmount = 0.5f setExpansionFractionWithoutShelfDuringAodToLockScreen( - ambientState, - algorithmState, - fraction = 0.5f + ambientState, + algorithmState, + fraction = 0.5f ) stackScrollAlgorithm.resetViewStates(ambientState, 0) // Then: pulsingNotificationView should show at full height assertEquals( - stackScrollAlgorithm.getMaxAllowedChildHeight(pulsingNotificationView), - pulsingNotificationView.viewState.height + stackScrollAlgorithm.getMaxAllowedChildHeight(pulsingNotificationView), + pulsingNotificationView.viewState.height ) // After: reset dozeAmount and expansionFraction ambientState.dozeAmount = 0f setExpansionFractionWithoutShelfDuringAodToLockScreen( - ambientState, - algorithmState, - fraction = 1f + ambientState, + algorithmState, + fraction = 1f ) } // region shouldPinHunToBottomOfExpandedQs @Test fun shouldHunBeVisibleWhenScrolled_mustStayOnScreenFalse_false() { - assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( - /* mustStayOnScreen= */false, - /* headsUpIsVisible= */false, - /* showingPulsing= */false, - /* isOnKeyguard=*/false, - /*headsUpOnKeyguard=*/false - )).isFalse() + assertThat( + stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( + /* mustStayOnScreen= */ false, + /* headsUpIsVisible= */ false, + /* showingPulsing= */ false, + /* isOnKeyguard=*/ false, + /*headsUpOnKeyguard=*/ false + ) + ) + .isFalse() } @Test fun shouldPinHunToBottomOfExpandedQs_headsUpIsVisible_false() { - assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( - /* mustStayOnScreen= */true, - /* headsUpIsVisible= */true, - /* showingPulsing= */false, - /* isOnKeyguard=*/false, - /*headsUpOnKeyguard=*/false - )).isFalse() + assertThat( + stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( + /* mustStayOnScreen= */ true, + /* headsUpIsVisible= */ true, + /* showingPulsing= */ false, + /* isOnKeyguard=*/ false, + /*headsUpOnKeyguard=*/ false + ) + ) + .isFalse() } @Test fun shouldHunBeVisibleWhenScrolled_showingPulsing_false() { - assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( - /* mustStayOnScreen= */true, - /* headsUpIsVisible= */false, - /* showingPulsing= */true, - /* isOnKeyguard=*/false, - /* headsUpOnKeyguard= */false - )).isFalse() + assertThat( + stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( + /* mustStayOnScreen= */ true, + /* headsUpIsVisible= */ false, + /* showingPulsing= */ true, + /* isOnKeyguard=*/ false, + /* headsUpOnKeyguard= */ false + ) + ) + .isFalse() } @Test fun shouldHunBeVisibleWhenScrolled_isOnKeyguard_false() { - assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( - /* mustStayOnScreen= */true, - /* headsUpIsVisible= */false, - /* showingPulsing= */false, - /* isOnKeyguard=*/true, - /* headsUpOnKeyguard= */false - )).isFalse() + assertThat( + stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( + /* mustStayOnScreen= */ true, + /* headsUpIsVisible= */ false, + /* showingPulsing= */ false, + /* isOnKeyguard=*/ true, + /* headsUpOnKeyguard= */ false + ) + ) + .isFalse() } @Test fun shouldHunBeVisibleWhenScrolled_isNotOnKeyguard_true() { - assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( - /* mustStayOnScreen= */true, - /* headsUpIsVisible= */false, - /* showingPulsing= */false, - /* isOnKeyguard=*/false, - /* headsUpOnKeyguard= */false - )).isTrue() + assertThat( + stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( + /* mustStayOnScreen= */ true, + /* headsUpIsVisible= */ false, + /* showingPulsing= */ false, + /* isOnKeyguard=*/ false, + /* headsUpOnKeyguard= */ false + ) + ) + .isTrue() } @Test fun shouldHunBeVisibleWhenScrolled_headsUpOnKeyguard_true() { - assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( - /* mustStayOnScreen= */true, - /* headsUpIsVisible= */false, - /* showingPulsing= */false, - /* isOnKeyguard=*/true, - /* headsUpOnKeyguard= */true - )).isTrue() + assertThat( + stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( + /* mustStayOnScreen= */ true, + /* headsUpIsVisible= */ false, + /* showingPulsing= */ false, + /* isOnKeyguard=*/ true, + /* headsUpOnKeyguard= */ true + ) + ) + .isTrue() } - // endregion - private fun createHunViewMock( - isShadeOpen: Boolean, - fullyVisible: Boolean, - headerVisibleAmount: Float - ) = - mock<ExpandableNotificationRow>().apply { - val childViewStateMock = createHunChildViewState(isShadeOpen, fullyVisible) - whenever(this.viewState).thenReturn(childViewStateMock) - - whenever(this.mustStayOnScreen()).thenReturn(true) - whenever(this.headerVisibleAmount).thenReturn(headerVisibleAmount) + @Test + fun shouldHunAppearFromBottom_hunAtMaxHunTranslation() { + ambientState.maxHeadsUpTranslation = 400f + val viewState = + ExpandableViewState().apply { + height = 100 + yTranslation = ambientState.maxHeadsUpTranslation - height // move it to the max } + assertTrue(stackScrollAlgorithm.shouldHunAppearFromBottom(ambientState, viewState)) + } - private fun createHunChildViewState(isShadeOpen: Boolean, fullyVisible: Boolean) = + @Test + fun shouldHunAppearFromBottom_hunBelowMaxHunTranslation() { + ambientState.maxHeadsUpTranslation = 400f + val viewState = ExpandableViewState().apply { - // Mock the HUN's height with ambientState.topPadding + - // ambientState.stackTranslation - height = (ambientState.topPadding + ambientState.stackTranslation).toInt() - if (isShadeOpen && fullyVisible) { - yTranslation = - ambientState.topPadding + ambientState.stackTranslation - } else { - yTranslation = 0f - } - headsUpIsVisible = fullyVisible + height = 100 + yTranslation = + ambientState.maxHeadsUpTranslation - height - 1 // move it below the max } - private fun createPulsingViewMock( + assertFalse(stackScrollAlgorithm.shouldHunAppearFromBottom(ambientState, viewState)) + } + // endregion + + private fun createHunViewMock( + isShadeOpen: Boolean, + fullyVisible: Boolean, + headerVisibleAmount: Float ) = - mock<ExpandableNotificationRow>().apply { - whenever(this.viewState).thenReturn(ExpandableViewState()) - whenever(this.showingPulsing()).thenReturn(true) + mock<ExpandableNotificationRow>().apply { + val childViewStateMock = createHunChildViewState(isShadeOpen, fullyVisible) + whenever(this.viewState).thenReturn(childViewStateMock) + + whenever(this.mustStayOnScreen()).thenReturn(true) + whenever(this.headerVisibleAmount).thenReturn(headerVisibleAmount) + } + + private fun createHunChildViewState(isShadeOpen: Boolean, fullyVisible: Boolean) = + ExpandableViewState().apply { + // Mock the HUN's height with ambientState.topPadding + + // ambientState.stackTranslation + height = (ambientState.topPadding + ambientState.stackTranslation).toInt() + if (isShadeOpen && fullyVisible) { + yTranslation = ambientState.topPadding + ambientState.stackTranslation + } else { + yTranslation = 0f } + headsUpIsVisible = fullyVisible + } + + private fun createPulsingViewMock() = + mock<ExpandableNotificationRow>().apply { + whenever(this.viewState).thenReturn(ExpandableViewState()) + whenever(this.showingPulsing()).thenReturn(true) + } private fun setExpansionFractionWithoutShelfDuringAodToLockScreen( - ambientState: AmbientState, - algorithmState: StackScrollAlgorithm.StackScrollAlgorithmState, - fraction: Float + ambientState: AmbientState, + algorithmState: StackScrollAlgorithm.StackScrollAlgorithmState, + fraction: Float ) { // showingShelf: false algorithmState.firstViewInShelf = null @@ -1002,11 +1155,10 @@ class StackScrollAlgorithmTest : SysuiTestCase() { ambientState.stackHeight = ambientState.stackEndHeight * fraction } - private fun resetViewStates_hunYTranslationIsInset() { + private fun resetViewStates_hunYTranslationIs(expected: Float) { stackScrollAlgorithm.resetViewStates(ambientState, 0) - assertThat(notificationRow.viewState.yTranslation) - .isEqualTo(stackScrollAlgorithm.mHeadsUpInset) + assertThat(notificationRow.viewState.yTranslation).isEqualTo(expected) } private fun resetViewStates_stackMargin_changesHunYTranslation() { @@ -1025,13 +1177,13 @@ class StackScrollAlgorithmTest : SysuiTestCase() { } private fun resetViewStates_hunsOverlapping_bottomHunClipped( - topHun: ExpandableNotificationRow, - bottomHun: ExpandableNotificationRow + topHun: ExpandableNotificationRow, + bottomHun: ExpandableNotificationRow ) { - val topHunHeight = mContext.resources.getDimensionPixelSize( - R.dimen.notification_content_min_height) - val bottomHunHeight = mContext.resources.getDimensionPixelSize( - R.dimen.notification_max_heads_up_height) + val topHunHeight = + mContext.resources.getDimensionPixelSize(R.dimen.notification_content_min_height) + val bottomHunHeight = + mContext.resources.getDimensionPixelSize(R.dimen.notification_max_heads_up_height) whenever(topHun.intrinsicHeight).thenReturn(topHunHeight) whenever(bottomHun.intrinsicHeight).thenReturn(bottomHunHeight) @@ -1054,8 +1206,8 @@ class StackScrollAlgorithmTest : SysuiTestCase() { } private fun resetViewStates_expansionChanging_notificationAlphaUpdated( - expansionFraction: Float, - expectedAlpha: Float, + expansionFraction: Float, + expectedAlpha: Float, ) { ambientState.isExpansionChanging = true ambientState.expansionFraction = expansionFraction diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt new file mode 100644 index 000000000000..5a5703512a39 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.statusbar.notification.stack + +import android.platform.test.annotations.EnableFlags +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.row.ExpandableView +import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent +import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_HEADS_UP_APPEAR +import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_HEADS_UP_DISAPPEAR +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mockito.any +import org.mockito.Mockito.description +import org.mockito.Mockito.eq +import org.mockito.Mockito.verify + +private const val VIEW_HEIGHT = 100 + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class StackStateAnimatorTest : SysuiTestCase() { + + private lateinit var stackStateAnimator: StackStateAnimator + private val stackScroller: NotificationStackScrollLayout = mock() + private val view: ExpandableView = mock() + private val viewState: ExpandableViewState = + ExpandableViewState().apply { height = VIEW_HEIGHT } + private val runnableCaptor: ArgumentCaptor<Runnable> = argumentCaptor() + @Before + fun setUp() { + whenever(stackScroller.context).thenReturn(context) + whenever(view.viewState).thenReturn(viewState) + stackStateAnimator = StackStateAnimator(stackScroller) + } + + @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) + fun startAnimationForEvents_headsUpFromTop_startsHeadsUpAppearAnim() { + val topMargin = 50f + val expectedStartY = -topMargin - stackStateAnimator.mHeadsUpAppearStartAboveScreen + val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) + stackStateAnimator.setStackTopMargin(topMargin.toInt()) + + stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0) + + verify(view).setActualHeight(VIEW_HEIGHT, false) + verify(view, description("should animate from the top")).translationY = expectedStartY + verify(view) + .performAddAnimation( + /* delay= */ 0L, + /* duration= */ ANIMATION_DURATION_HEADS_UP_APPEAR.toLong(), + /* isHeadsUpAppear= */ true, + /* onEndRunnable= */ null + ) + } + + @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) + fun startAnimationForEvents_headsUpFromBottom_startsHeadsUpAppearAnim() { + val screenHeight = 2000f + val expectedStartY = screenHeight + stackStateAnimator.mHeadsUpAppearStartAboveScreen + val event = + AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR).apply { + headsUpFromBottom = true + } + stackStateAnimator.setHeadsUpAppearHeightBottom(screenHeight.toInt()) + + stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0) + + verify(view).setActualHeight(VIEW_HEIGHT, false) + verify(view, description("should animate from the bottom")).translationY = expectedStartY + verify(view) + .performAddAnimation( + /* delay= */ 0L, + /* duration= */ ANIMATION_DURATION_HEADS_UP_APPEAR.toLong(), + /* isHeadsUpAppear= */ true, + /* onEndRunnable= */ null + ) + } + + @Test + fun startAnimationForEvents_startsHeadsUpDisappearAnim() { + val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR) + stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0) + + verify(view) + .performRemoveAnimation( + /* duration= */ eq(ANIMATION_DURATION_HEADS_UP_DISAPPEAR.toLong()), + /* delay= */ eq(0L), + /* translationDirection= */ eq(0f), + /* isHeadsUpAnimation= */ eq(true), + /* onStartedRunnable= */ any(), + /* onFinishedRunnable= */ runnableCaptor.capture(), + /* animationListener= */ any() + ) + + runnableCaptor.value.run() // execute the end runnable + + verify(view, description("should be called at the end of the animation")) + .removeFromTransientContainer() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt index da543d4454b8..cd6bb5f4966a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt @@ -17,10 +17,10 @@ package com.android.systemui.statusbar.notification.stack import android.testing.AndroidTestingRunner -import android.util.Log -import android.util.Log.TerribleFailureHandler import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.log.assertDoesNotLogWtf +import com.android.systemui.log.assertLogsWtf import kotlin.math.log2 import kotlin.math.sqrt import org.junit.Assert @@ -32,61 +32,36 @@ import org.junit.runner.RunWith class ViewStateTest : SysuiTestCase() { private val viewState = ViewState() - private var wtfHandler: TerribleFailureHandler? = null - private var wtfCount = 0 - @Suppress("DIVISION_BY_ZERO") @Test fun testWtfs() { - interceptWtfs() - // Setting valid values doesn't cause any wtfs. - viewState.alpha = 0.1f - viewState.xTranslation = 0f - viewState.yTranslation = 10f - viewState.zTranslation = 20f - viewState.scaleX = 0.5f - viewState.scaleY = 0.25f - - expectWtfs(0) + assertDoesNotLogWtf { + viewState.alpha = 0.1f + viewState.xTranslation = 0f + viewState.yTranslation = 10f + viewState.zTranslation = 20f + viewState.scaleX = 0.5f + viewState.scaleY = 0.25f + } // Setting NaN values leads to wtfs being logged, and the value not being changed. - viewState.alpha = 0.0f / 0.0f - expectWtfs(1) + assertLogsWtf { viewState.alpha = 0.0f / 0.0f } Assert.assertEquals(viewState.alpha, 0.1f) - viewState.xTranslation = Float.NaN - expectWtfs(2) + assertLogsWtf { viewState.xTranslation = Float.NaN } Assert.assertEquals(viewState.xTranslation, 0f) - viewState.yTranslation = log2(-10.0).toFloat() - expectWtfs(3) + assertLogsWtf { viewState.yTranslation = log2(-10.0).toFloat() } Assert.assertEquals(viewState.yTranslation, 10f) - viewState.zTranslation = sqrt(-1.0).toFloat() - expectWtfs(4) + assertLogsWtf { viewState.zTranslation = sqrt(-1.0).toFloat() } Assert.assertEquals(viewState.zTranslation, 20f) - viewState.scaleX = Float.POSITIVE_INFINITY + Float.NEGATIVE_INFINITY - expectWtfs(5) + assertLogsWtf { viewState.scaleX = Float.POSITIVE_INFINITY + Float.NEGATIVE_INFINITY } Assert.assertEquals(viewState.scaleX, 0.5f) - viewState.scaleY = Float.POSITIVE_INFINITY * 0 - expectWtfs(6) + assertLogsWtf { viewState.scaleY = Float.POSITIVE_INFINITY * 0 } Assert.assertEquals(viewState.scaleY, 0.25f) } - - private fun interceptWtfs() { - wtfCount = 0 - wtfHandler = - Log.setWtfHandler { _: String?, e: Log.TerribleFailure, _: Boolean -> - Log.e("ViewStateTest", "Observed WTF: $e") - wtfCount++ - } - } - - private fun expectWtfs(expectedWtfCount: Int) { - Assert.assertNotNull(wtfHandler) - Assert.assertEquals(expectedWtfCount, wtfCount) - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index 1cc611c2df87..14751c2dc29e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -43,7 +43,6 @@ import androidx.test.filters.SmallTest; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.SysuiBaseFragmentTest; import com.android.systemui.animation.AnimatorTestRule; -import com.android.systemui.common.ui.ConfigurationState; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; import com.android.systemui.log.LogBuffer; @@ -57,9 +56,7 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.OperatorNameViewController; import com.android.systemui.statusbar.disableflags.DisableFlagsLogger; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker; -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarNotificationIconViewStore; -import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel; +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.phone.NotificationIconAreaController; import com.android.systemui.statusbar.phone.StatusBarHideIconsForBouncerManager; @@ -70,7 +67,6 @@ import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeCollapsedStatusBarViewBinder; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeCollapsedStatusBarViewModel; import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.statusbar.ui.SystemBarUtilsState; import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.statusbar.window.StatusBarWindowStateListener; import com.android.systemui.util.CarrierConfigTracker; @@ -702,7 +698,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { mKeyguardStateController, mShadeViewController, mStatusBarStateController, - mock(StatusBarIconViewBindingFailureTracker.class), + mock(NotificationIconContainerStatusBarViewBinder.class), mCommandQueue, mCarrierConfigTracker, new CollapsedStatusBarFragmentLogger( @@ -715,10 +711,6 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { mDumpManager, mStatusBarWindowStateController, mKeyguardUpdateMonitor, - mock(NotificationIconContainerStatusBarViewModel.class), - mock(ConfigurationState.class), - mock(SystemBarUtilsState.class), - mock(StatusBarNotificationIconViewStore.class), mock(DemoModeController.class)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index 0f779d977c71..44fa13283991 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -16,7 +16,8 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel -import android.platform.test.flag.junit.SetFlagsRule +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH @@ -73,8 +74,6 @@ import org.mockito.MockitoAnnotations class MobileIconViewModelTest : SysuiTestCase() { private var connectivityRepository = FakeConnectivityRepository() - private val setFlagsRule = SetFlagsRule() - private lateinit var underTest: MobileIconViewModel private lateinit var interactor: MobileIconInteractorImpl private lateinit var iconsInteractor: MobileIconsInteractorImpl @@ -561,11 +560,9 @@ class MobileIconViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS) fun dataActivity_configOn_testIndicators_staticFlagOff() = testScope.runTest { - // GIVEN STATUS_BAR_STATIC_NETWORK_INDICATORS flag is off - setFlagsRule.disableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS) - // Create a new view model here so the constants are properly read whenever(constants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() @@ -618,11 +615,9 @@ class MobileIconViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS) fun dataActivity_configOn_testIndicators_staticFlagOn() = testScope.runTest { - // GIVEN STATUS_BAR_STATIC_NETWORK_INDICATORS flag is on - setFlagsRule.enableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS) - // Create a new view model here so the constants are properly read whenever(constants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt index bf851eb69a0d..6714c94b017c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt @@ -1115,6 +1115,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { broadcastDispatcher = fakeBroadcastDispatcher, keyguardUpdateMonitor = keyguardUpdateMonitor, backgroundDispatcher = utils.testDispatcher, + mainDispatcher = utils.testDispatcher, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, guestUserInteractor = diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt index d1870b1d8fcd..21d4549c904d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt @@ -258,6 +258,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { broadcastDispatcher = fakeBroadcastDispatcher, keyguardUpdateMonitor = keyguardUpdateMonitor, backgroundDispatcher = testDispatcher, + mainDispatcher = testDispatcher, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, guestUserInteractor = guestUserInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index b7b24f6dc7dd..d0804be81072 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -170,6 +170,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { broadcastDispatcher = fakeBroadcastDispatcher, keyguardUpdateMonitor = keyguardUpdateMonitor, backgroundDispatcher = testDispatcher, + mainDispatcher = testDispatcher, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, guestUserInteractor = guestUserInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java deleted file mode 100644 index a2b016f22473..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2017 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.systemui.util.leak; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.testing.AndroidTestingRunner; - -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.util.concurrency.FakeExecutor; -import com.android.systemui.util.concurrency.MessageRouterImpl; -import com.android.systemui.util.time.FakeSystemClock; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -public class GarbageMonitorTest extends SysuiTestCase { - - @Mock private LeakReporter mLeakReporter; - @Mock private TrackedGarbage mTrackedGarbage; - @Mock private DumpManager mDumpManager; - private GarbageMonitor mGarbageMonitor; - private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - mGarbageMonitor = - new GarbageMonitor( - mContext, - mFakeExecutor, - new MessageRouterImpl(mFakeExecutor), - new LeakDetector(null, mTrackedGarbage, null, mDumpManager), - mLeakReporter, - mDumpManager); - } - - @Test - public void testALittleGarbage_doesntDump() { - when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE); - - mGarbageMonitor.reinspectGarbageAfterGc(); - - verify(mLeakReporter, never()).dumpLeak(anyInt()); - } - - @Test - public void testTransientGarbage_doesntDump() { - when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1); - - // Start the leak monitor. Nothing gets reported immediately. - mGarbageMonitor.startLeakMonitor(); - mFakeExecutor.runAllReady(); - verify(mLeakReporter, never()).dumpLeak(anyInt()); - - // Garbage gets reset to 0 before the leak reporte actually gets called. - when(mTrackedGarbage.countOldGarbage()).thenReturn(0); - mFakeExecutor.advanceClockToLast(); - mFakeExecutor.runAllReady(); - - // Therefore nothing gets dumped. - verify(mLeakReporter, never()).dumpLeak(anyInt()); - } - - @Test - public void testLotsOfPersistentGarbage_dumps() { - when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1); - - mGarbageMonitor.reinspectGarbageAfterGc(); - - verify(mLeakReporter).dumpLeak(GarbageMonitor.GARBAGE_ALLOWANCE + 1); - } - - @Test - public void testLotsOfPersistentGarbage_dumpsAfterAtime() { - when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1); - - // Start the leak monitor. Nothing gets reported immediately. - mGarbageMonitor.startLeakMonitor(); - mFakeExecutor.runAllReady(); - verify(mLeakReporter, never()).dumpLeak(anyInt()); - - mFakeExecutor.advanceClockToLast(); - mFakeExecutor.runAllReady(); - - verify(mLeakReporter).dumpLeak(GarbageMonitor.GARBAGE_ALLOWANCE + 1); - } -}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt index e59e4759fc7b..78016840c3fb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt @@ -28,8 +28,8 @@ import androidx.test.filters.SmallTest import com.android.systemui.model.SysUiStateTest import com.android.wm.shell.bubbles.Bubble import com.android.wm.shell.bubbles.BubbleEducationController -import com.android.wm.shell.bubbles.PREF_MANAGED_EDUCATION -import com.android.wm.shell.bubbles.PREF_STACK_EDUCATION +import com.android.wm.shell.bubbles.ManageEducationView.Companion.PREF_MANAGED_EDUCATION +import com.android.wm.shell.bubbles.StackEducationView.Companion.PREF_STACK_EDUCATION import com.google.common.truth.Truth.assertThat import com.google.common.util.concurrent.MoreExecutors.directExecutor import org.junit.Assert.assertEquals diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index b217195000b4..814ea19a7dcb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -29,8 +29,6 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.google.common.truth.Truth.assertThat; -import static kotlinx.coroutines.flow.FlowKt.emptyFlow; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -50,6 +48,8 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static kotlinx.coroutines.flow.FlowKt.emptyFlow; + import android.app.ActivityManager; import android.app.IActivityManager; import android.app.INotificationManager; @@ -186,7 +186,7 @@ import com.android.wm.shell.bubbles.BubbleStackView; import com.android.wm.shell.bubbles.BubbleViewInfoTask; import com.android.wm.shell.bubbles.BubbleViewProvider; import com.android.wm.shell.bubbles.Bubbles; -import com.android.wm.shell.bubbles.StackEducationViewKt; +import com.android.wm.shell.bubbles.StackEducationView; import com.android.wm.shell.bubbles.properties.BubbleProperties; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.FloatingContentCoordinator; @@ -1930,7 +1930,7 @@ public class BubblesTest extends SysuiTestCase { @Test public void testShowStackEdu_isNotConversationBubble() { // Setup - setPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION, false); + setPrefBoolean(StackEducationView.PREF_STACK_EDUCATION, false); BubbleEntry bubbleEntry = createBubbleEntry(false /* isConversation */); mBubbleController.updateBubble(bubbleEntry); assertTrue(mBubbleController.hasBubbles()); @@ -1948,7 +1948,7 @@ public class BubblesTest extends SysuiTestCase { @Test public void testShowStackEdu_isConversationBubble() { // Setup - setPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION, false); + setPrefBoolean(StackEducationView.PREF_STACK_EDUCATION, false); BubbleEntry bubbleEntry = createBubbleEntry(true /* isConversation */); mBubbleController.updateBubble(bubbleEntry); assertTrue(mBubbleController.hasBubbles()); @@ -1966,7 +1966,7 @@ public class BubblesTest extends SysuiTestCase { @Test public void testShowStackEdu_isSeenConversationBubble() { // Setup - setPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION, true); + setPrefBoolean(StackEducationView.PREF_STACK_EDUCATION, true); BubbleEntry bubbleEntry = createBubbleEntry(true /* isConversation */); mBubbleController.updateBubble(bubbleEntry); assertTrue(mBubbleController.hasBubbles()); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryKosmos.kt index a464fa80d629..fa7958041641 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryKosmos.kt @@ -16,10 +16,8 @@ package com.android.systemui.accessibility.data.repository -import android.view.accessibility.accessibilityManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -val Kosmos.accessibilityRepository by Fixture { - AccessibilityRepository.invoke(a11yManager = accessibilityManager) -} +val Kosmos.fakeAccessibilityRepository by Fixture { FakeAccessibilityRepository() } +val Kosmos.accessibilityRepository by Fixture { fakeAccessibilityRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/ShelfNotificationIconViewStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/AuthControllerKosmos.kt index f7f16a4671f9..7f707850e3dd 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/ShelfNotificationIconViewStoreKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/AuthControllerKosmos.kt @@ -14,12 +14,10 @@ * limitations under the License. */ -package com.android.systemui.statusbar.notification.icon.ui.viewbinder +package com.android.systemui.biometrics import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.statusbar.notification.stack.ui.viewbinder.notifCollection +import com.android.systemui.util.mockito.mock -val Kosmos.shelfNotificationIconViewStore by Fixture { - ShelfNotificationIconViewStore(notifCollection = notifCollection) -} +var Kosmos.authController by Fixture { mock<AuthController>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorKosmos.kt new file mode 100644 index 000000000000..cbfc7686a896 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorKosmos.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.biometrics.domain.interactor + +import android.content.applicationContext +import com.android.systemui.biometrics.authController +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.user.domain.interactor.selectedUserInteractor + +val Kosmos.udfpsOverlayInteractor by Fixture { + UdfpsOverlayInteractor( + context = applicationContext, + authController = authController, + selectedUserInteractor = selectedUserInteractor, + scope = applicationCoroutineScope, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt new file mode 100644 index 000000000000..9175f093c171 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.deviceentry.data.ui.viewmodel + +import com.android.systemui.accessibility.domain.interactor.accessibilityInteractor +import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor +import com.android.systemui.deviceentry.ui.viewmodel.UdfpsAccessibilityOverlayViewModel +import com.android.systemui.keyguard.ui.viewmodel.deviceEntryForegroundIconViewModel +import com.android.systemui.keyguard.ui.viewmodel.deviceEntryIconViewModel +import com.android.systemui.kosmos.Kosmos + +val Kosmos.udfpsAccessibilityOverlayViewModel by + Kosmos.Fixture { + UdfpsAccessibilityOverlayViewModel( + udfpsOverlayInteractor = udfpsOverlayInteractor, + accessibilityInteractor = accessibilityInteractor, + deviceEntryIconViewModel = deviceEntryIconViewModel, + deviceEntryFgIconViewModel = deviceEntryForegroundIconViewModel, + ) + } diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/SetFlagsRuleExtensions.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SetFlagsRuleExtensions.kt index f4f05d85540e..f4f05d85540e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/flags/SetFlagsRuleExtensions.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SetFlagsRuleExtensions.kt diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryFgIconViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryFgIconViewModelKosmos.kt new file mode 100644 index 000000000000..4bfe4f571b05 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryFgIconViewModelKosmos.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor +import com.android.systemui.common.ui.data.repository.configurationRepository +import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +val Kosmos.deviceEntryForegroundIconViewModel by Fixture { + DeviceEntryForegroundViewModel( + context = applicationContext, + configurationRepository = configurationRepository, + deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor, + transitionInteractor = keyguardTransitionInteractor, + deviceEntryIconViewModel = deviceEntryIconViewModel, + udfpsOverlayInteractor = udfpsOverlayInteractor, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt index 67e9289f5d92..5ceefde32d2a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt @@ -28,8 +28,10 @@ import com.android.systemui.scene.shared.flag.sceneContainerFlags import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager +val Kosmos.fakeDeviceEntryIconViewModelTransition by Fixture { FakeDeviceEntryIconTransition() } + val Kosmos.deviceEntryIconViewModelTransitionsMock by Fixture { - mutableSetOf<DeviceEntryIconTransition>() + setOf<DeviceEntryIconTransition>(fakeDeviceEntryIconViewModelTransition) } val Kosmos.deviceEntryIconViewModel by Fixture { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardInternalViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/FakeDeviceEntryIconTransition.kt index d894a1139eeb..6d872a3d8028 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardInternalViewModel.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/FakeDeviceEntryIconTransition.kt @@ -16,11 +16,16 @@ package com.android.systemui.keyguard.ui.viewmodel -import com.android.systemui.biometrics.UdfpsKeyguardAccessibilityDelegate -import javax.inject.Inject -import kotlinx.coroutines.ExperimentalCoroutinesApi +import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow -@ExperimentalCoroutinesApi -class UdfpsKeyguardInternalViewModel -@Inject -constructor(val accessibilityDelegate: UdfpsKeyguardAccessibilityDelegate) +class FakeDeviceEntryIconTransition : DeviceEntryIconTransition { + private val _deviceEntryParentViewAlpha: MutableStateFlow<Float> = MutableStateFlow(0f) + override val deviceEntryParentViewAlpha: Flow<Float> = _deviceEntryParentViewAlpha.asStateFlow() + + fun setDeviceEntryParentViewAlpha(alpha: Float) { + _deviceEntryParentViewAlpha.value = alpha + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt index 6ccb3bc2812e..5e67182d7353 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt @@ -20,6 +20,29 @@ import android.util.Log import android.util.Log.TerribleFailureHandler import junit.framework.Assert +/** Asserts that the given block does not make a call to Log.wtf */ +fun assertDoesNotLogWtf( + message: String = "Expected Log.wtf not to be called", + notLoggingBlock: () -> Unit, +) { + var caught: TerribleFailureLog? = null + val newHandler = TerribleFailureHandler { tag, failure, system -> + caught = TerribleFailureLog(tag, failure, system) + } + val oldHandler = Log.setWtfHandler(newHandler) + try { + notLoggingBlock() + } finally { + Log.setWtfHandler(oldHandler) + } + caught?.let { throw AssertionError("$message: $it", it.failure) } +} + +fun assertDoesNotLogWtf( + message: String = "Expected Log.wtf not to be called", + notLoggingRunnable: Runnable, +) = assertDoesNotLogWtf(message = message) { notLoggingRunnable.run() } + /** * Assert that the given block makes a call to Log.wtf * diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt new file mode 100644 index 000000000000..d70524840145 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.qs.tiles.impl.custom + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.qs.external.FakeCustomTileStatePersister +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileDefaultsRepository +import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTilePackageUpdatesRepository +import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileRepository +import com.android.systemui.qs.tiles.impl.custom.data.repository.FakePackageManagerAdapterFacade + +var Kosmos.tileSpec: TileSpec.CustomTileSpec by Kosmos.Fixture() + +val Kosmos.customTileStatePersister: FakeCustomTileStatePersister by + Kosmos.Fixture { FakeCustomTileStatePersister() } + +val Kosmos.customTileRepository: FakeCustomTileRepository by + Kosmos.Fixture { + FakeCustomTileRepository( + customTileStatePersister, + packageManagerAdapterFacade, + testScope.testScheduler, + ) + } + +val Kosmos.customTileDefaultsRepository: FakeCustomTileDefaultsRepository by + Kosmos.Fixture { FakeCustomTileDefaultsRepository() } + +val Kosmos.customTilePackagesUpdatesRepository: FakeCustomTilePackageUpdatesRepository by + Kosmos.Fixture { FakeCustomTilePackageUpdatesRepository() } + +val Kosmos.packageManagerAdapterFacade: FakePackageManagerAdapterFacade by + Kosmos.Fixture { FakePackageManagerAdapterFacade(tileSpec) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt index ccf03911495f..ba803d8baef0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt @@ -19,21 +19,21 @@ package com.android.systemui.qs.tiles.impl.custom.data.repository import android.os.UserHandle import android.service.quicksettings.Tile import com.android.systemui.qs.external.FakeCustomTileStatePersister -import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.flow.Flow class FakeCustomTileRepository( - tileSpec: TileSpec.CustomTileSpec, customTileStatePersister: FakeCustomTileStatePersister, + private val packageManagerAdapterFacade: FakePackageManagerAdapterFacade, testBackgroundContext: CoroutineContext, ) : CustomTileRepository { private val realDelegate: CustomTileRepository = CustomTileRepositoryImpl( - tileSpec, + packageManagerAdapterFacade.tileSpec, customTileStatePersister, + packageManagerAdapterFacade.packageManagerAdapter, testBackgroundContext, ) @@ -44,6 +44,10 @@ class FakeCustomTileRepository( override fun getTile(user: UserHandle): Tile? = realDelegate.getTile(user) + override suspend fun isTileActive(): Boolean = realDelegate.isTileActive() + + override suspend fun isTileToggleable(): Boolean = realDelegate.isTileToggleable() + override suspend fun updateWithTile( user: UserHandle, newTile: Tile, @@ -55,4 +59,8 @@ class FakeCustomTileRepository( defaults: CustomTileDefaults, isPersistable: Boolean, ) = realDelegate.updateWithDefaults(user, defaults, isPersistable) + + fun setTileActive(isActive: Boolean) = packageManagerAdapterFacade.setIsActive(isActive) + + fun setTileToggleable(isActive: Boolean) = packageManagerAdapterFacade.setIsToggleable(isActive) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt new file mode 100644 index 000000000000..c9a7655b5571 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.qs.tiles.impl.custom.data.repository + +import android.content.pm.ServiceInfo +import android.os.Bundle +import com.android.systemui.qs.external.PackageManagerAdapter +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever + +class FakePackageManagerAdapterFacade( + val tileSpec: TileSpec.CustomTileSpec, + val packageManagerAdapter: PackageManagerAdapter = mock {}, +) { + + private var isToggleable: Boolean = false + private var isActive: Boolean = false + + init { + whenever(packageManagerAdapter.getServiceInfo(eq(tileSpec.componentName), any())) + .thenAnswer { + ServiceInfo().apply { + metaData = + Bundle().apply { + putBoolean( + android.service.quicksettings.TileService.META_DATA_TOGGLEABLE_TILE, + isToggleable + ) + putBoolean( + android.service.quicksettings.TileService.META_DATA_ACTIVE_TILE, + isActive + ) + } + } + } + } + + fun setIsActive(isActive: Boolean) { + this.isActive = isActive + } + + fun setIsToggleable(isToggleable: Boolean) { + this.isToggleable = isToggleable + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt new file mode 100644 index 000000000000..67fecb4d8bcd --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.statusbar.notification.icon.ui.viewbinder + +import com.android.systemui.common.ui.configurationState +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.statusbar.notification.icon.ui.viewmodel.notificationIconContainerShelfViewModel +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.notifCollection +import com.android.systemui.statusbar.ui.systemBarUtilsState + +val Kosmos.notificationIconContainerShelfViewBinder by Fixture { + NotificationIconContainerShelfViewBinder( + notificationIconContainerShelfViewModel, + configurationState, + systemBarUtilsState, + statusBarIconViewBindingFailureTracker, + shelfNotificationIconViewStore, + ) +} + +val Kosmos.shelfNotificationIconViewStore by Fixture { + ShelfNotificationIconViewStore(notifCollection = notifCollection) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelKosmos.kt index 988172c7bb13..b906b6060245 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelKosmos.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.shelf.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.statusbar.notification.icon.ui.viewmodel.notificationIconContainerShelfViewModel import com.android.systemui.statusbar.notification.row.ui.viewmodel.activatableNotificationViewModel import com.android.systemui.statusbar.notification.shelf.domain.interactor.notificationShelfInteractor @@ -26,6 +25,5 @@ val Kosmos.notificationShelfViewModel by Fixture { NotificationShelfViewModel( interactor = notificationShelfInteractor, activatableViewModel = activatableNotificationViewModel, - icons = notificationIconContainerShelfViewModel, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt index ca5b4010fd7d..04716b9c48a3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt @@ -22,11 +22,9 @@ import com.android.systemui.common.ui.configurationState import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testDispatcher -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.shelfNotificationIconViewStore -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.statusBarIconViewBindingFailureTracker +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.notificationIconContainerShelfViewBinder import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel import com.android.systemui.statusbar.phone.notificationIconAreaController -import com.android.systemui.statusbar.ui.systemBarUtilsState val Kosmos.notificationListViewBinder by Fixture { NotificationListViewBinder( @@ -35,9 +33,7 @@ val Kosmos.notificationListViewBinder by Fixture { configuration = configurationState, falsingManager = falsingManager, iconAreaController = notificationIconAreaController, - iconViewBindingFailureTracker = statusBarIconViewBindingFailureTracker, metricsLogger = metricsLogger, - shelfIconViewStore = shelfNotificationIconViewStore, - systemBarUtilsState = systemBarUtilsState, + nicBinder = notificationIconContainerShelfViewBinder, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt index 42c77aaac53f..4e2dc7af8cb4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt @@ -47,6 +47,7 @@ val Kosmos.userSwitcherInteractor by broadcastDispatcher = broadcastDispatcher, keyguardUpdateMonitor = keyguardUpdateMonitor, backgroundDispatcher = testDispatcher, + mainDispatcher = testDispatcher, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, guestUserInteractor = guestUserInteractor, diff --git a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING index a159a5e003c9..5a548fdca12e 100644 --- a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING +++ b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING @@ -54,9 +54,7 @@ "exclude-annotation": "android.support.test.filters.FlakyTest" } ] - } - ], - "postsubmit": [ + }, { "name": "CtsVirtualDevicesCameraTestCases", "options": [ diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java index 6940ffe40a51..f24c4cc59336 100644 --- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java +++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * 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. @@ -70,7 +70,7 @@ public final class VirtualCameraConversionUtil { @Override public void onProcessCaptureRequest(int streamId, int frameId) throws RemoteException { - camera.onProcessCaptureRequest(streamId, frameId, /*metadata=*/ null); + camera.onProcessCaptureRequest(streamId, frameId); } @Override diff --git a/services/core/Android.bp b/services/core/Android.bp index b5c9ffdfffae..5111b08a1812 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -103,7 +103,6 @@ java_library_static { "android.hardware.power-java_shared", ], srcs: [ - ":android.hardware.biometrics.face-V4-java-source", ":android.hardware.tv.hdmi.connection-V1-java-source", ":android.hardware.tv.hdmi.earc-V1-java-source", ":statslog-art-java-gen", diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index df8f17ac9d7c..3ae5527153b7 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -70,7 +70,6 @@ import static android.os.PowerExemptionManager.REASON_INSTR_BACKGROUND_FGS_PERMI import static android.os.PowerExemptionManager.REASON_OPT_OUT_REQUESTED; import static android.os.PowerExemptionManager.REASON_OP_ACTIVATE_PLATFORM_VPN; import static android.os.PowerExemptionManager.REASON_OP_ACTIVATE_VPN; -import static android.os.PowerExemptionManager.REASON_OTHER; import static android.os.PowerExemptionManager.REASON_PACKAGE_INSTALLER; import static android.os.PowerExemptionManager.REASON_PROC_STATE_PERSISTENT; import static android.os.PowerExemptionManager.REASON_PROC_STATE_PERSISTENT_UI; @@ -127,7 +126,6 @@ import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; import android.Manifest; -import android.Manifest.permission; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -1085,7 +1083,7 @@ public final class ActiveServices { // Use that as a shortcut if possible to avoid having to recheck all the conditions. final boolean whileInUseAllowsUiJobScheduling = ActivityManagerService.doesReasonCodeAllowSchedulingUserInitiatedJobs( - r.getFgsAllowWIU()); + r.getFgsAllowWiu_forStart()); r.updateAllowUiJobScheduling(whileInUseAllowsUiJobScheduling || mAm.canScheduleUserInitiatedJobs(callingUid, callingPid, callingPackage)); } else { @@ -2320,7 +2318,7 @@ public final class ActiveServices { // If the foreground service is not started from TOP process, do not allow it to // have while-in-use location/camera/microphone access. - if (!r.isFgsAllowedWIU()) { + if (!r.isFgsAllowedWiu_forCapabilities()) { Slog.w(TAG, "Foreground service started from background can not have " + "location/camera/microphone access: service " @@ -2436,7 +2434,7 @@ public final class ActiveServices { // mAllowWhileInUsePermissionInFgs. r.mAllowStartForegroundAtEntering = r.getFgsAllowStart(); r.mAllowWhileInUsePermissionInFgsAtEntering = - r.isFgsAllowedWIU(); + r.isFgsAllowedWiu_forCapabilities(); r.mStartForegroundCount++; r.mFgsEnterTime = SystemClock.uptimeMillis(); if (!stopProcStatsOp) { @@ -2632,7 +2630,7 @@ public final class ActiveServices { policy.getForegroundServiceTypePolicyInfo(type, defaultToType); final @ForegroundServicePolicyCheckCode int code = policy.checkForegroundServiceTypePolicy( mAm.mContext, r.packageName, r.app.uid, r.app.getPid(), - r.isFgsAllowedWIU(), policyInfo); + r.isFgsAllowedWiu_forStart(), policyInfo); RuntimeException exception = null; switch (code) { case FGS_TYPE_POLICY_CHECK_DEPRECATED: { @@ -7580,78 +7578,76 @@ public final class ActiveServices { * @param callingUid caller app's uid. * @param intent intent to start/bind service. * @param r the service to start. - * @param isBindService True if it's called from bindService(). + * @param inBindService True if it's called from bindService(). * @param forBoundFgs set to true if it's called from Service.startForeground() for a * service that's not started but bound. - * @return true if allow, false otherwise. */ private void setFgsRestrictionLocked(String callingPackage, int callingPid, int callingUid, Intent intent, ServiceRecord r, int userId, - BackgroundStartPrivileges backgroundStartPrivileges, boolean isBindService, + BackgroundStartPrivileges backgroundStartPrivileges, boolean inBindService, boolean forBoundFgs) { - @ReasonCode int allowWIU; + @ReasonCode int allowWiu; @ReasonCode int allowStart; // If called from bindService(), do not update the actual fields, but instead // keep it in a separate set of fields. - if (isBindService) { - allowWIU = r.mAllowWIUInBindService; - allowStart = r.mAllowStartInBindService; + if (inBindService) { + allowWiu = r.mAllowWiu_inBindService; + allowStart = r.mAllowStart_inBindService; } else { - allowWIU = r.mAllowWhileInUsePermissionInFgsReasonNoBinding; - allowStart = r.mAllowStartForegroundNoBinding; + allowWiu = r.mAllowWiu_noBinding; + allowStart = r.mAllowStart_noBinding; } - // Check DeviceConfig flag. - if (!mAm.mConstants.mFlagBackgroundFgsStartRestrictionEnabled) { - if (allowWIU == REASON_DENIED) { - // BGFGS start restrictions are disabled. We're allowing while-in-use permissions. - // Note REASON_OTHER since there's no other suitable reason. - allowWIU = REASON_OTHER; - } - } - - if ((allowWIU == REASON_DENIED) - || (allowStart == REASON_DENIED)) { + if ((allowWiu == REASON_DENIED) || (allowStart == REASON_DENIED)) { @ReasonCode final int allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked( callingPackage, callingPid, callingUid, r.app, backgroundStartPrivileges); // We store them to compare the old and new while-in-use logics to each other. // (They're not used for any other purposes.) - if (allowWIU == REASON_DENIED) { - allowWIU = allowWhileInUse; + if (allowWiu == REASON_DENIED) { + allowWiu = allowWhileInUse; } if (allowStart == REASON_DENIED) { allowStart = shouldAllowFgsStartForegroundWithBindingCheckLocked( allowWhileInUse, callingPackage, callingPid, callingUid, intent, r, - backgroundStartPrivileges, isBindService); + backgroundStartPrivileges, inBindService); } } - if (isBindService) { - r.mAllowWIUInBindService = allowWIU; - r.mAllowStartInBindService = allowStart; + if (inBindService) { + r.mAllowWiu_inBindService = allowWiu; + r.mAllowStart_inBindService = allowStart; } else { if (!forBoundFgs) { - // This is for "normal" situation. - r.mAllowWhileInUsePermissionInFgsReasonNoBinding = allowWIU; - r.mAllowStartForegroundNoBinding = allowStart; + // This is for "normal" situation -- either: + // - in Context.start[Foreground]Service() + // - or, in Service.startForeground() on a started service. + r.mAllowWiu_noBinding = allowWiu; + r.mAllowStart_noBinding = allowStart; } else { - // This logic is only for logging, so we only update the "by-binding" fields. - if (r.mAllowWIUByBindings == REASON_DENIED) { - r.mAllowWIUByBindings = allowWIU; + // Service.startForeground() is called on a service that's not started, but bound. + // In this case, we set them to "byBindings", not "noBinding", because + // we don't want to use them when we calculate the "legacy" code. + // + // We don't want to set them to "no binding" codes, because on U-QPR1 and below, + // we didn't call setFgsRestrictionLocked() in the code path which sets + // forBoundFgs to true, and we wanted to preserve the original behavior in other + // places to compare the legacy and new logic. + if (r.mAllowWiu_byBindings == REASON_DENIED) { + r.mAllowWiu_byBindings = allowWiu; } - if (r.mAllowStartByBindings == REASON_DENIED) { - r.mAllowStartByBindings = allowStart; + if (r.mAllowStart_byBindings == REASON_DENIED) { + r.mAllowStart_byBindings = allowStart; } } // Also do a binding client check, unless called from bindService(). - if (r.mAllowWIUByBindings == REASON_DENIED) { - r.mAllowWIUByBindings = + if (r.mAllowWiu_byBindings == REASON_DENIED) { + r.mAllowWiu_byBindings = shouldAllowFgsWhileInUsePermissionByBindingsLocked(callingUid); } - if (r.mAllowStartByBindings == REASON_DENIED) { - r.mAllowStartByBindings = r.mAllowWIUByBindings; + if (r.mAllowStart_byBindings == REASON_DENIED) { + r.mAllowStart_byBindings = r.mAllowWiu_byBindings; } } } @@ -7660,13 +7656,13 @@ public final class ActiveServices { * Reset various while-in-use and BFSL related information. */ void resetFgsRestrictionLocked(ServiceRecord r) { - r.clearFgsAllowWIU(); + r.clearFgsAllowWiu(); r.clearFgsAllowStart(); r.mInfoAllowStartForeground = null; r.mInfoTempFgsAllowListReason = null; r.mLoggedInfoAllowStartForeground = false; - r.updateAllowUiJobScheduling(r.isFgsAllowedWIU()); + r.updateAllowUiJobScheduling(r.isFgsAllowedWiu_forStart()); } boolean canStartForegroundServiceLocked(int callingPid, int callingUid, String callingPackage) { @@ -8284,7 +8280,8 @@ public final class ActiveServices { allowWhileInUsePermissionInFgs = r.mAllowWhileInUsePermissionInFgsAtEntering; fgsStartReasonCode = r.mAllowStartForegroundAtEntering; } else { - allowWhileInUsePermissionInFgs = r.isFgsAllowedWIU(); + // TODO: Also log "forStart" + allowWhileInUsePermissionInFgs = r.isFgsAllowedWiu_forCapabilities(); fgsStartReasonCode = r.getFgsAllowStart(); } final int callerTargetSdkVersion = r.mRecentCallerApplicationInfo != null @@ -8323,12 +8320,12 @@ public final class ActiveServices { mAm.getUidProcessCapabilityLocked(r.mRecentCallingUid), 0, 0, - r.mAllowWhileInUsePermissionInFgsReasonNoBinding, - r.mAllowWIUInBindService, - r.mAllowWIUByBindings, - r.mAllowStartForegroundNoBinding, - r.mAllowStartInBindService, - r.mAllowStartByBindings, + r.mAllowWiu_noBinding, + r.mAllowWiu_inBindService, + r.mAllowWiu_byBindings, + r.mAllowStart_noBinding, + r.mAllowStart_inBindService, + r.mAllowStart_byBindings, fgsStartApi, fgsRestrictionRecalculated); diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index d0ab287785e3..626b70b51093 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -275,7 +275,7 @@ public final class CachedAppOptimizer { @VisibleForTesting static final String DEFAULT_COMPACT_PROC_STATE_THROTTLE = String.valueOf(ActivityManager.PROCESS_STATE_RECEIVER); @VisibleForTesting static final long DEFAULT_FREEZER_DEBOUNCE_TIMEOUT = 10_000L; - @VisibleForTesting static final boolean DEFAULT_FREEZER_EXEMPT_INST_PKG = true; + @VisibleForTesting static final boolean DEFAULT_FREEZER_EXEMPT_INST_PKG = false; @VisibleForTesting static final boolean DEFAULT_FREEZER_BINDER_ENABLED = true; @VisibleForTesting static final long DEFAULT_FREEZER_BINDER_DIVISOR = 4; @VisibleForTesting static final int DEFAULT_FREEZER_BINDER_OFFSET = 500; diff --git a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java index fc8ad6bc94e7..05303f6fef63 100644 --- a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java +++ b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java @@ -507,7 +507,8 @@ public class ForegroundServiceTypeLoggerModule { r.appInfo.uid, r.shortInstanceName, fgsState, // FGS State - r.isFgsAllowedWIU(), // allowWhileInUsePermissionInFgs + // TODO: Also log "forStart" + r.isFgsAllowedWiu_forCapabilities(), // allowWhileInUsePermissionInFgs r.getFgsAllowStart(), // fgsStartReasonCode r.appInfo.targetSdkVersion, r.mRecentCallingUid, @@ -535,12 +536,12 @@ public class ForegroundServiceTypeLoggerModule { ActivityManager.PROCESS_CAPABILITY_NONE, apiDurationBeforeFgsStart, apiDurationAfterFgsEnd, - r.mAllowWhileInUsePermissionInFgsReasonNoBinding, - r.mAllowWIUInBindService, - r.mAllowWIUByBindings, - r.mAllowStartForegroundNoBinding, - r.mAllowStartInBindService, - r.mAllowStartByBindings, + r.mAllowWiu_noBinding, + r.mAllowWiu_inBindService, + r.mAllowWiu_byBindings, + r.mAllowStart_noBinding, + r.mAllowStart_inBindService, + r.mAllowStart_byBindings, FOREGROUND_SERVICE_STATE_CHANGED__FGS_START_API__FGSSTARTAPI_NA, false ); diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 3424578a78d2..b507a604343e 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -2254,7 +2254,7 @@ public class OomAdjuster { if (s.isForeground) { final int fgsType = s.foregroundServiceType; - if (s.isFgsAllowedWIU()) { + if (s.isFgsAllowedWiu_forCapabilities()) { capabilityFromFGS |= (fgsType & FOREGROUND_SERVICE_TYPE_LOCATION) != 0 ? PROCESS_CAPABILITY_FOREGROUND_LOCATION : 0; diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java index 0fba73998bb9..08b129eeb8e5 100644 --- a/services/core/java/com/android/server/am/ServiceRecord.java +++ b/services/core/java/com/android/server/am/ServiceRecord.java @@ -37,6 +37,9 @@ import android.app.IApplicationThread; import android.app.Notification; import android.app.PendingIntent; import android.app.RemoteServiceException.CannotPostForegroundServiceNotificationException; +import android.app.compat.CompatChanges; +import android.compat.annotation.ChangeId; +import android.compat.annotation.Disabled; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -81,6 +84,37 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN // Maximum number of times it can fail during execution before giving up. static final int MAX_DONE_EXECUTING_COUNT = 6; + + // Compat IDs for the new FGS logic. For now, we just disable all of them. + // TODO: Enable them at some point, but only for V+ builds. + + /** + * Compat ID to enable the new FGS start logic, for permission calculation used for + * per-FGS-type eligibility calculation. + * (See also android.app.ForegroundServiceTypePolicy) + */ + @ChangeId + // @EnabledAfter(targetSdkVersion = VERSION_CODES.UPSIDE_DOWN_CAKE) + @Disabled + static final long USE_NEW_WIU_LOGIC_FOR_START = 311208629L; + + /** + * Compat ID to enable the new FGS start logic, for capability calculation. + */ + @ChangeId + // Always enabled + @Disabled + static final long USE_NEW_WIU_LOGIC_FOR_CAPABILITIES = 313677553L; + + /** + * Compat ID to enable the new FGS start logic, for deciding whether to allow FGS start from + * the background. + */ + @ChangeId + // @EnabledAfter(targetSdkVersion = VERSION_CODES.UPSIDE_DOWN_CAKE) + @Disabled + static final long USE_NEW_BFSL_LOGIC = 311208749L; + final ActivityManagerService ams; final ComponentName name; // service component. final ComponentName instanceName; // service component's per-instance name. @@ -178,7 +212,7 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN // If it's not DENIED, while-in-use permissions are allowed. // while-in-use permissions in FGS started from background might be restricted. @PowerExemptionManager.ReasonCode - int mAllowWhileInUsePermissionInFgsReasonNoBinding = REASON_DENIED; + int mAllowWiu_noBinding = REASON_DENIED; // A copy of mAllowWhileInUsePermissionInFgs's value when the service is entering FGS state. boolean mAllowWhileInUsePermissionInFgsAtEntering; @@ -208,7 +242,7 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN // allow the service becomes foreground service? Service started from background may not be // allowed to become a foreground service. @PowerExemptionManager.ReasonCode - int mAllowStartForegroundNoBinding = REASON_DENIED; + int mAllowStart_noBinding = REASON_DENIED; // A copy of mAllowStartForeground's value when the service is entering FGS state. @PowerExemptionManager.ReasonCode int mAllowStartForegroundAtEntering = REASON_DENIED; @@ -220,72 +254,172 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN boolean mLoggedInfoAllowStartForeground; @PowerExemptionManager.ReasonCode - int mAllowWIUInBindService = REASON_DENIED; + int mAllowWiu_inBindService = REASON_DENIED; + + @PowerExemptionManager.ReasonCode + int mAllowWiu_byBindings = REASON_DENIED; + + @PowerExemptionManager.ReasonCode + int mAllowStart_inBindService = REASON_DENIED; + + @PowerExemptionManager.ReasonCode + int mAllowStart_byBindings = REASON_DENIED; + + /** + * Whether to use the new "while-in-use permission" logic for FGS start + */ + private boolean useNewWiuLogic_forStart() { + return Flags.newFgsRestrictionLogic() // This flag should only be set on V+ + && CompatChanges.isChangeEnabled(USE_NEW_WIU_LOGIC_FOR_START, appInfo.uid); + } + + /** + * Whether to use the new "while-in-use permission" logic for capabilities + */ + private boolean useNewWiuLogic_forCapabilities() { + return Flags.newFgsRestrictionLogic() // This flag should only be set on V+ + && CompatChanges.isChangeEnabled(USE_NEW_WIU_LOGIC_FOR_CAPABILITIES, appInfo.uid); + } + + /** + * Whether to use the new "FGS BG start exemption" logic. + */ + private boolean useNewBfslLogic() { + return Flags.newFgsRestrictionLogic() // This flag should only be set on V+ + && CompatChanges.isChangeEnabled(USE_NEW_BFSL_LOGIC, appInfo.uid); + } + + + @PowerExemptionManager.ReasonCode + private int getFgsAllowWiu_legacy() { + // In the legacy mode (U-), we use mAllowWiu_inBindService and mAllowWiu_noBinding. + return reasonOr( + mAllowWiu_noBinding, + mAllowWiu_inBindService + ); + } @PowerExemptionManager.ReasonCode - int mAllowWIUByBindings = REASON_DENIED; + private int getFgsAllowWiu_new() { + // In the new mode, use by-bindings instead of in-bind-service + return reasonOr( + mAllowWiu_noBinding, + mAllowWiu_byBindings + ); + } + /** + * We use this logic for ForegroundServiceTypePolicy and UIDT eligibility check. + */ @PowerExemptionManager.ReasonCode - int mAllowStartInBindService = REASON_DENIED; + int getFgsAllowWiu_forStart() { + if (useNewWiuLogic_forStart()) { + return getFgsAllowWiu_new(); + } else { + return getFgsAllowWiu_legacy(); + } + } + /** + * We use this logic for the capability calculation in oom-adjuster. + */ @PowerExemptionManager.ReasonCode - int mAllowStartByBindings = REASON_DENIED; + int getFgsAllowWiu_forCapabilities() { + if (useNewWiuLogic_forCapabilities()) { + return getFgsAllowWiu_new(); + } else { + // If alwaysUseNewLogicForWiuCapabilities() isn't set, just use the same logic as + // getFgsAllowWiu_forStart(). + return getFgsAllowWiu_forStart(); + } + } + + /** + * We use this logic for ForegroundServiceTypePolicy and UIDT eligibility check. + */ + boolean isFgsAllowedWiu_forStart() { + return getFgsAllowWiu_forStart() != REASON_DENIED; + } + + /** + * We use this logic for the capability calculation in oom-adjuster. + */ + boolean isFgsAllowedWiu_forCapabilities() { + return getFgsAllowWiu_forCapabilities() != REASON_DENIED; + } @PowerExemptionManager.ReasonCode - int getFgsAllowWIU() { - return mAllowWhileInUsePermissionInFgsReasonNoBinding != REASON_DENIED - ? mAllowWhileInUsePermissionInFgsReasonNoBinding - : mAllowWIUInBindService; + private int getFgsAllowStart_legacy() { + // This is used for BFSL (background FGS launch) exemption. + // Originally -- on U-QPR1 and before -- we only used [in-bind-service] + [no-binding]. + // This would exclude certain "valid" situations, so in U-QPR2, we started + // using [by-bindings] too. + return reasonOr( + mAllowStart_noBinding, + mAllowStart_inBindService, + mAllowStart_byBindings + ); } - boolean isFgsAllowedWIU() { - return getFgsAllowWIU() != REASON_DENIED; + @PowerExemptionManager.ReasonCode + private int getFgsAllowStart_new() { + // In the new mode, we don't use [in-bind-service]. + return reasonOr( + mAllowStart_noBinding, + mAllowStart_byBindings + ); } + /** + * Calculate a BFSL exemption code. + */ @PowerExemptionManager.ReasonCode int getFgsAllowStart() { - return mAllowStartForegroundNoBinding != REASON_DENIED - ? mAllowStartForegroundNoBinding - : (mAllowStartByBindings != REASON_DENIED - ? mAllowStartByBindings - : mAllowStartInBindService); + if (useNewBfslLogic()) { + return getFgsAllowStart_new(); + } else { + return getFgsAllowStart_legacy(); + } } + /** + * Return whether BFSL is allowed or not. + */ boolean isFgsAllowedStart() { return getFgsAllowStart() != REASON_DENIED; } - void clearFgsAllowWIU() { - mAllowWhileInUsePermissionInFgsReasonNoBinding = REASON_DENIED; - mAllowWIUInBindService = REASON_DENIED; - mAllowWIUByBindings = REASON_DENIED; + void clearFgsAllowWiu() { + mAllowWiu_noBinding = REASON_DENIED; + mAllowWiu_inBindService = REASON_DENIED; + mAllowWiu_byBindings = REASON_DENIED; } void clearFgsAllowStart() { - mAllowStartForegroundNoBinding = REASON_DENIED; - mAllowStartInBindService = REASON_DENIED; - mAllowStartByBindings = REASON_DENIED; + mAllowStart_noBinding = REASON_DENIED; + mAllowStart_inBindService = REASON_DENIED; + mAllowStart_byBindings = REASON_DENIED; } @PowerExemptionManager.ReasonCode - int reasonOr(@PowerExemptionManager.ReasonCode int first, + static int reasonOr( + @PowerExemptionManager.ReasonCode int first, @PowerExemptionManager.ReasonCode int second) { return first != REASON_DENIED ? first : second; } - boolean allowedChanged(@PowerExemptionManager.ReasonCode int first, - @PowerExemptionManager.ReasonCode int second) { - return (first == REASON_DENIED) != (second == REASON_DENIED); + @PowerExemptionManager.ReasonCode + static int reasonOr( + @PowerExemptionManager.ReasonCode int first, + @PowerExemptionManager.ReasonCode int second, + @PowerExemptionManager.ReasonCode int third) { + return first != REASON_DENIED ? first : reasonOr(second, third); } - String changeMessage(@PowerExemptionManager.ReasonCode int first, - @PowerExemptionManager.ReasonCode int second) { - return reasonOr(first, second) == REASON_DENIED ? "DENIED" - : ("ALLOWED (" - + reasonCodeToString(first) - + "+" - + reasonCodeToString(second) - + ")"); + boolean allowedChanged( + @PowerExemptionManager.ReasonCode int legacyCode, + @PowerExemptionManager.ReasonCode int newCode) { + return (legacyCode == REASON_DENIED) != (newCode == REASON_DENIED); } private String getFgsInfoForWtf() { @@ -295,15 +429,14 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN } void maybeLogFgsLogicChange() { - final int origWiu = reasonOr(mAllowWhileInUsePermissionInFgsReasonNoBinding, - mAllowWIUInBindService); - final int newWiu = reasonOr(mAllowWhileInUsePermissionInFgsReasonNoBinding, - mAllowWIUByBindings); - final int origStart = reasonOr(mAllowStartForegroundNoBinding, mAllowStartInBindService); - final int newStart = reasonOr(mAllowStartForegroundNoBinding, mAllowStartByBindings); + final int wiuLegacy = getFgsAllowWiu_legacy(); + final int wiuNew = getFgsAllowWiu_new(); + + final int startLegacy = getFgsAllowStart_legacy(); + final int startNew = getFgsAllowStart_new(); - final boolean wiuChanged = allowedChanged(origWiu, newWiu); - final boolean startChanged = allowedChanged(origStart, newStart); + final boolean wiuChanged = allowedChanged(wiuLegacy, wiuNew); + final boolean startChanged = allowedChanged(startLegacy, startNew); if (!wiuChanged && !startChanged) { return; @@ -311,15 +444,14 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN final String message = "FGS logic changed:" + (wiuChanged ? " [WIU changed]" : "") + (startChanged ? " [BFSL changed]" : "") - + " OW:" // Orig-WIU - + changeMessage(mAllowWhileInUsePermissionInFgsReasonNoBinding, - mAllowWIUInBindService) - + " NW:" // New-WIU - + changeMessage(mAllowWhileInUsePermissionInFgsReasonNoBinding, mAllowWIUByBindings) - + " OS:" // Orig-start - + changeMessage(mAllowStartForegroundNoBinding, mAllowStartInBindService) - + " NS:" // New-start - + changeMessage(mAllowStartForegroundNoBinding, mAllowStartByBindings) + + " Orig WIU:" + + reasonCodeToString(wiuLegacy) + + " New WIU:" + + reasonCodeToString(wiuNew) + + " Orig BFSL:" + + reasonCodeToString(startLegacy) + + " New BFSL:" + + reasonCodeToString(startNew) + getFgsInfoForWtf(); Slog.wtf(TAG_SERVICE, message); } @@ -587,6 +719,7 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN proto.write(ServiceRecordProto.AppInfo.RES_DIR, appInfo.publicSourceDir); } proto.write(ServiceRecordProto.AppInfo.DATA_DIR, appInfo.dataDir); + proto.write(ServiceRecordProto.AppInfo.TARGET_SDK_VERSION, appInfo.targetSdkVersion); proto.end(appInfoToken); } if (app != null) { @@ -611,8 +744,10 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN ProtoUtils.toDuration(proto, ServiceRecordProto.LAST_ACTIVITY_TIME, lastActivity, now); ProtoUtils.toDuration(proto, ServiceRecordProto.RESTART_TIME, restartTime, now); proto.write(ServiceRecordProto.CREATED_FROM_FG, createdFromFg); + + // TODO: Log "forStart" too. proto.write(ServiceRecordProto.ALLOW_WHILE_IN_USE_PERMISSION_IN_FGS, - isFgsAllowedWIU()); + isFgsAllowedWiu_forCapabilities()); if (startRequested || delayedStop || lastStartId != 0) { long startToken = proto.start(ServiceRecordProto.START); @@ -691,15 +826,25 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN proto.end(shortFgsToken); } + // TODO: Dump all the mAllowWiu* and mAllowStart* fields as needed. + proto.end(token); } + void dumpReasonCode(PrintWriter pw, String prefix, String fieldName, int code) { + pw.print(prefix); + pw.print(fieldName); + pw.print("="); + pw.println(PowerExemptionManager.reasonCodeToString(code)); + } + void dump(PrintWriter pw, String prefix) { pw.print(prefix); pw.print("intent={"); pw.print(intent.getIntent().toShortString(false, true, false, false)); pw.println('}'); pw.print(prefix); pw.print("packageName="); pw.println(packageName); pw.print(prefix); pw.print("processName="); pw.println(processName); + pw.print(prefix); pw.print("targetSdkVersion="); pw.println(appInfo.targetSdkVersion); if (permission != null) { pw.print(prefix); pw.print("permission="); pw.println(permission); } @@ -727,26 +872,38 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN pw.print(prefix); pw.print("mIsAllowedBgActivityStartsByStart="); pw.println(mBackgroundStartPrivilegesByStartMerged); } - pw.print(prefix); pw.print("mAllowWhileInUsePermissionInFgsReason="); - pw.println(PowerExemptionManager.reasonCodeToString( - mAllowWhileInUsePermissionInFgsReasonNoBinding)); - pw.print(prefix); pw.print("mAllowWIUInBindService="); - pw.println(PowerExemptionManager.reasonCodeToString(mAllowWIUInBindService)); - pw.print(prefix); pw.print("mAllowWIUByBindings="); - pw.println(PowerExemptionManager.reasonCodeToString(mAllowWIUByBindings)); + pw.print(prefix); pw.print("useNewWiuLogic_forCapabilities()="); + pw.println(useNewWiuLogic_forCapabilities()); + pw.print(prefix); pw.print("useNewWiuLogic_forStart()="); + pw.println(useNewWiuLogic_forStart()); + pw.print(prefix); pw.print("useNewBfslLogic()="); + pw.println(useNewBfslLogic()); + + dumpReasonCode(pw, prefix, "mAllowWiu_noBinding", mAllowWiu_noBinding); + dumpReasonCode(pw, prefix, "mAllowWiu_inBindService", mAllowWiu_inBindService); + dumpReasonCode(pw, prefix, "mAllowWiu_byBindings", mAllowWiu_byBindings); + + dumpReasonCode(pw, prefix, "getFgsAllowWiu_legacy", getFgsAllowWiu_legacy()); + dumpReasonCode(pw, prefix, "getFgsAllowWiu_new", getFgsAllowWiu_new()); + + dumpReasonCode(pw, prefix, "getFgsAllowWiu_forStart", getFgsAllowWiu_forStart()); + dumpReasonCode(pw, prefix, "getFgsAllowWiu_forCapabilities", + getFgsAllowWiu_forCapabilities()); pw.print(prefix); pw.print("allowUiJobScheduling="); pw.println(mAllowUiJobScheduling); pw.print(prefix); pw.print("recentCallingPackage="); pw.println(mRecentCallingPackage); pw.print(prefix); pw.print("recentCallingUid="); pw.println(mRecentCallingUid); - pw.print(prefix); pw.print("allowStartForeground="); - pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartForegroundNoBinding)); - pw.print(prefix); pw.print("mAllowStartInBindService="); - pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartInBindService)); - pw.print(prefix); pw.print("mAllowStartByBindings="); - pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartByBindings)); + + dumpReasonCode(pw, prefix, "mAllowStart_noBinding", mAllowStart_noBinding); + dumpReasonCode(pw, prefix, "mAllowStart_inBindService", mAllowStart_inBindService); + dumpReasonCode(pw, prefix, "mAllowStart_byBindings", mAllowStart_byBindings); + + dumpReasonCode(pw, prefix, "getFgsAllowStart_legacy", getFgsAllowStart_legacy()); + dumpReasonCode(pw, prefix, "getFgsAllowStart_new", getFgsAllowStart_new()); + dumpReasonCode(pw, prefix, "getFgsAllowStart", getFgsAllowStart()); pw.print(prefix); pw.print("startForegroundCount="); pw.println(mStartForegroundCount); diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index a6b532cdef09..badd7f085e56 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -3050,8 +3050,8 @@ class UserController implements Handler.Callback { /** * Returns whether the given user requires credential entry at this time. This is used to - * intercept activity launches for locked work apps due to work challenge being triggered - * or when the profile user is yet to be unlocked. + * intercept activity launches for apps corresponding to locked profiles due to separate + * challenge being triggered or when the profile user is yet to be unlocked. */ protected boolean shouldConfirmCredentials(@UserIdInt int userId) { if (getStartedUserState(userId) == null) { diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index d9e8dddddae4..654aebd89de2 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -28,3 +28,10 @@ flag { description: "Restrict network access for certain applications in BFGS process state" bug: "304347838" } +# Whether to use the new while-in-use / BG-FGS-start logic +flag { + namespace: "backstage_power" + name: "new_fgs_restriction_logic" + description: "Enable the new FGS restriction logic" + bug: "276963716" +} diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java index b3fb9c947ca4..8b7e56ec410d 100644 --- a/services/core/java/com/android/server/app/GameManagerService.java +++ b/services/core/java/com/android/server/app/GameManagerService.java @@ -20,6 +20,7 @@ import static android.content.Intent.ACTION_PACKAGE_ADDED; import static android.content.Intent.ACTION_PACKAGE_REMOVED; import static android.content.Intent.EXTRA_REPLACING; import static android.server.app.Flags.gameDefaultFrameRate; +import static android.server.app.Flags.disableGameModeWhenAppTop; import static com.android.internal.R.styleable.GameModeConfig_allowGameAngleDriver; import static com.android.internal.R.styleable.GameModeConfig_allowGameDownscaling; @@ -181,7 +182,9 @@ public final class GameManagerService extends IGameManagerService.Stub { @Nullable final MyUidObserver mUidObserver; @GuardedBy("mUidObserverLock") - private final Set<Integer> mForegroundGameUids = new HashSet<>(); + private final Set<Integer> mGameForegroundUids = new HashSet<>(); + @GuardedBy("mUidObserverLock") + private final Set<Integer> mNonGameForegroundUids = new HashSet<>(); private final GameManagerServiceSystemPropertiesWrapper mSysProps; private float mGameDefaultFrameRateValue; @@ -238,12 +241,10 @@ public final class GameManagerService extends IGameManagerService.Stub { FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP | FileUtils.S_IWGRP, -1, -1); - if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_GAME_SERVICE)) { + if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_GAME_SERVICE)) { mGameServiceController = new GameServiceController( context, BackgroundThread.getExecutor(), - new GameServiceProviderSelectorImpl( - context.getResources(), - context.getPackageManager()), + new GameServiceProviderSelectorImpl(context.getResources(), mPackageManager), new GameServiceProviderInstanceFactoryImpl(context)); } else { mGameServiceController = null; @@ -2245,7 +2246,7 @@ public final class GameManagerService extends IGameManagerService.Stub { // Update all foreground games' frame rate. synchronized (mUidObserverLock) { - for (int uid : mForegroundGameUids) { + for (int uid : mGameForegroundUids) { setGameDefaultFrameRateOverride(uid, getGameDefaultFrameRate(isEnabled)); } } @@ -2261,31 +2262,44 @@ public final class GameManagerService extends IGameManagerService.Stub { @Override public void onUidGone(int uid, boolean disabled) { synchronized (mUidObserverLock) { - disableGameMode(uid); + handleUidMovedOffTop(uid); } } @Override public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) { - synchronized (mUidObserverLock) { - - if (procState != ActivityManager.PROCESS_STATE_TOP) { - disableGameMode(uid); + switch (procState) { + case ActivityManager.PROCESS_STATE_TOP: + handleUidMovedToTop(uid); return; - } + default: + handleUidMovedOffTop(uid); + } + } - final String[] packages = mContext.getPackageManager().getPackagesForUid(uid); - if (packages == null || packages.length == 0) { - return; - } + private void handleUidMovedToTop(int uid) { + final String[] packages = mPackageManager.getPackagesForUid(uid); + if (packages == null || packages.length == 0) { + return; + } - final int userId = mContext.getUserId(); - if (!Arrays.stream(packages).anyMatch(p -> isPackageGame(p, userId))) { + final int userId = mContext.getUserId(); + final boolean isNotGame = Arrays.stream(packages).noneMatch( + p -> isPackageGame(p, userId)); + synchronized (mUidObserverLock) { + if (isNotGame) { + if (disableGameModeWhenAppTop()) { + if (!mGameForegroundUids.isEmpty() && mNonGameForegroundUids.isEmpty()) { + Slog.v(TAG, "Game power mode OFF (first non-game in foreground)"); + mPowerManagerInternal.setPowerMode(Mode.GAME, false); + } + mNonGameForegroundUids.add(uid); + } return; } - - if (mForegroundGameUids.isEmpty()) { - Slog.v(TAG, "Game power mode ON (process state was changed to foreground)"); + if (mGameForegroundUids.isEmpty() && (!disableGameModeWhenAppTop() + || mNonGameForegroundUids.isEmpty())) { + Slog.v(TAG, "Game power mode ON (first game in foreground)"); mPowerManagerInternal.setPowerMode(Mode.GAME, true); } final boolean isGameDefaultFrameRateDisabled = @@ -2293,22 +2307,26 @@ public final class GameManagerService extends IGameManagerService.Stub { PROPERTY_DEBUG_GFX_GAME_DEFAULT_FRAME_RATE_DISABLED, false); setGameDefaultFrameRateOverride(uid, getGameDefaultFrameRate(!isGameDefaultFrameRateDisabled)); - mForegroundGameUids.add(uid); + mGameForegroundUids.add(uid); } } - private void disableGameMode(int uid) { + private void handleUidMovedOffTop(int uid) { synchronized (mUidObserverLock) { - if (!mForegroundGameUids.contains(uid)) { - return; - } - mForegroundGameUids.remove(uid); - if (!mForegroundGameUids.isEmpty()) { - return; + if (mGameForegroundUids.contains(uid)) { + mGameForegroundUids.remove(uid); + if (mGameForegroundUids.isEmpty() && (!disableGameModeWhenAppTop() + || mNonGameForegroundUids.isEmpty())) { + Slog.v(TAG, "Game power mode OFF (no games in foreground)"); + mPowerManagerInternal.setPowerMode(Mode.GAME, false); + } + } else if (disableGameModeWhenAppTop() && mNonGameForegroundUids.contains(uid)) { + mNonGameForegroundUids.remove(uid); + if (mNonGameForegroundUids.isEmpty() && !mGameForegroundUids.isEmpty()) { + Slog.v(TAG, "Game power mode ON (only games in foreground)"); + mPowerManagerInternal.setPowerMode(Mode.GAME, true); + } } - Slog.v(TAG, - "Game power mode OFF (process remomved or state changed to background)"); - mPowerManagerInternal.setPowerMode(Mode.GAME, false); } } } diff --git a/services/core/java/com/android/server/app/flags.aconfig b/services/core/java/com/android/server/app/flags.aconfig index f2e4783bd9eb..0673013bdc44 100644 --- a/services/core/java/com/android/server/app/flags.aconfig +++ b/services/core/java/com/android/server/app/flags.aconfig @@ -6,4 +6,11 @@ flag { description: "This flag guards the new behavior with the addition of Game Default Frame Rate feature." bug: "286084594" is_fixed_read_only: true -}
\ No newline at end of file +} + +flag { + name: "disable_game_mode_when_app_top" + namespace: "game" + description: "Disable game power mode when a non-game app is also top and visible" + bug: "299295925" +} diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 80917533cce1..dada72eb51d9 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -56,6 +56,7 @@ import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; +import android.util.Pair; import android.util.PrintWriterPrinter; import com.android.internal.annotations.GuardedBy; @@ -1640,7 +1641,7 @@ public class AudioDeviceBroker { return mBtHelper.getLeAudioDeviceGroupId(device); } - /*package*/ List<String> getLeAudioGroupAddresses(int groupId) { + /*package*/ List<Pair<String, String>> getLeAudioGroupAddresses(int groupId) { return mBtHelper.getLeAudioGroupAddresses(groupId); } @@ -2651,9 +2652,9 @@ public class AudioDeviceBroker { } } - List<String> getDeviceAddresses(AudioDeviceAttributes device) { + List<String> getDeviceIdentityAddresses(AudioDeviceAttributes device) { synchronized (mDeviceStateLock) { - return mDeviceInventory.getDeviceAddresses(device); + return mDeviceInventory.getDeviceIdentityAddresses(device); } } diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index d138f24e7dc7..e05824a5f1ab 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -566,23 +566,40 @@ public class AudioDeviceInventory { final int mDeviceType; final @NonNull String mDeviceName; final @NonNull String mDeviceAddress; + @NonNull String mDeviceIdentityAddress; int mDeviceCodecFormat; - @NonNull String mPeerDeviceAddress; final int mGroupId; + @NonNull String mPeerDeviceAddress; + @NonNull String mPeerIdentityDeviceAddress; /** Disabled operating modes for this device. Use a negative logic so that by default * an empty list means all modes are allowed. * See BluetoothAdapter.AUDIO_MODE_DUPLEX and BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY */ @NonNull ArraySet<String> mDisabledModes = new ArraySet(0); - DeviceInfo(int deviceType, String deviceName, String deviceAddress, - int deviceCodecFormat, String peerDeviceAddress, int groupId) { + DeviceInfo(int deviceType, String deviceName, String address, + String identityAddress, int codecFormat, + int groupId, String peerAddress, String peerIdentityAddress) { mDeviceType = deviceType; - mDeviceName = deviceName == null ? "" : deviceName; - mDeviceAddress = deviceAddress == null ? "" : deviceAddress; - mDeviceCodecFormat = deviceCodecFormat; - mPeerDeviceAddress = peerDeviceAddress == null ? "" : peerDeviceAddress; + mDeviceName = TextUtils.emptyIfNull(deviceName); + mDeviceAddress = TextUtils.emptyIfNull(address); + mDeviceIdentityAddress = TextUtils.emptyIfNull(identityAddress); + mDeviceCodecFormat = codecFormat; mGroupId = groupId; + mPeerDeviceAddress = TextUtils.emptyIfNull(peerAddress); + mPeerIdentityDeviceAddress = TextUtils.emptyIfNull(peerIdentityAddress); + } + + /** Constructor for all devices except A2DP sink and LE Audio */ + DeviceInfo(int deviceType, String deviceName, String address) { + this(deviceType, deviceName, address, null, AudioSystem.AUDIO_FORMAT_DEFAULT); + } + + /** Constructor for A2DP sink devices */ + DeviceInfo(int deviceType, String deviceName, String address, + String identityAddress, int codecFormat) { + this(deviceType, deviceName, address, identityAddress, codecFormat, + BluetoothLeAudio.GROUP_ID_INVALID, null, null); } void setModeDisabled(String mode) { @@ -601,25 +618,20 @@ public class AudioDeviceInventory { return isModeEnabled(BluetoothAdapter.AUDIO_MODE_DUPLEX); } - DeviceInfo(int deviceType, String deviceName, String deviceAddress, - int deviceCodecFormat) { - this(deviceType, deviceName, deviceAddress, deviceCodecFormat, - null, BluetoothLeAudio.GROUP_ID_INVALID); - } - - DeviceInfo(int deviceType, String deviceName, String deviceAddress) { - this(deviceType, deviceName, deviceAddress, AudioSystem.AUDIO_FORMAT_DEFAULT); - } - @Override public String toString() { return "[DeviceInfo: type:0x" + Integer.toHexString(mDeviceType) + " (" + AudioSystem.getDeviceName(mDeviceType) + ") name:" + mDeviceName + " addr:" + Utils.anonymizeBluetoothAddress(mDeviceType, mDeviceAddress) + + " identity addr:" + + Utils.anonymizeBluetoothAddress(mDeviceType, mDeviceIdentityAddress) + " codec: " + Integer.toHexString(mDeviceCodecFormat) - + " peer addr:" + mPeerDeviceAddress + " group:" + mGroupId + + " peer addr:" + + Utils.anonymizeBluetoothAddress(mDeviceType, mPeerDeviceAddress) + + " peer identity addr:" + + Utils.anonymizeBluetoothAddress(mDeviceType, mPeerIdentityDeviceAddress) + " disabled modes: " + mDisabledModes + "]"; } @@ -947,20 +959,44 @@ public class AudioDeviceInventory { } + /** + * Goes over all connected LE Audio devices in the provided group ID and + * update: + * - the peer address according to the addres of other device in the same + * group (can also clear the peer address is not anymore in the group) + * - The dentity address if not yet set. + * LE Audio buds in a pair are in the same group. + * @param groupId the LE Audio group to update + */ /*package*/ void onUpdateLeAudioGroupAddresses(int groupId) { synchronized (mDevicesLock) { + // <address, identy address> + List<Pair<String, String>> addresses = new ArrayList<>(); for (DeviceInfo di : mConnectedDevices.values()) { if (di.mGroupId == groupId) { - List<String> addresses = mDeviceBroker.getLeAudioGroupAddresses(groupId); + if (addresses.isEmpty()) { + addresses = mDeviceBroker.getLeAudioGroupAddresses(groupId); + } if (di.mPeerDeviceAddress.equals("")) { - for (String addr : addresses) { - if (!addr.equals(di.mDeviceAddress)) { - di.mPeerDeviceAddress = addr; + for (Pair<String, String> addr : addresses) { + if (!addr.first.equals(di.mDeviceAddress)) { + di.mPeerDeviceAddress = addr.first; + di.mPeerIdentityDeviceAddress = addr.second; break; } } - } else if (!addresses.contains(di.mPeerDeviceAddress)) { + } else if (!addresses.contains( + new Pair(di.mPeerDeviceAddress, di.mPeerIdentityDeviceAddress))) { di.mPeerDeviceAddress = ""; + di.mPeerIdentityDeviceAddress = ""; + } + if (di.mDeviceIdentityAddress.equals("")) { + for (Pair<String, String> addr : addresses) { + if (addr.first.equals(di.mDeviceAddress)) { + di.mDeviceIdentityAddress = addr.second; + break; + } + } } } } @@ -1964,7 +2000,7 @@ public class AudioDeviceInventory { mDeviceBroker.clearA2dpSuspended(true /* internalOnly */); final DeviceInfo di = new DeviceInfo(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, name, - address, codec); + address, btInfo.mDevice.getIdentityAddress(), codec); final String diKey = di.getKey(); mConnectedDevices.put(diKey, di); // on a connection always overwrite the device seen by AudioPolicy, see comment above when @@ -2381,12 +2417,15 @@ public class AudioDeviceInventory { // Find LE Group ID and peer headset address if available final int groupId = mDeviceBroker.getLeAudioDeviceGroupId(btInfo.mDevice); String peerAddress = ""; + String peerIdentityAddress = ""; if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) { - List<String> addresses = mDeviceBroker.getLeAudioGroupAddresses(groupId); + List<Pair<String, String>> addresses = + mDeviceBroker.getLeAudioGroupAddresses(groupId); if (addresses.size() > 1) { - for (String addr : addresses) { - if (!addr.equals(address)) { - peerAddress = addr; + for (Pair<String, String> addr : addresses) { + if (!addr.first.equals(address)) { + peerAddress = addr.first; + peerIdentityAddress = addr.second; break; } } @@ -2420,8 +2459,9 @@ public class AudioDeviceInventory { // Reset LEA suspend state each time a new sink is connected mDeviceBroker.clearLeAudioSuspended(true /* internalOnly */); mConnectedDevices.put(DeviceInfo.makeDeviceListKey(device, address), - new DeviceInfo(device, name, address, codec, - peerAddress, groupId)); + new DeviceInfo(device, name, address, + btInfo.mDevice.getIdentityAddress(), codec, + groupId, peerAddress, peerIdentityAddress)); mDeviceBroker.postAccessoryPlugMediaUnmute(device); setCurrentAudioRouteNameIfPossible(name, /*fromA2dp=*/false); addAudioDeviceInInventoryIfNeeded(device, address, peerAddress, @@ -2806,18 +2846,19 @@ public class AudioDeviceInventory { mDevRoleCapturePresetDispatchers.finishBroadcast(); } - List<String> getDeviceAddresses(AudioDeviceAttributes device) { + List<String> getDeviceIdentityAddresses(AudioDeviceAttributes device) { List<String> addresses = new ArrayList<String>(); final String key = DeviceInfo.makeDeviceListKey(device.getInternalType(), device.getAddress()); synchronized (mDevicesLock) { DeviceInfo di = mConnectedDevices.get(key); if (di != null) { - if (!di.mDeviceAddress.isEmpty()) { - addresses.add(di.mDeviceAddress); + if (!di.mDeviceIdentityAddress.isEmpty()) { + addresses.add(di.mDeviceIdentityAddress); } - if (!di.mPeerDeviceAddress.isEmpty()) { - addresses.add(di.mPeerDeviceAddress); + if (!di.mPeerIdentityDeviceAddress.isEmpty() + && !di.mPeerIdentityDeviceAddress.equals(di.mDeviceIdentityAddress)) { + addresses.add(di.mPeerIdentityDeviceAddress); } } } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 0c782318fe89..ea791b775125 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -33,6 +33,7 @@ import static android.media.AudioManager.RINGER_MODE_NORMAL; import static android.media.AudioManager.RINGER_MODE_SILENT; import static android.media.AudioManager.RINGER_MODE_VIBRATE; import static android.media.AudioManager.STREAM_SYSTEM; +import static android.media.audiopolicy.Flags.enableFadeManagerConfiguration; import static android.os.Process.FIRST_APPLICATION_UID; import static android.os.Process.INVALID_UID; import static android.provider.Settings.Secure.VOLUME_HUSH_MUTE; @@ -116,6 +117,7 @@ import android.media.AudioRoutesInfo; import android.media.AudioSystem; import android.media.AudioTrack; import android.media.BluetoothProfileConnectionInfo; +import android.media.FadeManagerConfiguration; import android.media.IAudioDeviceVolumeDispatcher; import android.media.IAudioFocusDispatcher; import android.media.IAudioModeDispatcher; @@ -4513,6 +4515,8 @@ public class AudioService extends IAudioService.Stub + bluetoothMacAddressAnonymization()); pw.println("\tcom.android.media.audio.disablePrescaleAbsoluteVolume:" + disablePrescaleAbsoluteVolume()); + pw.println("\tandroid.media.audiopolicy.enableFadeManagerConfiguration:" + + enableFadeManagerConfiguration()); } private void dumpAudioMode(PrintWriter pw) { @@ -12614,6 +12618,47 @@ public class AudioService extends IAudioService.Stub } /** + * see {@link AudioPolicy#setFadeManagerConfigurationForFocusLoss(FadeManagerConfiguration)} + */ + @android.annotation.EnforcePermission( + android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public int setFadeManagerConfigurationForFocusLoss( + @NonNull FadeManagerConfiguration fmcForFocusLoss) { + super.setFadeManagerConfigurationForFocusLoss_enforcePermission(); + ensureFadeManagerConfigIsEnabled(); + Objects.requireNonNull(fmcForFocusLoss, + "Fade manager config for focus loss cannot be null"); + validateFadeManagerConfiguration(fmcForFocusLoss); + + return mPlaybackMonitor.setFadeManagerConfiguration(AudioManager.AUDIOFOCUS_LOSS, + fmcForFocusLoss); + } + + /** + * see {@link AudioPolicy#clearFadeManagerConfigurationForFocusLoss()} + */ + @android.annotation.EnforcePermission( + android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public int clearFadeManagerConfigurationForFocusLoss() { + super.clearFadeManagerConfigurationForFocusLoss_enforcePermission(); + ensureFadeManagerConfigIsEnabled(); + + return mPlaybackMonitor.clearFadeManagerConfiguration(AudioManager.AUDIOFOCUS_LOSS); + } + + /** + * see {@link AudioPolicy#getFadeManagerConfigurationForFocusLoss()} + */ + @android.annotation.EnforcePermission( + android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public FadeManagerConfiguration getFadeManagerConfigurationForFocusLoss() { + super.getFadeManagerConfigurationForFocusLoss_enforcePermission(); + ensureFadeManagerConfigIsEnabled(); + + return mPlaybackMonitor.getFadeManagerConfiguration(AudioManager.AUDIOFOCUS_LOSS); + } + + /** * @see AudioManager#getHalVersion */ public @Nullable AudioHalVersionInfo getHalVersion() { @@ -12814,6 +12859,19 @@ public class AudioService extends IAudioService.Stub } } + private void ensureFadeManagerConfigIsEnabled() { + Preconditions.checkState(enableFadeManagerConfiguration(), + "Fade manager configuration not supported"); + } + + private void validateFadeManagerConfiguration(FadeManagerConfiguration fmc) { + // validate permission of audio attributes + List<AudioAttributes> attrs = fmc.getAudioAttributesWithVolumeShaperConfigs(); + for (int index = 0; index < attrs.size(); index++) { + validateAudioAttributesUsage(attrs.get(index)); + } + } + //====================== // Audio policy callbacks from AudioSystem for dynamic policies //====================== @@ -13114,6 +13172,7 @@ public class AudioService extends IAudioService.Stub + "could not link to " + projection + " binder death", e); } } + int status = connectMixes(); if (status != AudioSystem.SUCCESS) { release(); @@ -13471,6 +13530,43 @@ public class AudioService extends IAudioService.Stub } } + /** + * see {@link AudioManager#dispatchAudioFocusChangeWithFade(AudioFocusInfo, int, AudioPolicy, + * List, FadeManagerConfiguration)} + */ + @android.annotation.EnforcePermission( + android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public int dispatchFocusChangeWithFade(AudioFocusInfo afi, int focusChange, + IAudioPolicyCallback pcb, List<AudioFocusInfo> otherActiveAfis, + FadeManagerConfiguration transientFadeMgrConfig) { + super.dispatchFocusChangeWithFade_enforcePermission(); + ensureFadeManagerConfigIsEnabled(); + Objects.requireNonNull(afi, "AudioFocusInfo cannot be null"); + Objects.requireNonNull(pcb, "AudioPolicy callback cannot be null"); + Objects.requireNonNull(otherActiveAfis, + "Other active AudioFocusInfo list cannot be null"); + if (transientFadeMgrConfig != null) { + validateFadeManagerConfiguration(transientFadeMgrConfig); + } + + synchronized (mAudioPolicies) { + Preconditions.checkState(mAudioPolicies.containsKey(pcb.asBinder()), + "Unregistered AudioPolicy for focus dispatch with fade"); + + // set the transient fade manager config to be used for handling this focus change + if (transientFadeMgrConfig != null) { + mPlaybackMonitor.setTransientFadeManagerConfiguration(focusChange, + transientFadeMgrConfig); + } + int status = mMediaFocusControl.dispatchFocusChangeWithFade(afi, focusChange, + otherActiveAfis); + + if (transientFadeMgrConfig != null) { + mPlaybackMonitor.clearTransientFadeManagerConfiguration(focusChange); + } + return status; + } + } //====================== // Audioserver state dispatch @@ -13775,8 +13871,8 @@ public class AudioService extends IAudioService.Stub return activeAssistantUids; } - List<String> getDeviceAddresses(AudioDeviceAttributes device) { - return mDeviceBroker.getDeviceAddresses(device); + List<String> getDeviceIdentityAddresses(AudioDeviceAttributes device) { + return mDeviceBroker.getDeviceIdentityAddresses(device); } @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index 8075618be200..a818c30c2e28 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -58,6 +58,7 @@ import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; +import android.util.Pair; import com.android.internal.annotations.GuardedBy; import com.android.server.utils.EventLogger; @@ -1077,8 +1078,8 @@ public class BtHelper { return mLeAudio.getGroupId(device); } - /*package*/ List<String> getLeAudioGroupAddresses(int groupId) { - List<String> addresses = new ArrayList<String>(); + /*package*/ List<Pair<String, String>> getLeAudioGroupAddresses(int groupId) { + List<Pair<String, String>> addresses = new ArrayList<>(); BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); if (adapter == null || mLeAudio == null) { return addresses; @@ -1086,7 +1087,7 @@ public class BtHelper { List<BluetoothDevice> activeDevices = adapter.getActiveDevices(BluetoothProfile.LE_AUDIO); for (BluetoothDevice device : activeDevices) { if (device != null && mLeAudio.getGroupId(device) == groupId) { - addresses.add(device.getAddress()); + addresses.add(new Pair(device.getAddress(), device.getIdentityAddress())); } } return addresses; diff --git a/services/core/java/com/android/server/audio/FadeConfigurations.java b/services/core/java/com/android/server/audio/FadeConfigurations.java index 2e27c7697e82..37ecf0bdcc7a 100644 --- a/services/core/java/com/android/server/audio/FadeConfigurations.java +++ b/services/core/java/com/android/server/audio/FadeConfigurations.java @@ -16,13 +16,22 @@ package com.android.server.audio; +import static android.media.audiopolicy.Flags.enableFadeManagerConfiguration; + import android.annotation.NonNull; +import android.annotation.Nullable; import android.media.AudioAttributes; +import android.media.AudioManager; import android.media.AudioPlaybackConfiguration; +import android.media.FadeManagerConfiguration; import android.media.VolumeShaper; import android.util.Slog; +import com.android.internal.annotations.GuardedBy; + +import java.util.Collections; import java.util.List; +import java.util.Objects; /** * Class to encapsulate configurations used for fading players @@ -69,51 +78,229 @@ public final class FadeConfigurations { private static final int INVALID_UID = -1; + private final Object mLock = new Object(); + @GuardedBy("mLock") + private FadeManagerConfiguration mDefaultFadeManagerConfig; + @GuardedBy("mLock") + private FadeManagerConfiguration mUpdatedFadeManagerConfig; + @GuardedBy("mLock") + private FadeManagerConfiguration mTransientFadeManagerConfig; + /** active fade manager is one of: transient > updated > default */ + @GuardedBy("mLock") + private FadeManagerConfiguration mActiveFadeManagerConfig; + + /** + * Sets the custom fade manager configuration + * + * @param fadeManagerConfig custom fade manager configuration + * @return {@link AudioManager#SUCCESS} if setting custom fade manager configuration succeeds + * or {@link AudioManager#ERROR} otherwise (example - when fade manager configuration + * feature is disabled) + */ + public int setFadeManagerConfiguration( + @NonNull FadeManagerConfiguration fadeManagerConfig) { + if (!enableFadeManagerConfiguration()) { + return AudioManager.ERROR; + } + + synchronized (mLock) { + mUpdatedFadeManagerConfig = Objects.requireNonNull(fadeManagerConfig, + "Fade manager configuration cannot be null"); + mActiveFadeManagerConfig = getActiveFadeMgrConfigLocked(); + } + return AudioManager.SUCCESS; + } + + /** + * Clears the fade manager configuration that was previously set with + * {@link #setFadeManagerConfiguration(FadeManagerConfiguration)} + * + * @return {@link AudioManager#SUCCESS} if previously set fade manager configuration is cleared + * or {@link AudioManager#ERROR} otherwise (example, when fade manager configuration feature + * is disabled) + */ + public int clearFadeManagerConfiguration() { + if (!enableFadeManagerConfiguration()) { + return AudioManager.ERROR; + } + + synchronized (mLock) { + mUpdatedFadeManagerConfig = null; + mActiveFadeManagerConfig = getActiveFadeMgrConfigLocked(); + } + return AudioManager.SUCCESS; + } + + /** + * Returns the active fade manager configuration + * + * @return {@code null} if feature is disabled, or the custom fade manager configuration if set, + * or default fade manager configuration if not set. + */ + @Nullable + public FadeManagerConfiguration getFadeManagerConfiguration() { + if (!enableFadeManagerConfiguration()) { + return null; + } + + synchronized (mLock) { + return mActiveFadeManagerConfig; + } + } + + /** + * Sets the transient fade manager configuration + * + * @param fadeManagerConfig custom fade manager configuration + * @return {@link AudioManager#SUCCESS} if setting custom fade manager configuration succeeds + * or {@link AudioManager#ERROR} otherwise (example - when fade manager configuration is + * disabled) + */ + public int setTransientFadeManagerConfiguration( + @NonNull FadeManagerConfiguration fadeManagerConfig) { + if (!enableFadeManagerConfiguration()) { + return AudioManager.ERROR; + } + + synchronized (mLock) { + mTransientFadeManagerConfig = Objects.requireNonNull(fadeManagerConfig, + "Transient FadeManagerConfiguration cannot be null"); + mActiveFadeManagerConfig = getActiveFadeMgrConfigLocked(); + } + return AudioManager.SUCCESS; + } + + /** + * Clears the transient fade manager configuration that was previously set with + * {@link #setTransientFadeManagerConfiguration(FadeManagerConfiguration)} + * + * @return {@link AudioManager#SUCCESS} if previously set transient fade manager configuration + * is cleared or {@link AudioManager#ERROR} otherwise (example - when fade manager + * configuration is disabled) + */ + public int clearTransientFadeManagerConfiguration() { + if (!enableFadeManagerConfiguration()) { + return AudioManager.ERROR; + } + synchronized (mLock) { + mTransientFadeManagerConfig = null; + mActiveFadeManagerConfig = getActiveFadeMgrConfigLocked(); + } + return AudioManager.SUCCESS; + } + + /** + * Query if fade should be enforecd on players + * + * @return {@code true} if fade is enabled or using default configurations, {@code false} + * otherwise. + */ + public boolean isFadeEnabled() { + if (!enableFadeManagerConfiguration()) { + return true; + } + + synchronized (mLock) { + return getUpdatedFadeManagerConfigLocked().isFadeEnabled(); + } + } + /** * Query {@link android.media.AudioAttributes.AttributeUsage usages} that are allowed to * fade + * * @return list of {@link android.media.AudioAttributes.AttributeUsage} */ @NonNull public List<Integer> getFadeableUsages() { - return DEFAULT_FADEABLE_USAGES; + if (!enableFadeManagerConfiguration()) { + return DEFAULT_FADEABLE_USAGES; + } + + synchronized (mLock) { + FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked(); + // when fade is not enabled, return an empty list instead + return fadeManagerConfig.isFadeEnabled() ? fadeManagerConfig.getFadeableUsages() + : Collections.EMPTY_LIST; + } } /** * Query {@link android.media.AudioAttributes.AttributeContentType content types} that are * exempted from fade enforcement + * * @return list of {@link android.media.AudioAttributes.AttributeContentType} */ @NonNull public List<Integer> getUnfadeableContentTypes() { - return DEFAULT_UNFADEABLE_CONTENT_TYPES; + if (!enableFadeManagerConfiguration()) { + return DEFAULT_UNFADEABLE_CONTENT_TYPES; + } + + synchronized (mLock) { + FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked(); + // when fade is not enabled, return an empty list instead + return fadeManagerConfig.isFadeEnabled() ? fadeManagerConfig.getUnfadeableContentTypes() + : Collections.EMPTY_LIST; + } } /** * Query {@link android.media.AudioPlaybackConfiguration.PlayerType player types} that are * exempted from fade enforcement + * * @return list of {@link android.media.AudioPlaybackConfiguration.PlayerType} */ @NonNull public List<Integer> getUnfadeablePlayerTypes() { - return DEFAULT_UNFADEABLE_PLAYER_TYPES; + if (!enableFadeManagerConfiguration()) { + return DEFAULT_UNFADEABLE_PLAYER_TYPES; + } + + synchronized (mLock) { + FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked(); + // when fade is not enabled, return an empty list instead + return fadeManagerConfig.isFadeEnabled() ? fadeManagerConfig.getUnfadeablePlayerTypes() + : Collections.EMPTY_LIST; + } } /** * Get the {@link android.media.VolumeShaper.Configuration} configuration to be applied * for the fade-out + * * @param aa The {@link android.media.AudioAttributes} * @return {@link android.media.VolumeShaper.Configuration} for the - * {@link android.media.AudioAttributes.AttributeUsage} or default volume shaper if not - * configured + * {@link android.media.AudioAttributes} or default volume shaper if not configured */ @NonNull public VolumeShaper.Configuration getFadeOutVolumeShaperConfig(@NonNull AudioAttributes aa) { - return DEFAULT_FADEOUT_VSHAPE; + if (!enableFadeManagerConfiguration()) { + return DEFAULT_FADEOUT_VSHAPE; + } + return getOptimalFadeOutVolShaperConfig(aa); + } + + /** + * Get the {@link android.media.VolumeShaper.Configuration} configuration to be applied for the + * fade in + * + * @param aa The {@link android.media.AudioAttributes} + * @return {@link android.media.VolumeShaper.Configuration} for the + * {@link android.media.AudioAttributes} or {@code null} otherwise + */ + @Nullable + public VolumeShaper.Configuration getFadeInVolumeShaperConfig(@NonNull AudioAttributes aa) { + if (!enableFadeManagerConfiguration()) { + return null; + } + return getOptimalFadeInVolShaperConfig(aa); } + /** * Get the duration to fade out a player of type usage + * * @param aa The {@link android.media.AudioAttributes} * @return duration in milliseconds for the * {@link android.media.AudioAttributes} or default duration if not configured @@ -122,22 +309,73 @@ public final class FadeConfigurations { if (!isFadeable(aa, INVALID_UID, AudioPlaybackConfiguration.PLAYER_TYPE_UNKNOWN)) { return 0; } - return DEFAULT_FADE_OUT_DURATION_MS; + if (!enableFadeManagerConfiguration()) { + return DEFAULT_FADE_OUT_DURATION_MS; + } + return getOptimalFadeOutDuration(aa); } /** - * Get the delay to fade in offending players that do not stop after losing audio focus. + * Get the delay to fade in offending players that do not stop after losing audio focus + * * @param aa The {@link android.media.AudioAttributes} * @return delay in milliseconds for the * {@link android.media.AudioAttributes.Attribute} or default delay if not configured */ public long getDelayFadeInOffenders(@NonNull AudioAttributes aa) { - return DEFAULT_DELAY_FADE_IN_OFFENDERS_MS; + if (!enableFadeManagerConfiguration()) { + return DEFAULT_DELAY_FADE_IN_OFFENDERS_MS; + } + + synchronized (mLock) { + return getUpdatedFadeManagerConfigLocked().getFadeInDelayForOffenders(); + } + } + + /** + * Query {@link android.media.AudioAttributes} that are exempted from fade enforcement + * + * @return list of {@link android.media.AudioAttributes} + */ + @NonNull + public List<AudioAttributes> getUnfadeableAudioAttributes() { + // unfadeable audio attributes is only supported with fade manager configurations + if (!enableFadeManagerConfiguration()) { + return Collections.EMPTY_LIST; + } + + synchronized (mLock) { + FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked(); + // when fade is not enabled, return empty list + return fadeManagerConfig.isFadeEnabled() + ? fadeManagerConfig.getUnfadeableAudioAttributes() : Collections.EMPTY_LIST; + } + } + + /** + * Query uids that are exempted from fade enforcement + * + * @return list of uids + */ + @NonNull + public List<Integer> getUnfadeableUids() { + // unfadeable uids is only supported with fade manager configurations + if (!enableFadeManagerConfiguration()) { + return Collections.EMPTY_LIST; + } + + synchronized (mLock) { + FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked(); + // when fade is not enabled, return empty list + return fadeManagerConfig.isFadeEnabled() ? fadeManagerConfig.getUnfadeableUids() + : Collections.EMPTY_LIST; + } } /** * Check if it is allowed to fade for the given {@link android.media.AudioAttributes}, - * client uid and {@link android.media.AudioPlaybackConfiguration.PlayerType} config. + * client uid and {@link android.media.AudioPlaybackConfiguration.PlayerType} config + * * @param aa The {@link android.media.AudioAttributes} * @param uid The uid of the client owning the player * @param playerType The {@link android.media.AudioPlaybackConfiguration.PlayerType} @@ -145,36 +383,173 @@ public final class FadeConfigurations { */ public boolean isFadeable(@NonNull AudioAttributes aa, int uid, @AudioPlaybackConfiguration.PlayerType int playerType) { - if (isPlayerTypeUnfadeable(playerType)) { - if (DEBUG) { - Slog.i(TAG, "not fadeable: player type:" + playerType); + synchronized (mLock) { + if (isPlayerTypeUnfadeableLocked(playerType)) { + if (DEBUG) { + Slog.i(TAG, "not fadeable: player type:" + playerType); + } + return false; } - return false; + if (isContentTypeUnfadeableLocked(aa.getContentType())) { + if (DEBUG) { + Slog.i(TAG, "not fadeable: content type:" + aa.getContentType()); + } + return false; + } + if (!isUsageFadeableLocked(aa.getSystemUsage())) { + if (DEBUG) { + Slog.i(TAG, "not fadeable: usage:" + aa.getUsage()); + } + return false; + } + // new configs using fade manager configuration + if (isUnfadeableForFadeMgrConfigLocked(aa, uid)) { + return false; + } + return true; + } + } + + /** Tries to get the fade out volume shaper config closest to the audio attributes */ + private VolumeShaper.Configuration getOptimalFadeOutVolShaperConfig(AudioAttributes aa) { + synchronized (mLock) { + FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked(); + // check if the specific audio attributes has a volume shaper config defined + VolumeShaper.Configuration volShaperConfig = + fadeManagerConfig.getFadeOutVolumeShaperConfigForAudioAttributes(aa); + if (volShaperConfig != null) { + return volShaperConfig; + } + + // get the volume shaper config for usage + // for fadeable usages, this should never return null + return fadeManagerConfig.getFadeOutVolumeShaperConfigForUsage( + aa.getSystemUsage()); } - if (isContentTypeUnfadeable(aa.getContentType())) { + } + + /** Tries to get the fade in volume shaper config closest to the audio attributes */ + private VolumeShaper.Configuration getOptimalFadeInVolShaperConfig(AudioAttributes aa) { + synchronized (mLock) { + FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked(); + // check if the specific audio attributes has a volume shaper config defined + VolumeShaper.Configuration volShaperConfig = + fadeManagerConfig.getFadeInVolumeShaperConfigForAudioAttributes(aa); + if (volShaperConfig != null) { + return volShaperConfig; + } + + // get the volume shaper config for usage + // for fadeable usages, this should never return null + return fadeManagerConfig.getFadeInVolumeShaperConfigForUsage(aa.getSystemUsage()); + } + } + + /** Tries to get the duration closest to the audio attributes */ + private long getOptimalFadeOutDuration(AudioAttributes aa) { + synchronized (mLock) { + FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked(); + // check if specific audio attributes has a duration defined + long duration = fadeManagerConfig.getFadeOutDurationForAudioAttributes(aa); + if (duration != FadeManagerConfiguration.DURATION_NOT_SET) { + return duration; + } + + // get the duration for usage + // for fadeable usages, this should never return DURATION_NOT_SET + return fadeManagerConfig.getFadeOutDurationForUsage(aa.getSystemUsage()); + } + } + + @GuardedBy("mLock") + private boolean isUnfadeableForFadeMgrConfigLocked(AudioAttributes aa, int uid) { + if (isAudioAttributesUnfadeableLocked(aa)) { if (DEBUG) { - Slog.i(TAG, "not fadeable: content type:" + aa.getContentType()); + Slog.i(TAG, "not fadeable: aa:" + aa); } - return false; + return true; } - if (!isUsageFadeable(aa.getUsage())) { + if (isUidUnfadeableLocked(uid)) { if (DEBUG) { - Slog.i(TAG, "not fadeable: usage:" + aa.getUsage()); + Slog.i(TAG, "not fadeable: uid:" + uid); } + return true; + } + return false; + } + + @GuardedBy("mLock") + private boolean isUsageFadeableLocked(int usage) { + if (!enableFadeManagerConfiguration()) { + return DEFAULT_FADEABLE_USAGES.contains(usage); + } + return getUpdatedFadeManagerConfigLocked().isUsageFadeable(usage); + } + + @GuardedBy("mLock") + private boolean isContentTypeUnfadeableLocked(int contentType) { + if (!enableFadeManagerConfiguration()) { + return DEFAULT_UNFADEABLE_CONTENT_TYPES.contains(contentType); + } + return getUpdatedFadeManagerConfigLocked().isContentTypeUnfadeable(contentType); + } + + @GuardedBy("mLock") + private boolean isPlayerTypeUnfadeableLocked(int playerType) { + if (!enableFadeManagerConfiguration()) { + return DEFAULT_UNFADEABLE_PLAYER_TYPES.contains(playerType); + } + return getUpdatedFadeManagerConfigLocked().isPlayerTypeUnfadeable(playerType); + } + + @GuardedBy("mLock") + private boolean isAudioAttributesUnfadeableLocked(AudioAttributes aa) { + if (!enableFadeManagerConfiguration()) { + // default fade configs do not support unfadeable audio attributes, hence return false return false; } - return true; + return getUpdatedFadeManagerConfigLocked().isAudioAttributesUnfadeable(aa); } - private boolean isUsageFadeable(int usage) { - return getFadeableUsages().contains(usage); + @GuardedBy("mLock") + private boolean isUidUnfadeableLocked(int uid) { + if (!enableFadeManagerConfiguration()) { + // default fade configs do not support unfadeable uids, hence return false + return false; + } + return getUpdatedFadeManagerConfigLocked().isUidUnfadeable(uid); } - private boolean isContentTypeUnfadeable(int contentType) { - return getUnfadeableContentTypes().contains(contentType); + @GuardedBy("mLock") + private FadeManagerConfiguration getUpdatedFadeManagerConfigLocked() { + if (mActiveFadeManagerConfig == null) { + mActiveFadeManagerConfig = getActiveFadeMgrConfigLocked(); + } + return mActiveFadeManagerConfig; } - private boolean isPlayerTypeUnfadeable(int playerType) { - return getUnfadeablePlayerTypes().contains(playerType); + /** Priority between fade manager configs: Transient > Updated > Default */ + @GuardedBy("mLock") + private FadeManagerConfiguration getActiveFadeMgrConfigLocked() { + // below configs are arranged in the order of priority + // configs placed higher have higher priority + if (mTransientFadeManagerConfig != null) { + return mTransientFadeManagerConfig; + } + + if (mUpdatedFadeManagerConfig != null) { + return mUpdatedFadeManagerConfig; + } + + // default - must be the lowest priority + return getDefaultFadeManagerConfigLocked(); + } + + @GuardedBy("mLock") + private FadeManagerConfiguration getDefaultFadeManagerConfigLocked() { + if (mDefaultFadeManagerConfig == null) { + mDefaultFadeManagerConfig = new FadeManagerConfiguration.Builder().build(); + } + return mDefaultFadeManagerConfig; } } diff --git a/services/core/java/com/android/server/audio/FadeOutManager.java b/services/core/java/com/android/server/audio/FadeOutManager.java index 1171f97533c7..2cceb5ab5d2e 100644 --- a/services/core/java/com/android/server/audio/FadeOutManager.java +++ b/services/core/java/com/android/server/audio/FadeOutManager.java @@ -16,21 +16,24 @@ package com.android.server.audio; +import static android.media.audiopolicy.Flags.enableFadeManagerConfiguration; + import android.annotation.NonNull; +import android.annotation.Nullable; import android.media.AudioAttributes; import android.media.AudioManager; import android.media.AudioPlaybackConfiguration; +import android.media.FadeManagerConfiguration; import android.media.VolumeShaper; import android.util.Slog; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; -import com.android.server.utils.EventLogger; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.HashMap; -import java.util.Objects; +import java.util.List; +import java.util.Map; /** * Class to handle fading out players @@ -40,14 +43,6 @@ public final class FadeOutManager { public static final String TAG = "AS.FadeOutManager"; private static final boolean DEBUG = PlaybackActivityMonitor.DEBUG; - private static final VolumeShaper.Operation PLAY_CREATE_IF_NEEDED = - new VolumeShaper.Operation.Builder(VolumeShaper.Operation.PLAY) - .createIfNeeded() - .build(); - - // like a PLAY_CREATE_IF_NEEDED operation but with a skip to the end of the ramp - private static final VolumeShaper.Operation PLAY_SKIP_RAMP = - new VolumeShaper.Operation.Builder(PLAY_CREATE_IF_NEEDED).setXOffset(1.0f).build(); private final Object mLock = new Object(); @@ -57,16 +52,81 @@ public final class FadeOutManager { @GuardedBy("mLock") private final SparseArray<FadedOutApp> mUidToFadedAppsMap = new SparseArray<>(); - @GuardedBy("mLock") - private FadeConfigurations mFadeConfigurations; + private final FadeConfigurations mFadeConfigurations = new FadeConfigurations(); + + /** + * Sets the custom fade manager configuration to be used for player fade out and in + * + * @param fadeManagerConfig custom fade manager configuration + * @return {@link AudioManager#SUCCESS} if setting fade manager config succeeded, + * {@link AudioManager#ERROR} otherwise + */ + int setFadeManagerConfiguration(FadeManagerConfiguration fadeManagerConfig) { + // locked to ensure the fade configs are not updated while faded app state is being updated + synchronized (mLock) { + return mFadeConfigurations.setFadeManagerConfiguration(fadeManagerConfig); + } + } - public FadeOutManager() { - mFadeConfigurations = new FadeConfigurations(); + /** + * Clears the fade manager configuration that was previously set with + * {@link #setFadeManagerConfiguration(FadeManagerConfiguration)} + * + * @return {@link AudioManager#SUCCESS} if clearing fade manager config succeeded, + * {@link AudioManager#ERROR} otherwise + */ + int clearFadeManagerConfiguration() { + // locked to ensure the fade configs are not updated while faded app state is being updated + synchronized (mLock) { + return mFadeConfigurations.clearFadeManagerConfiguration(); + } } - public FadeOutManager(FadeConfigurations fadeConfigurations) { - mFadeConfigurations = Objects.requireNonNull(fadeConfigurations, - "Fade configurations can not be null"); + /** + * Returns the active fade manager configuration + * + * @return the {@link FadeManagerConfiguration} + */ + FadeManagerConfiguration getFadeManagerConfiguration() { + return mFadeConfigurations.getFadeManagerConfiguration(); + } + + /** + * Sets the transient fade manager configuration to be used for player fade out and in + * + * @param fadeManagerConfig fade manager config that has higher priority than the existing + * fade manager configuration. This is expected to be transient. + * @return {@link AudioManager#SUCCESS} if setting fade manager config succeeded, + * {@link AudioManager#ERROR} otherwise + */ + int setTransientFadeManagerConfiguration(FadeManagerConfiguration fadeManagerConfig) { + // locked to ensure the fade configs are not updated while faded app state is being updated + synchronized (mLock) { + return mFadeConfigurations.setTransientFadeManagerConfiguration(fadeManagerConfig); + } + } + + /** + * Clears the transient fade manager configuration that was previously set with + * {@link #setTransientFadeManagerConfiguration(FadeManagerConfiguration)} + * + * @return {@link AudioManager#SUCCESS} if clearing fade manager config succeeded, + * {@link AudioManager#ERROR} otherwise + */ + int clearTransientFadeManagerConfiguration() { + // locked to ensure the fade configs are not updated while faded app state is being updated + synchronized (mLock) { + return mFadeConfigurations.clearTransientFadeManagerConfiguration(); + } + } + + /** + * Query if fade is enblead and can be enforced on players + * + * @return {@code true} if fade is enabled, {@code false} otherwise. + */ + boolean isFadeEnabled() { + return mFadeConfigurations.isFadeEnabled(); } // TODO explore whether a shorter fade out would be a better UX instead of not fading out at all @@ -128,7 +188,7 @@ public final class FadeOutManager { } } - void fadeOutUid(int uid, ArrayList<AudioPlaybackConfiguration> players) { + void fadeOutUid(int uid, List<AudioPlaybackConfiguration> players) { Slog.i(TAG, "fadeOutUid() uid:" + uid); synchronized (mLock) { if (!mUidToFadedAppsMap.contains(uid)) { @@ -148,15 +208,31 @@ public final class FadeOutManager { * @param uid the uid for the app to unfade out * @param players map of current available players (so we can get an APC from piid) */ - void unfadeOutUid(int uid, HashMap<Integer, AudioPlaybackConfiguration> players) { + void unfadeOutUid(int uid, Map<Integer, AudioPlaybackConfiguration> players) { Slog.i(TAG, "unfadeOutUid() uid:" + uid); synchronized (mLock) { - final FadedOutApp fa = mUidToFadedAppsMap.get(uid); + FadedOutApp fa = mUidToFadedAppsMap.get(uid); if (fa == null) { return; } mUidToFadedAppsMap.remove(uid); - fa.removeUnfadeAll(players); + + if (!enableFadeManagerConfiguration()) { + fa.removeUnfadeAll(players); + return; + } + + // since fade manager configs may have volume-shaper config per audio attributes, + // iterate through each palyer and gather respective configs for fade in + ArrayList<AudioPlaybackConfiguration> apcs = new ArrayList<>(players.values()); + for (int index = 0; index < apcs.size(); index++) { + AudioPlaybackConfiguration apc = apcs.get(index); + VolumeShaper.Configuration config = mFadeConfigurations + .getFadeInVolumeShaperConfig(apc.getAudioAttributes()); + fa.fadeInPlayer(apc, config); + } + // ideal case all players should be faded in + fa.clear(); } } @@ -209,16 +285,6 @@ public final class FadeOutManager { } } - /** - * Update fade configurations used for player fade operations - * @param fadeConfigurations set of configs that define fade properties - */ - void setFadeConfigurations(@NonNull FadeConfigurations fadeConfigurations) { - synchronized (mLock) { - mFadeConfigurations = fadeConfigurations; - } - } - void dump(PrintWriter pw) { synchronized (mLock) { for (int index = 0; index < mUidToFadedAppsMap.size(); index++) { @@ -232,6 +298,15 @@ public final class FadeOutManager { * Class to group players from a common app, that are faded out. */ private static final class FadedOutApp { + private static final VolumeShaper.Operation PLAY_CREATE_IF_NEEDED = + new VolumeShaper.Operation.Builder(VolumeShaper.Operation.PLAY) + .createIfNeeded() + .build(); + + // like a PLAY_CREATE_IF_NEEDED operation but with a skip to the end of the ramp + private static final VolumeShaper.Operation PLAY_SKIP_RAMP = + new VolumeShaper.Operation.Builder(PLAY_CREATE_IF_NEEDED).setXOffset(1.0f).build(); + private final int mUid; // key -> piid; value -> volume shaper config applied private final SparseArray<VolumeShaper.Configuration> mFadedPlayers = new SparseArray<>(); @@ -269,17 +344,8 @@ public final class FadeOutManager { return; } if (apc.getPlayerProxy() != null) { - try { - PlaybackActivityMonitor.sEventLogger.enqueue( - (new PlaybackActivityMonitor.FadeOutEvent(apc, skipRamp)).printLog( - TAG)); - apc.getPlayerProxy().applyVolumeShaper(volShaper, - skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED); - mFadedPlayers.put(piid, volShaper); - } catch (Exception e) { - Slog.e(TAG, "Error fading out player piid:" + piid - + " uid:" + apc.getClientUid(), e); - } + applyVolumeShaperInternal(apc, piid, volShaper, + skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED); } else { if (DEBUG) { Slog.v(TAG, "Error fading out player piid:" + piid @@ -288,21 +354,13 @@ public final class FadeOutManager { } } - void removeUnfadeAll(HashMap<Integer, AudioPlaybackConfiguration> players) { + void removeUnfadeAll(Map<Integer, AudioPlaybackConfiguration> players) { for (int index = 0; index < mFadedPlayers.size(); index++) { int piid = mFadedPlayers.keyAt(index); final AudioPlaybackConfiguration apc = players.get(piid); if ((apc != null) && (apc.getPlayerProxy() != null)) { - final VolumeShaper.Configuration volShaper = mFadedPlayers.valueAt(index); - try { - PlaybackActivityMonitor.sEventLogger.enqueue( - (new EventLogger.StringEvent("unfading out piid:" - + piid)).printLog(TAG)); - apc.getPlayerProxy().applyVolumeShaper(volShaper, - VolumeShaper.Operation.REVERSE); - } catch (Exception e) { - Slog.e(TAG, "Error unfading out player piid:" + piid + " uid:" + mUid, e); - } + applyVolumeShaperInternal(apc, piid, /* volShaperConfig= */ null, + VolumeShaper.Operation.REVERSE); } else { // this piid was in the list of faded players, but wasn't found if (DEBUG) { @@ -314,8 +372,61 @@ public final class FadeOutManager { mFadedPlayers.clear(); } + void fadeInPlayer(@NonNull AudioPlaybackConfiguration apc, + @Nullable VolumeShaper.Configuration config) { + int piid = Integer.valueOf(apc.getPlayerInterfaceId()); + // if not found, no need to fade in since it was never faded out + if (!mFadedPlayers.contains(piid)) { + if (DEBUG) { + Slog.v(TAG, "Player (piid: " + piid + ") for uid (" + mUid + + ") is not faded out, no need to fade in"); + } + return; + } + + mFadedPlayers.remove(piid); + if (apc.getPlayerProxy() != null) { + applyVolumeShaperInternal(apc, piid, config, + config != null ? PLAY_CREATE_IF_NEEDED : VolumeShaper.Operation.REVERSE); + } else { + if (DEBUG) { + Slog.v(TAG, "Error fading in player piid:" + piid + + ", player not found for uid " + mUid); + } + } + } + + void clear() { + if (mFadedPlayers.size() > 0) { + if (DEBUG) { + Slog.v(TAG, "Non empty faded players list being cleared! Faded out players:" + + mFadedPlayers); + } + } + // should the players be faded in irrespective? + mFadedPlayers.clear(); + } + void removeReleased(@NonNull AudioPlaybackConfiguration apc) { mFadedPlayers.delete(Integer.valueOf(apc.getPlayerInterfaceId())); } + + private void applyVolumeShaperInternal(AudioPlaybackConfiguration apc, int piid, + VolumeShaper.Configuration volShaperConfig, VolumeShaper.Operation operation) { + VolumeShaper.Configuration config = volShaperConfig; + // when operation is reverse, use the fade out volume shaper config instead + if (operation.equals(VolumeShaper.Operation.REVERSE)) { + config = mFadedPlayers.get(piid); + } + try { + PlaybackActivityMonitor.sEventLogger.enqueue( + (new PlaybackActivityMonitor.FadeEvent(apc, config, operation)) + .printLog(TAG)); + apc.getPlayerProxy().applyVolumeShaper(config, operation); + } catch (Exception e) { + Slog.e(TAG, "Error fading player piid:" + piid + " uid:" + mUid + + " operation:" + operation, e); + } + } } } diff --git a/services/core/java/com/android/server/audio/FocusRequester.java b/services/core/java/com/android/server/audio/FocusRequester.java index 00c04ff12c89..f462539d5bbf 100644 --- a/services/core/java/com/android/server/audio/FocusRequester.java +++ b/services/core/java/com/android/server/audio/FocusRequester.java @@ -33,6 +33,7 @@ import com.android.server.audio.MediaFocusControl.AudioFocusDeathHandler; import com.android.server.pm.UserManagerInternal; import java.io.PrintWriter; +import java.util.List; /** * @hide @@ -534,6 +535,33 @@ public class FocusRequester { return AudioManager.AUDIOFOCUS_REQUEST_GRANTED; } + @GuardedBy("MediaFocusControl.mAudioFocusLock") + int dispatchFocusChangeWithFadeLocked(int focusChange, List<FocusRequester> otherActiveFrs) { + if (focusChange == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK + || focusChange == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE + || focusChange == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT + || focusChange == AudioManager.AUDIOFOCUS_GAIN) { + mFocusLossFadeLimbo = false; + mFocusController.restoreVShapedPlayers(this); + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS + && mFocusController.shouldEnforceFade()) { + for (int index = 0; index < otherActiveFrs.size(); index++) { + // candidate for fade-out before a receiving a loss + if (mFocusController.fadeOutPlayers(otherActiveFrs.get(index), /* loser= */ this)) { + // active players are being faded out, delay the dispatch of focus loss + // mark this instance as being faded so it's not released yet as the focus loss + // will be dispatched later, it is now in limbo mode + mFocusLossFadeLimbo = true; + mFocusController.postDelayedLossAfterFade(this, + mFocusController.getFadeOutDurationOnFocusLossMillis( + this.getAudioAttributes())); + return AudioManager.AUDIOFOCUS_REQUEST_DELAYED; + } + } + } + return dispatchFocusChange(focusChange); + } + void dispatchFocusResultFromExtPolicy(int requestResult) { final IAudioFocusDispatcher fd = mFocusDispatcher; if (fd == null) { diff --git a/services/core/java/com/android/server/audio/MediaFocusControl.java b/services/core/java/com/android/server/audio/MediaFocusControl.java index 58f5d5e21cf0..0df0006c7be3 100644 --- a/services/core/java/com/android/server/audio/MediaFocusControl.java +++ b/services/core/java/com/android/server/audio/MediaFocusControl.java @@ -16,6 +16,8 @@ package com.android.server.audio; +import static android.media.audiopolicy.Flags.enableFadeManagerConfiguration; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AppOpsManager; @@ -195,6 +197,15 @@ public class MediaFocusControl implements PlayerFocusEnforcer { } return mFocusEnforcer.getFadeInDelayForOffendersMillis(aa); } + + @Override + public boolean shouldEnforceFade() { + if (!enableFadeManagerConfiguration()) { + return ENFORCE_FADEOUT_FOR_FOCUS_LOSS; + } + + return mFocusEnforcer.shouldEnforceFade(); + } //========================================================================================== // AudioFocus //========================================================================================== @@ -861,14 +872,17 @@ public class MediaFocusControl implements PlayerFocusEnforcer { return; } } - final FocusRequester fr; - if (requestResult == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { - fr = mFocusOwnersForFocusPolicy.remove(afi.getClientId()); - } else { - fr = mFocusOwnersForFocusPolicy.get(afi.getClientId()); - } - if (fr != null) { - fr.dispatchFocusResultFromExtPolicy(requestResult); + synchronized (mAudioFocusLock) { + FocusRequester fr = getFocusRequesterLocked(afi.getClientId(), + /* shouldRemove= */ requestResult == AudioManager.AUDIOFOCUS_REQUEST_FAILED); + if (fr != null) { + fr.dispatchFocusResultFromExtPolicy(requestResult); + // if fade is enabled for external focus policies, apply it when setting + // focus result as well + if (enableFadeManagerConfiguration()) { + fr.handleFocusGainFromRequest(requestResult); + } + } } } @@ -902,22 +916,78 @@ public class MediaFocusControl implements PlayerFocusEnforcer { + afi.getClientId()); } synchronized (mAudioFocusLock) { - if (mFocusPolicy == null) { - if (DEBUG) { Log.v(TAG, "> failed: no focus policy" ); } + FocusRequester fr = getFocusRequesterLocked(afi.getClientId(), + /* shouldRemove= */ focusChange == AudioManager.AUDIOFOCUS_LOSS); + if (fr == null) { + if (DEBUG) { + Log.v(TAG, "> failed: no such focus requester known"); + } return AudioManager.AUDIOFOCUS_REQUEST_FAILED; } - final FocusRequester fr; - if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { - fr = mFocusOwnersForFocusPolicy.remove(afi.getClientId()); - } else { - fr = mFocusOwnersForFocusPolicy.get(afi.getClientId()); - } + return fr.dispatchFocusChange(focusChange); + } + } + + int dispatchFocusChangeWithFade(AudioFocusInfo afi, int focusChange, + List<AudioFocusInfo> otherActiveAfis) { + if (DEBUG) { + Log.v(TAG, "dispatchFocusChangeWithFade " + AudioManager.audioFocusToString(focusChange) + + " to afi client=" + afi.getClientId() + + " other active afis=" + otherActiveAfis); + } + + synchronized (mAudioFocusLock) { + String clientId = afi.getClientId(); + // do not remove the entry since it can be posted for fade + FocusRequester fr = getFocusRequesterLocked(clientId, /* shouldRemove= */ false); if (fr == null) { - if (DEBUG) { Log.v(TAG, "> failed: no such focus requester known" ); } + if (DEBUG) { + Log.v(TAG, "> failed: no such focus requester known"); + } return AudioManager.AUDIOFOCUS_REQUEST_FAILED; } - return fr.dispatchFocusChange(focusChange); + + // convert other AudioFocusInfo to corresponding FocusRequester + ArrayList<FocusRequester> otherActiveFrs = new ArrayList<>(); + for (int index = 0; index < otherActiveAfis.size(); index++) { + FocusRequester otherFr = getFocusRequesterLocked( + otherActiveAfis.get(index).getClientId(), /* shouldRemove= */ false); + if (otherFr == null) { + continue; + } + otherActiveFrs.add(otherFr); + } + + int status = fr.dispatchFocusChangeWithFadeLocked(focusChange, otherActiveFrs); + if (status != AudioManager.AUDIOFOCUS_REQUEST_DELAYED + && focusChange == AudioManager.AUDIOFOCUS_LOSS) { + mFocusOwnersForFocusPolicy.remove(clientId); + } + + return status; + } + } + + @GuardedBy("mAudioFocusLock") + private FocusRequester getFocusRequesterLocked(String clientId, boolean shouldRemove) { + if (mFocusPolicy == null) { + if (DEBUG) { + Log.v(TAG, "> failed: no focus policy"); + } + return null; + } + + FocusRequester fr; + if (shouldRemove) { + fr = mFocusOwnersForFocusPolicy.remove(clientId); + } else { + fr = mFocusOwnersForFocusPolicy.get(clientId); + } + + if (fr == null && DEBUG) { + Log.v(TAG, "> failed: no such focus requester known"); } + return fr; } private void dumpExtFocusPolicyFocusOwners(PrintWriter pw) { diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java index bc9b9b4b1c88..e69fbbd2a083 100644 --- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java +++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java @@ -38,6 +38,7 @@ import android.media.AudioPlaybackConfiguration; import android.media.AudioPlaybackConfiguration.FormatInfo; import android.media.AudioPlaybackConfiguration.PlayerMuteEvent; import android.media.AudioSystem; +import android.media.FadeManagerConfiguration; import android.media.IPlaybackConfigDispatcher; import android.media.PlayerBase; import android.media.VolumeShaper; @@ -156,8 +157,7 @@ public final class PlaybackActivityMonitor private final int mMaxAlarmVolume; private int mPrivilegedAlarmActiveCount = 0; private final Consumer<AudioDeviceAttributes> mMuteAwaitConnectionTimeoutCb; - private final FadeOutManager mFadeOutManager; - + private final FadeOutManager mFadeOutManager = new FadeOutManager(); PlaybackActivityMonitor(Context context, int maxAlarmVolume, Consumer<AudioDeviceAttributes> muteTimeoutCallback) { @@ -167,7 +167,6 @@ public final class PlaybackActivityMonitor AudioPlaybackConfiguration.sPlayerDeathMonitor = this; mMuteAwaitConnectionTimeoutCb = muteTimeoutCallback; initEventHandler(); - mFadeOutManager = new FadeOutManager(new FadeConfigurations()); } //================================================================= @@ -971,6 +970,12 @@ public final class PlaybackActivityMonitor return mFadeOutManager.getFadeInDelayForOffendersMillis(aa); } + @Override + public boolean shouldEnforceFade() { + return mFadeOutManager.isFadeEnabled(); + } + + //================================================================= // Track playback activity listeners @@ -1010,6 +1015,27 @@ public final class PlaybackActivityMonitor } } + int setFadeManagerConfiguration(int focusType, FadeManagerConfiguration fadeMgrConfig) { + return mFadeOutManager.setFadeManagerConfiguration(fadeMgrConfig); + } + + int clearFadeManagerConfiguration(int focusType) { + return mFadeOutManager.clearFadeManagerConfiguration(); + } + + FadeManagerConfiguration getFadeManagerConfiguration(int focusType) { + return mFadeOutManager.getFadeManagerConfiguration(); + } + + int setTransientFadeManagerConfiguration(int focusType, + FadeManagerConfiguration fadeMgrConfig) { + return mFadeOutManager.setTransientFadeManagerConfiguration(fadeMgrConfig); + } + + int clearTransientFadeManagerConfiguration(int focusType) { + return mFadeOutManager.clearTransientFadeManagerConfiguration(); + } + /** * Inner class to track clients that want to be notified of playback updates */ @@ -1337,6 +1363,38 @@ public final class PlaybackActivityMonitor } } + static final class FadeEvent extends EventLogger.Event { + private final int mPlayerIId; + private final int mPlayerType; + private final int mClientUid; + private final int mClientPid; + private final AudioAttributes mPlayerAttr; + private final VolumeShaper.Configuration mVShaper; + private final VolumeShaper.Operation mVOperation; + + FadeEvent(AudioPlaybackConfiguration apc, VolumeShaper.Configuration vShaper, + VolumeShaper.Operation vOperation) { + mPlayerIId = apc.getPlayerInterfaceId(); + mClientUid = apc.getClientUid(); + mClientPid = apc.getClientPid(); + mPlayerAttr = apc.getAudioAttributes(); + mPlayerType = apc.getPlayerType(); + mVShaper = vShaper; + mVOperation = vOperation; + } + + @Override + public String eventToString() { + return "Fade Event:" + " player piid:" + mPlayerIId + + " uid/pid:" + mClientUid + "/" + mClientPid + + " player type:" + + AudioPlaybackConfiguration.toLogFriendlyPlayerType(mPlayerType) + + " attr:" + mPlayerAttr + + " volume shaper:" + mVShaper + + " volume operation:" + mVOperation; + } + } + private abstract static class VolumeShaperEvent extends EventLogger.Event { private final int mPlayerIId; private final boolean mSkipRamp; diff --git a/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java b/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java index f1d42f3571a9..4a29eca5eef7 100644 --- a/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java +++ b/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java @@ -79,4 +79,11 @@ public interface PlayerFocusEnforcer { * @return delay in milliseconds */ long getFadeInDelayForOffendersMillis(@NonNull AudioAttributes aa); + + /** + * Check if the fade should be enforced + * + * @return {@code true} if fade should be enforced, {@code false} otherwise + */ + boolean shouldEnforceFade(); }
\ No newline at end of file diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index 4f7f31dfa7dc..8428f127e77d 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -1639,8 +1639,7 @@ public class SpatializerHelper { return -1; } final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0); - List<String> deviceAddresses = mAudioService.getDeviceAddresses(currentDevice); - + List<String> deviceAddresses = mAudioService.getDeviceIdentityAddresses(currentDevice); // We limit only to Sensor.TYPE_HEAD_TRACKER here to avoid confusion // with gaming sensors. (Note that Sensor.TYPE_ROTATION_VECTOR // and Sensor.TYPE_GAME_ROTATION_VECTOR are supported internally by diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java index dafea9a199fd..d5d8fd22314b 100644 --- a/services/core/java/com/android/server/biometrics/AuthService.java +++ b/services/core/java/com/android/server/biometrics/AuthService.java @@ -51,9 +51,13 @@ import android.hardware.biometrics.ITestSessionCallback; import android.hardware.biometrics.PromptInfo; import android.hardware.biometrics.SensorLocationInternal; import android.hardware.biometrics.SensorPropertiesInternal; +import android.hardware.biometrics.face.IFace; +import android.hardware.biometrics.fingerprint.IFingerprint; +import android.hardware.face.FaceSensorConfigurations; import android.hardware.face.FaceSensorProperties; import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.face.IFaceService; +import android.hardware.fingerprint.FingerprintSensorConfigurations; import android.hardware.fingerprint.FingerprintSensorProperties; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IFingerprintService; @@ -122,8 +126,6 @@ public class AuthService extends SystemService { /** * Allows to test with various device sensor configurations. - * @param context - * @return */ @VisibleForTesting public String[] getConfiguration(Context context) { @@ -131,6 +133,30 @@ public class AuthService extends SystemService { } /** + * Allows to test with various device sensor configurations. + */ + @VisibleForTesting + public String[] getFingerprintConfiguration(Context context) { + return getConfiguration(context); + } + + /** + * Allows to test with various device sensor configurations. + */ + @VisibleForTesting + public String[] getFaceConfiguration(Context context) { + return getConfiguration(context); + } + + /** + * Allows to test with various device sensor configurations. + */ + @VisibleForTesting + public String[] getIrisConfiguration(Context context) { + return getConfiguration(context); + } + + /** * Allows us to mock FingerprintService for testing */ @VisibleForTesting @@ -173,6 +199,22 @@ public class AuthService extends SystemService { } return false; } + + /** + * Allows to test with various fingerprint aidl instances. + */ + @VisibleForTesting + public String[] getFingerprintAidlInstances() { + return ServiceManager.getDeclaredInstances(IFingerprint.DESCRIPTOR); + } + + /** + * Allows to test with various face aidl instances. + */ + @VisibleForTesting + public String[] getFaceAidlInstances() { + return ServiceManager.getDeclaredInstances(IFace.DESCRIPTOR); + } } private final class AuthServiceImpl extends IAuthService.Stub { @@ -717,12 +759,129 @@ public class AuthService extends SystemService { hidlConfigs = null; } - // Registers HIDL and AIDL authenticators, but only HIDL configs need to be provided. - registerAuthenticators(hidlConfigs); + if (com.android.server.biometrics.Flags.deHidl()) { + Slog.d(TAG, "deHidl flag is on."); + registerAuthenticators(); + } else { + // Registers HIDL and AIDL authenticators, but only HIDL configs need to be provided. + registerAuthenticators(hidlConfigs); + } mInjector.publishBinderService(this, mImpl); } + private void registerAuthenticators() { + registerFingerprintSensors(mInjector.getFingerprintAidlInstances(), + mInjector.getFingerprintConfiguration(getContext())); + registerFaceSensors(mInjector.getFaceAidlInstances(), + mInjector.getFaceConfiguration(getContext())); + registerIrisSensors(mInjector.getIrisConfiguration(getContext())); + } + + private void registerIrisSensors(String[] hidlConfigStrings) { + final SensorConfig[] hidlConfigs; + if (!mInjector.isHidlDisabled(getContext())) { + final int firstApiLevel = SystemProperties.getInt(SYSPROP_FIRST_API_LEVEL, 0); + final int apiLevel = SystemProperties.getInt(SYSPROP_API_LEVEL, firstApiLevel); + if (hidlConfigStrings.length == 0 && apiLevel == Build.VERSION_CODES.R) { + // For backwards compatibility with R where biometrics could work without being + // configured in config_biometric_sensors. In the absence of a vendor provided + // configuration, we assume the weakest biometric strength (i.e. convenience). + Slog.w(TAG, "Found R vendor partition without config_biometric_sensors"); + hidlConfigStrings = generateRSdkCompatibleConfiguration(); + } + hidlConfigs = new SensorConfig[hidlConfigStrings.length]; + for (int i = 0; i < hidlConfigStrings.length; ++i) { + hidlConfigs[i] = new SensorConfig(hidlConfigStrings[i]); + } + } else { + hidlConfigs = null; + } + + final List<SensorPropertiesInternal> hidlIrisSensors = new ArrayList<>(); + + if (hidlConfigs != null) { + for (SensorConfig sensor : hidlConfigs) { + switch (sensor.modality) { + case TYPE_IRIS: + hidlIrisSensors.add(getHidlIrisSensorProps(sensor.id, sensor.strength)); + break; + + default: + Slog.e(TAG, "Unknown modality: " + sensor.modality); + } + } + } + + final IIrisService irisService = mInjector.getIrisService(); + if (irisService != null) { + try { + irisService.registerAuthenticators(hidlIrisSensors); + } catch (RemoteException e) { + Slog.e(TAG, "RemoteException when registering iris authenticators", e); + } + } else if (hidlIrisSensors.size() > 0) { + Slog.e(TAG, "HIDL iris configuration exists, but IrisService is null."); + } + } + + private void registerFaceSensors(final String[] faceAidlInstances, + final String[] hidlConfigStrings) { + final FaceSensorConfigurations mFaceSensorConfigurations = + new FaceSensorConfigurations(hidlConfigStrings != null + && hidlConfigStrings.length > 0); + + if (hidlConfigStrings != null && hidlConfigStrings.length > 0) { + mFaceSensorConfigurations.addHidlConfigs( + hidlConfigStrings, getContext()); + } + + if (faceAidlInstances != null && faceAidlInstances.length > 0) { + mFaceSensorConfigurations.addAidlConfigs(faceAidlInstances, + name -> IFace.Stub.asInterface(Binder.allowBlocking( + ServiceManager.waitForDeclaredService(name)))); + } + + final IFaceService faceService = mInjector.getFaceService(); + if (faceService != null) { + try { + faceService.registerAuthenticatorsLegacy(mFaceSensorConfigurations); + } catch (RemoteException e) { + Slog.e(TAG, "RemoteException when registering face authenticators", e); + } + } else if (mFaceSensorConfigurations.hasSensorConfigurations()) { + Slog.e(TAG, "Face configuration exists, but FaceService is null."); + } + } + + private void registerFingerprintSensors(final String[] fingerprintAidlInstances, + final String[] hidlConfigStrings) { + final FingerprintSensorConfigurations mFingerprintSensorConfigurations = + new FingerprintSensorConfigurations(!(hidlConfigStrings != null + && hidlConfigStrings.length > 0)); + + if (hidlConfigStrings != null && hidlConfigStrings.length > 0) { + mFingerprintSensorConfigurations.addHidlSensors(hidlConfigStrings, getContext()); + } + + if (fingerprintAidlInstances != null && fingerprintAidlInstances.length > 0) { + mFingerprintSensorConfigurations.addAidlSensors(fingerprintAidlInstances, + name -> IFingerprint.Stub.asInterface(Binder.allowBlocking( + ServiceManager.waitForDeclaredService(name)))); + } + + final IFingerprintService fingerprintService = mInjector.getFingerprintService(); + if (fingerprintService != null) { + try { + fingerprintService.registerAuthenticatorsLegacy(mFingerprintSensorConfigurations); + } catch (RemoteException e) { + Slog.e(TAG, "RemoteException when registering fingerprint authenticators", e); + } + } else if (mFingerprintSensorConfigurations.hasSensorConfigurations()) { + Slog.e(TAG, "Fingerprint configuration exists, but FingerprintService is null."); + } + } + /** * Generates an array of string configs with entries that correspond to the biometric features * declared on the device. Returns an empty array if no biometric features are declared. diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java index 037ea38a2d17..89b638be3500 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java @@ -202,7 +202,7 @@ public class BiometricScheduler { }; @VisibleForTesting - BiometricScheduler(@NonNull String tag, + public BiometricScheduler(@NonNull String tag, @NonNull Handler handler, @SensorType int sensorType, @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher, diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java index 57feedc0e68e..0c3dfa7b3b84 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java @@ -387,6 +387,11 @@ public class BiometricSchedulerOperation { return isAuthentication || isDetection; } + /** If this operation is {@link StartUserClient}. */ + public boolean isStartUserOperation() { + return mClientMonitor instanceof StartUserClient<?, ?>; + } + /** If this operation performs acquisition {@link AcquisitionClient}. */ public boolean isAcquisitionOperation() { return mClientMonitor instanceof AcquisitionClient; diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java index 7f8f38f3e9d2..6daaad1baf83 100644 --- a/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java @@ -54,8 +54,6 @@ public abstract class InternalEnumerateClient<T> extends HalClientMonitor<T> // is all done internally. super(context, lazyDaemon, token, null /* ClientMonitorCallbackConverter */, userId, owner, 0 /* cookie */, sensorId, logger, biometricContext); - //, BiometricsProtoEnums.ACTION_ENUMERATE, - // BiometricsProtoEnums.CLIENT_UNKNOWN); mEnrolledList = enrolledList; mUtils = utils; } diff --git a/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java index 80754702415a..3753bbdba276 100644 --- a/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java +++ b/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java @@ -135,7 +135,7 @@ public class UserAwareBiometricScheduler extends BiometricScheduler { final int currentUserId = mCurrentUserRetriever.getCurrentUserId(); final int nextUserId = mPendingOperations.getFirst().getTargetUserId(); - if (nextUserId == currentUserId) { + if (nextUserId == currentUserId || mPendingOperations.getFirst().isStartUserOperation()) { super.startNextOperationIfIdle(); } else if (currentUserId == UserHandle.USER_NULL) { final BaseClientMonitor startClient = diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java index 578d9dc2aede..6af223b3660a 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java @@ -36,6 +36,7 @@ import android.hardware.biometrics.face.IFace; import android.hardware.biometrics.face.SensorProps; import android.hardware.face.Face; import android.hardware.face.FaceAuthenticateOptions; +import android.hardware.face.FaceSensorConfigurations; import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.face.FaceServiceReceiver; import android.hardware.face.IFaceAuthenticatorsRegisteredCallback; @@ -55,6 +56,7 @@ import android.util.Slog; import android.util.proto.ProtoOutputStream; import android.view.Surface; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DumpUtils; import com.android.internal.widget.LockPatternUtils; import com.android.server.SystemService; @@ -76,6 +78,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.function.Supplier; /** * A service to manage multiple clients that want to access the face HAL API. @@ -86,7 +89,7 @@ public class FaceService extends SystemService { protected static final String TAG = "FaceService"; - private final FaceServiceWrapper mServiceWrapper; + @VisibleForTesting final FaceServiceWrapper mServiceWrapper; private final LockoutResetDispatcher mLockoutResetDispatcher; private final LockPatternUtils mLockPatternUtils; @NonNull @@ -94,11 +97,18 @@ public class FaceService extends SystemService { @NonNull private final BiometricStateCallback<ServiceProvider, FaceSensorPropertiesInternal> mBiometricStateCallback; + @NonNull + private final FaceProviderFunction mFaceProviderFunction; + + interface FaceProviderFunction { + FaceProvider getFaceProvider(Pair<String, SensorProps[]> filteredSensorProps, + boolean resetLockoutRequiresChallenge); + } /** * Receives the incoming binder calls from FaceManager. */ - private final class FaceServiceWrapper extends IFaceService.Stub { + @VisibleForTesting final class FaceServiceWrapper extends IFaceService.Stub { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback, @@ -672,7 +682,8 @@ public class FaceService extends SystemService { final SensorProps[] props = face.getSensorProps(); final FaceProvider provider = new FaceProvider(getContext(), mBiometricStateCallback, props, instance, mLockoutResetDispatcher, - BiometricContext.getInstance(getContext())); + BiometricContext.getInstance(getContext()), + false /* resetLockoutRequiresChallenge */); providers.add(provider); } catch (RemoteException e) { Slog.e(TAG, "Remote exception in getSensorProps: " + fqName); @@ -704,6 +715,55 @@ public class FaceService extends SystemService { }); } + @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) + public void registerAuthenticatorsLegacy( + FaceSensorConfigurations faceSensorConfigurations) { + super.registerAuthenticatorsLegacy_enforcePermission(); + + if (!faceSensorConfigurations.hasSensorConfigurations()) { + Slog.d(TAG, "No face sensors to register."); + return; + } + mRegistry.registerAll(() -> getProviders(faceSensorConfigurations)); + } + + private List<ServiceProvider> getProviders( + FaceSensorConfigurations faceSensorConfigurations) { + final List<ServiceProvider> providers = new ArrayList<>(); + final Pair<String, SensorProps[]> filteredSensorProps = + filterAvailableHalInstances(faceSensorConfigurations); + providers.add(mFaceProviderFunction.getFaceProvider(filteredSensorProps, + faceSensorConfigurations.getResetLockoutRequiresChallenge())); + return providers; + } + + @NonNull + private Pair<String, SensorProps[]> filterAvailableHalInstances( + FaceSensorConfigurations faceSensorConfigurations) { + Pair<String, SensorProps[]> finalSensorPair = faceSensorConfigurations.getSensorPair(); + + if (faceSensorConfigurations.isSingleSensorConfigurationPresent()) { + return finalSensorPair; + } + + final Pair<String, SensorProps[]> virtualSensorProps = faceSensorConfigurations + .getSensorPairForInstance("virtual"); + + if (Utils.isVirtualEnabled(getContext())) { + if (virtualSensorProps != null) { + return virtualSensorProps; + } else { + Slog.e(TAG, "Could not find virtual interface while it is enabled"); + return finalSensorPair; + } + } else { + if (virtualSensorProps != null) { + return faceSensorConfigurations.getSensorPairNotForInstance("virtual"); + } + } + return finalSensorPair; + } + private Pair<List<FaceSensorPropertiesInternal>, List<String>> filterAvailableHalInstances( @NonNull List<FaceSensorPropertiesInternal> hidlInstances, @@ -752,20 +812,36 @@ public class FaceService extends SystemService { } public FaceService(Context context) { + this(context, null /* faceProviderFunction */, () -> IBiometricService.Stub.asInterface( + ServiceManager.getService(Context.BIOMETRIC_SERVICE))); + } + + @VisibleForTesting FaceService(Context context, FaceProviderFunction faceProviderFunction, + Supplier<IBiometricService> biometricServiceSupplier) { super(context); mServiceWrapper = new FaceServiceWrapper(); mLockoutResetDispatcher = new LockoutResetDispatcher(context); mLockPatternUtils = new LockPatternUtils(context); mBiometricStateCallback = new BiometricStateCallback<>(UserManager.get(context)); - mRegistry = new FaceServiceRegistry(mServiceWrapper, - () -> IBiometricService.Stub.asInterface( - ServiceManager.getService(Context.BIOMETRIC_SERVICE))); + mRegistry = new FaceServiceRegistry(mServiceWrapper, biometricServiceSupplier); mRegistry.addAllRegisteredCallback(new IFaceAuthenticatorsRegisteredCallback.Stub() { @Override public void onAllAuthenticatorsRegistered(List<FaceSensorPropertiesInternal> sensors) { mBiometricStateCallback.start(mRegistry.getProviders()); } }); + + if (Flags.deHidl()) { + mFaceProviderFunction = faceProviderFunction != null ? faceProviderFunction : + ((filteredSensorProps, resetLockoutRequiresChallenge) -> new FaceProvider( + getContext(), mBiometricStateCallback, + filteredSensorProps.second, + filteredSensorProps.first, mLockoutResetDispatcher, + BiometricContext.getInstance(getContext()), + resetLockoutRequiresChallenge)); + } else { + mFaceProviderFunction = ((filteredSensorProps, resetLockoutRequiresChallenge) -> null); + } } @Override diff --git a/services/core/java/com/android/server/biometrics/sensors/face/LockoutHalImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/LockoutHalImpl.java index e5d4a635876d..ef2be790ed34 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/LockoutHalImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/LockoutHalImpl.java @@ -24,7 +24,8 @@ import com.android.server.biometrics.sensors.LockoutTracker; * the user changes. */ public class LockoutHalImpl implements LockoutTracker { - private @LockoutMode int mCurrentUserLockoutMode; + @LockoutMode + private int mCurrentUserLockoutMode; @Override public int getLockoutModeForUser(int userId) { diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlResponseHandler.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlResponseHandler.java index 57f5b34c197a..098be2120e03 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlResponseHandler.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlResponseHandler.java @@ -27,6 +27,7 @@ import android.hardware.face.Face; import android.hardware.keymaster.HardwareAuthToken; import android.util.Slog; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.HardwareAuthTokenUtils; import com.android.server.biometrics.Utils; import com.android.server.biometrics.sensors.AcquisitionClient; @@ -59,6 +60,20 @@ public class AidlResponseHandler extends ISessionCallback.Stub { void onHardwareUnavailable(); } + /** + * Interface to send results to the AidlResponseHandler's owner. + */ + public interface AidlResponseHandlerCallback { + /** + * Invoked when enrollment is successful. + */ + void onEnrollSuccess(); + /** + * Invoked when the HAL sends ERROR_HW_UNAVAILABLE. + */ + void onHardwareUnavailable(); + } + private static final String TAG = "AidlResponseHandler"; @NonNull @@ -68,7 +83,7 @@ public class AidlResponseHandler extends ISessionCallback.Stub { private final int mSensorId; private final int mUserId; @NonNull - private final LockoutTracker mLockoutCache; + private final LockoutTracker mLockoutTracker; @NonNull private final LockoutResetDispatcher mLockoutResetDispatcher; @@ -76,6 +91,8 @@ public class AidlResponseHandler extends ISessionCallback.Stub { private final AuthSessionCoordinator mAuthSessionCoordinator; @NonNull private final HardwareUnavailableCallback mHardwareUnavailableCallback; + @NonNull + private final AidlResponseHandlerCallback mAidlResponseHandlerCallback; public AidlResponseHandler(@NonNull Context context, @NonNull BiometricScheduler scheduler, int sensorId, int userId, @@ -83,14 +100,33 @@ public class AidlResponseHandler extends ISessionCallback.Stub { @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull AuthSessionCoordinator authSessionCoordinator, @NonNull HardwareUnavailableCallback hardwareUnavailableCallback) { + this(context, scheduler, sensorId, userId, lockoutTracker, lockoutResetDispatcher, + authSessionCoordinator, hardwareUnavailableCallback, + new AidlResponseHandlerCallback() { + @Override + public void onEnrollSuccess() {} + + @Override + public void onHardwareUnavailable() {} + }); + } + + public AidlResponseHandler(@NonNull Context context, + @NonNull BiometricScheduler scheduler, int sensorId, int userId, + @NonNull LockoutTracker lockoutTracker, + @NonNull LockoutResetDispatcher lockoutResetDispatcher, + @NonNull AuthSessionCoordinator authSessionCoordinator, + @NonNull HardwareUnavailableCallback hardwareUnavailableCallback, + @NonNull AidlResponseHandlerCallback aidlResponseHandlerCallback) { mContext = context; mScheduler = scheduler; mSensorId = sensorId; mUserId = userId; - mLockoutCache = lockoutTracker; + mLockoutTracker = lockoutTracker; mLockoutResetDispatcher = lockoutResetDispatcher; mAuthSessionCoordinator = authSessionCoordinator; mHardwareUnavailableCallback = hardwareUnavailableCallback; + mAidlResponseHandlerCallback = aidlResponseHandlerCallback; } @Override @@ -106,13 +142,13 @@ public class AidlResponseHandler extends ISessionCallback.Stub { @Override public void onChallengeGenerated(long challenge) { handleResponse(FaceGenerateChallengeClient.class, (c) -> c.onChallengeGenerated(mSensorId, - mUserId, challenge), null); + mUserId, challenge)); } @Override public void onChallengeRevoked(long challenge) { handleResponse(FaceRevokeChallengeClient.class, (c) -> c.onChallengeRevoked(mSensorId, - mUserId, challenge), null); + mUserId, challenge)); } @Override @@ -123,7 +159,7 @@ public class AidlResponseHandler extends ISessionCallback.Stub { return; } c.onAuthenticationFrame(AidlConversionUtils.toFrameworkAuthenticationFrame(frame)); - }, null); + }); } @Override @@ -134,7 +170,7 @@ public class AidlResponseHandler extends ISessionCallback.Stub { return; } c.onEnrollmentFrame(AidlConversionUtils.toFrameworkEnrollmentFrame(frame)); - }, null); + }); } @Override @@ -149,9 +185,13 @@ public class AidlResponseHandler extends ISessionCallback.Stub { handleResponse(ErrorConsumer.class, (c) -> { c.onError(error, vendorCode); if (error == Error.HW_UNAVAILABLE) { - mHardwareUnavailableCallback.onHardwareUnavailable(); + if (Flags.deHidl()) { + mAidlResponseHandlerCallback.onHardwareUnavailable(); + } else { + mHardwareUnavailableCallback.onHardwareUnavailable(); + } } - }, null); + }); } @Override @@ -167,7 +207,12 @@ public class AidlResponseHandler extends ISessionCallback.Stub { .getUniqueName(mContext, currentUserId); final Face face = new Face(name, enrollmentId, mSensorId); - handleResponse(FaceEnrollClient.class, (c) -> c.onEnrollResult(face, remaining), null); + handleResponse(FaceEnrollClient.class, (c) -> { + c.onEnrollResult(face, remaining); + if (remaining == 0) { + mAidlResponseHandlerCallback.onEnrollSuccess(); + } + }); } @Override @@ -179,37 +224,37 @@ public class AidlResponseHandler extends ISessionCallback.Stub { byteList.add(b); } handleResponse(AuthenticationConsumer.class, (c) -> c.onAuthenticated(face, - true /* authenticated */, byteList), null); + true /* authenticated */, byteList)); } @Override public void onAuthenticationFailed() { final Face face = new Face("" /* name */, 0 /* faceId */, mSensorId); handleResponse(AuthenticationConsumer.class, (c) -> c.onAuthenticated(face, - false /* authenticated */, null /* hat */), null); + false /* authenticated */, null /* hat */)); } @Override public void onLockoutTimed(long durationMillis) { - handleResponse(LockoutConsumer.class, (c) -> c.onLockoutTimed(durationMillis), null); + handleResponse(LockoutConsumer.class, (c) -> c.onLockoutTimed(durationMillis)); } @Override public void onLockoutPermanent() { - handleResponse(LockoutConsumer.class, LockoutConsumer::onLockoutPermanent, null); + handleResponse(LockoutConsumer.class, LockoutConsumer::onLockoutPermanent); } @Override public void onLockoutCleared() { handleResponse(FaceResetLockoutClient.class, FaceResetLockoutClient::onLockoutCleared, (c) -> FaceResetLockoutClient.resetLocalLockoutStateToNone(mSensorId, mUserId, - mLockoutCache, mLockoutResetDispatcher, mAuthSessionCoordinator, - Utils.getCurrentStrength(mSensorId), -1 /* requestId */)); + mLockoutTracker, mLockoutResetDispatcher, mAuthSessionCoordinator, + Utils.getCurrentStrength(mSensorId), -1 /* requestId */)); } @Override public void onInteractionDetected() { - handleResponse(FaceDetectClient.class, FaceDetectClient::onInteractionDetected, null); + handleResponse(FaceDetectClient.class, FaceDetectClient::onInteractionDetected); } @Override @@ -219,23 +264,23 @@ public class AidlResponseHandler extends ISessionCallback.Stub { final Face face = new Face("" /* name */, enrollmentIds[i], mSensorId); final int finalI = i; handleResponse(EnumerateConsumer.class, (c) -> c.onEnumerationResult(face, - enrollmentIds.length - finalI - 1), null); + enrollmentIds.length - finalI - 1 /* remaining */)); } } else { handleResponse(EnumerateConsumer.class, (c) -> c.onEnumerationResult( - null /* identifier */, 0 /* remaining */), null); + null /* identifier */, 0 /* remaining */)); } } @Override public void onFeaturesRetrieved(byte[] features) { handleResponse(FaceGetFeatureClient.class, (c) -> c.onFeatureGet(true /* success */, - features), null); + features)); } @Override public void onFeatureSet(byte feature) { - handleResponse(FaceSetFeatureClient.class, (c) -> c.onFeatureSet(true /* success */), null); + handleResponse(FaceSetFeatureClient.class, (c) -> c.onFeatureSet(true /* success */)); } @Override @@ -245,33 +290,32 @@ public class AidlResponseHandler extends ISessionCallback.Stub { final Face face = new Face("" /* name */, enrollmentIds[i], mSensorId); final int finalI = i; handleResponse(RemovalConsumer.class, - (c) -> c.onRemoved(face, enrollmentIds.length - finalI - 1), - null); + (c) -> c.onRemoved(face, + enrollmentIds.length - finalI - 1 /* remaining */)); } } else { handleResponse(RemovalConsumer.class, (c) -> c.onRemoved(null /* identifier */, - 0 /* remaining */), null); + 0 /* remaining */)); } } @Override public void onAuthenticatorIdRetrieved(long authenticatorId) { handleResponse(FaceGetAuthenticatorIdClient.class, (c) -> c.onAuthenticatorIdRetrieved( - authenticatorId), null); + authenticatorId)); } @Override public void onAuthenticatorIdInvalidated(long newAuthenticatorId) { handleResponse(FaceInvalidationClient.class, (c) -> c.onAuthenticatorIdInvalidated( - newAuthenticatorId), null); + newAuthenticatorId)); } /** * Handles acquired messages sent by the HAL (specifically for HIDL HAL). */ public void onAcquired(int acquiredInfo, int vendorCode) { - handleResponse(AcquisitionClient.class, (c) -> c.onAcquired(acquiredInfo, vendorCode), - null); + handleResponse(AcquisitionClient.class, (c) -> c.onAcquired(acquiredInfo, vendorCode)); } /** @@ -288,7 +332,7 @@ public class AidlResponseHandler extends ISessionCallback.Stub { lockoutMode = LockoutTracker.LOCKOUT_TIMED; } - mLockoutCache.setLockoutModeForUser(mUserId, lockoutMode); + mLockoutTracker.setLockoutModeForUser(mUserId, lockoutMode); if (duration == 0) { mLockoutResetDispatcher.notifyLockoutResetCallbacks(mSensorId); @@ -296,6 +340,20 @@ public class AidlResponseHandler extends ISessionCallback.Stub { }); } + /** + * Handle clients which are not supported in HIDL HAL. For face, FaceInvalidationClient + * is the only AIDL client which is not supported in HIDL. + */ + public void onUnsupportedClientScheduled() { + Slog.e(TAG, "FaceInvalidationClient is not supported in the HAL."); + handleResponse(FaceInvalidationClient.class, BaseClientMonitor::cancel); + } + + private <T> void handleResponse(@NonNull Class<T> className, + @NonNull Consumer<T> action) { + handleResponse(className, action, null /* alternateAction */); + } + private <T> void handleResponse(@NonNull Class<T> className, @NonNull Consumer<T> actionIfClassMatchesClient, @Nullable Consumer<BaseClientMonitor> alternateAction) { diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlSession.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlSession.java index e70e25aebe9b..af46f441d6ce 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlSession.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlSession.java @@ -21,7 +21,7 @@ import android.content.Context; import android.hardware.biometrics.face.ISession; import android.hardware.biometrics.face.V1_0.IBiometricsFace; -import com.android.server.biometrics.sensors.face.hidl.AidlToHidlAdapter; +import com.android.server.biometrics.sensors.face.hidl.HidlToAidlSessionAdapter; import java.util.function.Supplier; @@ -47,7 +47,7 @@ public class AidlSession { public AidlSession(Context context, Supplier<IBiometricsFace> session, int userId, AidlResponseHandler aidlResponseHandler) { - mSession = new AidlToHidlAdapter(context, session, userId, aidlResponseHandler); + mSession = new HidlToAidlSessionAdapter(context, session, userId, aidlResponseHandler); mHalInterfaceVersion = 0; mUserId = userId; mAidlResponseHandler = aidlResponseHandler; @@ -64,7 +64,7 @@ public class AidlSession { } /** The HAL callback, which should only be used in tests {@See BiometricTestSessionImpl}. */ - AidlResponseHandler getHalSessionCallback() { + public AidlResponseHandler getHalSessionCallback() { return mAidlResponseHandler; } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetFeatureClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetFeatureClient.java index c15049b48bb2..c41b706555e8 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetFeatureClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetFeatureClient.java @@ -34,7 +34,7 @@ import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; import com.android.server.biometrics.sensors.ErrorConsumer; import com.android.server.biometrics.sensors.HalClientMonitor; -import com.android.server.biometrics.sensors.face.hidl.AidlToHidlAdapter; +import com.android.server.biometrics.sensors.face.hidl.HidlToAidlSessionAdapter; import java.util.HashMap; import java.util.Map; @@ -75,8 +75,8 @@ public class FaceGetFeatureClient extends HalClientMonitor<AidlSession> implemen protected void startHalOperation() { try { ISession session = getFreshDaemon().getSession(); - if (session instanceof AidlToHidlAdapter) { - ((AidlToHidlAdapter) session).setFeature(mFeature); + if (session instanceof HidlToAidlSessionAdapter) { + ((HidlToAidlSessionAdapter) session).setFeature(mFeature); } session.getFeatures(); } catch (RemoteException e) { diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java index dd9c6d50ae9e..9fa15b8ea3a1 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java @@ -24,6 +24,7 @@ import android.app.SynchronousUserSwitchObserver; import android.app.TaskStackListener; import android.content.Context; import android.content.pm.UserInfo; +import android.hardware.biometrics.BiometricFaceConstants; import android.hardware.biometrics.BiometricsProtoEnums; import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.IInvalidationCallback; @@ -51,6 +52,7 @@ import android.view.Surface; import com.android.internal.annotations.VisibleForTesting; import com.android.server.biometrics.AuthenticationStatsBroadcastReceiver; import com.android.server.biometrics.AuthenticationStatsCollector; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.Utils; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; @@ -64,11 +66,13 @@ import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback; import com.android.server.biometrics.sensors.InvalidationRequesterClient; import com.android.server.biometrics.sensors.LockoutResetDispatcher; +import com.android.server.biometrics.sensors.LockoutTracker; import com.android.server.biometrics.sensors.PerformanceTracker; import com.android.server.biometrics.sensors.SensorList; import com.android.server.biometrics.sensors.face.FaceUtils; import com.android.server.biometrics.sensors.face.ServiceProvider; import com.android.server.biometrics.sensors.face.UsageStats; +import com.android.server.biometrics.sensors.face.hidl.HidlToAidlSensorAdapter; import org.json.JSONArray; import org.json.JSONException; @@ -152,9 +156,11 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @NonNull SensorProps[] props, @NonNull String halInstanceName, @NonNull LockoutResetDispatcher lockoutResetDispatcher, - @NonNull BiometricContext biometricContext) { + @NonNull BiometricContext biometricContext, + boolean resetLockoutRequiresChallenge) { this(context, biometricStateCallback, props, halInstanceName, lockoutResetDispatcher, - biometricContext, null /* daemon */); + biometricContext, null /* daemon */, resetLockoutRequiresChallenge, + false /* testHalEnabled */); } @VisibleForTesting FaceProvider(@NonNull Context context, @@ -163,7 +169,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @NonNull String halInstanceName, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull BiometricContext biometricContext, - IFace daemon) { + @Nullable IFace daemon, boolean resetLockoutRequiresChallenge, + boolean testHalEnabled) { mContext = context; mBiometricStateCallback = biometricStateCallback; mHalInstanceName = halInstanceName; @@ -176,48 +183,118 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { mBiometricContext = biometricContext; mAuthSessionCoordinator = mBiometricContext.getAuthSessionCoordinator(); mDaemon = daemon; + mTestHalEnabled = testHalEnabled; - AuthenticationStatsBroadcastReceiver mBroadcastReceiver = - new AuthenticationStatsBroadcastReceiver( - mContext, - BiometricsProtoEnums.MODALITY_FACE, - (AuthenticationStatsCollector collector) -> { - Slog.d(getTag(), "Initializing AuthenticationStatsCollector"); - mAuthenticationStatsCollector = collector; - }); + initAuthenticationBroadcastReceiver(); + initSensors(resetLockoutRequiresChallenge, props); + } - for (SensorProps prop : props) { - final int sensorId = prop.commonProps.sensorId; + private void initAuthenticationBroadcastReceiver() { + new AuthenticationStatsBroadcastReceiver( + mContext, + BiometricsProtoEnums.MODALITY_FACE, + (AuthenticationStatsCollector collector) -> { + Slog.d(getTag(), "Initializing AuthenticationStatsCollector"); + mAuthenticationStatsCollector = collector; + }); + } - final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); - if (prop.commonProps.componentInfo != null) { - for (ComponentInfo info : prop.commonProps.componentInfo) { - componentInfo.add(new ComponentInfoInternal(info.componentId, - info.hardwareVersion, info.firmwareVersion, info.serialNumber, - info.softwareVersion)); + private void initSensors(boolean resetLockoutRequiresChallenge, SensorProps[] props) { + if (Flags.deHidl()) { + if (resetLockoutRequiresChallenge) { + Slog.d(getTag(), "Adding HIDL configs"); + for (SensorProps prop : props) { + addHidlSensors(prop, resetLockoutRequiresChallenge); + } + } else { + Slog.d(getTag(), "Adding AIDL configs"); + for (SensorProps prop : props) { + addAidlSensors(prop, resetLockoutRequiresChallenge); } } + } else { + for (SensorProps prop : props) { + final int sensorId = prop.commonProps.sensorId; + + final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); + if (prop.commonProps.componentInfo != null) { + for (ComponentInfo info : prop.commonProps.componentInfo) { + componentInfo.add(new ComponentInfoInternal(info.componentId, + info.hardwareVersion, info.firmwareVersion, info.serialNumber, + info.softwareVersion)); + } + } - final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal( - prop.commonProps.sensorId, prop.commonProps.sensorStrength, - prop.commonProps.maxEnrollmentsPerUser, componentInfo, prop.sensorType, - prop.supportsDetectInteraction, prop.halControlsPreview, - false /* resetLockoutRequiresChallenge */); - final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext, mHandler, - internalProp, lockoutResetDispatcher, mBiometricContext); - final int userId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL : - sensor.getLazySession().get().getUserId(); - mFaceSensors.addSensor(sensorId, sensor, userId, - new SynchronousUserSwitchObserver() { - @Override - public void onUserSwitching(int newUserId) { - scheduleInternalCleanup(sensorId, newUserId, null /* callback */); - } - }); - Slog.d(getTag(), "Added: " + internalProp); + final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal( + prop.commonProps.sensorId, prop.commonProps.sensorStrength, + prop.commonProps.maxEnrollmentsPerUser, componentInfo, prop.sensorType, + prop.supportsDetectInteraction, prop.halControlsPreview, + false /* resetLockoutRequiresChallenge */); + final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, + mContext, mHandler, internalProp, mLockoutResetDispatcher, + mBiometricContext); + sensor.init(mLockoutResetDispatcher, this); + final int userId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL : + sensor.getLazySession().get().getUserId(); + mFaceSensors.addSensor(sensorId, sensor, userId, + new SynchronousUserSwitchObserver() { + @Override + public void onUserSwitching(int newUserId) { + scheduleInternalCleanup(sensorId, newUserId, null /* callback */); + } + }); + Slog.d(getTag(), "Added: " + internalProp); + } } } + private void addHidlSensors(SensorProps prop, boolean resetLockoutRequiresChallenge) { + final int sensorId = prop.commonProps.sensorId; + final Sensor sensor = new HidlToAidlSensorAdapter(getTag() + "/" + sensorId, this, + mContext, mHandler, prop, mLockoutResetDispatcher, + mBiometricContext, resetLockoutRequiresChallenge, + () -> { + //TODO: update to make this testable + scheduleInternalCleanup(sensorId, ActivityManager.getCurrentUser(), + null /* callback */); + scheduleGetFeature(sensorId, new Binder(), ActivityManager.getCurrentUser(), + BiometricFaceConstants.FEATURE_REQUIRE_ATTENTION, null, + mContext.getOpPackageName()); + }); + sensor.init(mLockoutResetDispatcher, this); + final int userId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL : + sensor.getLazySession().get().getUserId(); + mFaceSensors.addSensor(sensorId, sensor, userId, + new SynchronousUserSwitchObserver() { + @Override + public void onUserSwitching(int newUserId) { + scheduleInternalCleanup(sensorId, newUserId, null /* callback */); + scheduleGetFeature(sensorId, new Binder(), newUserId, + BiometricFaceConstants.FEATURE_REQUIRE_ATTENTION, + null, mContext.getOpPackageName()); + } + }); + Slog.d(getTag(), "Added: " + mFaceSensors.get(sensorId)); + } + + private void addAidlSensors(SensorProps prop, boolean resetLockoutRequiresChallenge) { + final int sensorId = prop.commonProps.sensorId; + final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext, + mHandler, prop, mLockoutResetDispatcher, mBiometricContext, + resetLockoutRequiresChallenge); + sensor.init(mLockoutResetDispatcher, this); + final int userId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL : + sensor.getLazySession().get().getUserId(); + mFaceSensors.addSensor(sensorId, sensor, userId, + new SynchronousUserSwitchObserver() { + @Override + public void onUserSwitching(int newUserId) { + scheduleInternalCleanup(sensorId, newUserId, null /* callback */); + } + }); + Slog.d(getTag(), "Added: " + mFaceSensors.get(sensorId)); + } + private String getTag() { return "FaceProvider/" + mHalInstanceName; } @@ -290,7 +367,10 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { } } - private void scheduleLoadAuthenticatorIdsForUser(int sensorId, int userId) { + /** + * Schedules FaceGetAuthenticatorIdClient for specific sensor and user. + */ + protected void scheduleLoadAuthenticatorIdsForUser(int sensorId, int userId) { mHandler.post(() -> { final FaceGetAuthenticatorIdClient client = new FaceGetAuthenticatorIdClient( mContext, mFaceSensors.get(sensorId).getLazySession(), userId, @@ -365,8 +445,12 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @Override public int getLockoutModeForUser(int sensorId, int userId) { - return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId, - Utils.getCurrentStrength(sensorId)); + if (Flags.deHidl()) { + return mFaceSensors.get(sensorId).getLockoutModeForUser(userId); + } else { + return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId, + Utils.getCurrentStrength(sensorId)); + } } @Override @@ -376,13 +460,18 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @Override public boolean isHardwareDetected(int sensorId) { - return hasHalInstance(); + if (Flags.deHidl()) { + return mFaceSensors.get(sensorId).isHardwareDetected(mHalInstanceName); + } else { + return hasHalInstance(); + } } @Override public void scheduleGenerateChallenge(int sensorId, int userId, @NonNull IBinder token, @NonNull IFaceServiceReceiver receiver, String opPackageName) { mHandler.post(() -> { + mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId); final FaceGenerateChallengeClient client = new FaceGenerateChallengeClient(mContext, mFaceSensors.get(sensorId).getLazySession(), token, new ClientMonitorCallbackConverter(receiver), userId, opPackageName, sensorId, @@ -416,6 +505,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @Nullable Surface previewSurface, boolean debugConsent) { final long id = mRequestCounter.incrementAndGet(); mHandler.post(() -> { + mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId); final int maxTemplatesPerUser = mFaceSensors.get( sensorId).getSensorProperties().maxEnrollmentsPerUser; final FaceEnrollClient client = new FaceEnrollClient(mContext, @@ -427,18 +517,23 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext, maxTemplatesPerUser, debugConsent); - scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback( - mBiometricStateCallback, new ClientMonitorCallback() { - @Override - public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, - boolean success) { - ClientMonitorCallback.super.onClientFinished(clientMonitor, success); - if (success) { - scheduleLoadAuthenticatorIdsForUser(sensorId, userId); - scheduleInvalidationRequest(sensorId, userId); + if (Flags.deHidl()) { + scheduleForSensor(sensorId, client, mBiometricStateCallback); + } else { + scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback( + mBiometricStateCallback, new ClientMonitorCallback() { + @Override + public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, + boolean success) { + ClientMonitorCallback.super.onClientFinished(clientMonitor, + success); + if (success) { + scheduleLoadAuthenticatorIdsForUser(sensorId, userId); + scheduleInvalidationRequest(sensorId, userId); + } } - } - })); + })); + } }); return id; } @@ -486,6 +581,13 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { final int userId = options.getUserId(); final int sensorId = options.getSensorId(); final boolean isStrongBiometric = Utils.isStrongBiometric(sensorId); + mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId); + final LockoutTracker lockoutTracker; + if (Flags.deHidl()) { + lockoutTracker = mFaceSensors.get(sensorId).getLockoutTracker(true /* forAuth */); + } else { + lockoutTracker = null; + } final FaceAuthenticationClient client = new FaceAuthenticationClient( mContext, mFaceSensors.get(sensorId).getLazySession(), token, requestId, callback, operationId, restricted, options, cookie, @@ -493,7 +595,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient, mAuthenticationStatsCollector), mBiometricContext, isStrongBiometric, - mUsageStats, null /* lockoutTracker */, + mUsageStats, lockoutTracker, allowBackgroundAuthentication, Utils.getCurrentStrength(sensorId)); scheduleForSensor(sensorId, client, new ClientMonitorCallback() { @Override @@ -555,6 +657,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { private void scheduleRemoveSpecifiedIds(int sensorId, @NonNull IBinder token, int[] faceIds, int userId, @NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) { mHandler.post(() -> { + mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId); final FaceRemovalClient client = new FaceRemovalClient(mContext, mFaceSensors.get(sensorId).getLazySession(), token, new ClientMonitorCallbackConverter(receiver), faceIds, userId, @@ -571,6 +674,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @Override public void scheduleResetLockout(int sensorId, int userId, @NonNull byte[] hardwareAuthToken) { mHandler.post(() -> { + mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId); final FaceResetLockoutClient client = new FaceResetLockoutClient( mContext, mFaceSensors.get(sensorId).getLazySession(), userId, mContext.getOpPackageName(), sensorId, @@ -578,8 +682,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext, hardwareAuthToken, - mFaceSensors.get(sensorId).getLockoutCache(), mLockoutResetDispatcher, - Utils.getCurrentStrength(sensorId)); + mFaceSensors.get(sensorId).getLockoutTracker(false/* forAuth */), + mLockoutResetDispatcher, Utils.getCurrentStrength(sensorId)); scheduleForSensor(sensorId, client); }); @@ -590,6 +694,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { boolean enabled, @NonNull byte[] hardwareAuthToken, @NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) { mHandler.post(() -> { + mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId); final List<Face> faces = FaceUtils.getInstance(sensorId) .getBiometricsForUser(mContext, userId); if (faces.isEmpty()) { @@ -610,6 +715,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { public void scheduleGetFeature(int sensorId, @NonNull IBinder token, int userId, int feature, @NonNull ClientMonitorCallbackConverter callback, @NonNull String opPackageName) { mHandler.post(() -> { + mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId); final List<Face> faces = FaceUtils.getInstance(sensorId) .getBiometricsForUser(mContext, userId); if (faces.isEmpty()) { @@ -641,6 +747,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { public void scheduleInternalCleanup(int sensorId, int userId, @Nullable ClientMonitorCallback callback, boolean favorHalEnrollments) { mHandler.post(() -> { + mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId); final FaceInternalCleanupClient client = new FaceInternalCleanupClient(mContext, mFaceSensors.get(sensorId).getLazySession(), userId, @@ -760,4 +867,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { } biometricScheduler.startWatchdog(); } + + public boolean getTestHalEnabled() { + return mTestHalEnabled; + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java index 77b5592c5064..d02eefaed101 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.content.Context; import android.hardware.biometrics.BiometricManager.Authenticators; import android.hardware.biometrics.face.IFace; +import android.hardware.biometrics.face.ISession; import android.hardware.keymaster.HardwareAuthToken; import android.os.RemoteException; import android.util.Slog; @@ -34,6 +35,7 @@ import com.android.server.biometrics.sensors.ErrorConsumer; import com.android.server.biometrics.sensors.HalClientMonitor; import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.LockoutTracker; +import com.android.server.biometrics.sensors.face.hidl.HidlToAidlSessionAdapter; import java.util.function.Supplier; @@ -47,7 +49,7 @@ public class FaceResetLockoutClient extends HalClientMonitor<AidlSession> implem private static final String TAG = "FaceResetLockoutClient"; private final HardwareAuthToken mHardwareAuthToken; - private final LockoutTracker mLockoutCache; + private final LockoutTracker mLockoutTracker; private final LockoutResetDispatcher mLockoutResetDispatcher; private final int mBiometricStrength; @@ -60,7 +62,7 @@ public class FaceResetLockoutClient extends HalClientMonitor<AidlSession> implem super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner, 0 /* cookie */, sensorId, logger, biometricContext); mHardwareAuthToken = HardwareAuthTokenUtils.toHardwareAuthToken(hardwareAuthToken); - mLockoutCache = lockoutTracker; + mLockoutTracker = lockoutTracker; mLockoutResetDispatcher = lockoutResetDispatcher; mBiometricStrength = biometricStrength; } @@ -79,7 +81,11 @@ public class FaceResetLockoutClient extends HalClientMonitor<AidlSession> implem @Override protected void startHalOperation() { try { - getFreshDaemon().getSession().resetLockout(mHardwareAuthToken); + final ISession session = getFreshDaemon().getSession(); + session.resetLockout(mHardwareAuthToken); + if (session instanceof HidlToAidlSessionAdapter) { + mCallback.onClientFinished(this, true /* success */); + } } catch (RemoteException e) { Slog.e(TAG, "Unable to reset lockout", e); mCallback.onClientFinished(this, false /* success */); @@ -87,7 +93,7 @@ public class FaceResetLockoutClient extends HalClientMonitor<AidlSession> implem } void onLockoutCleared() { - resetLocalLockoutStateToNone(getSensorId(), getTargetUserId(), mLockoutCache, + resetLocalLockoutStateToNone(getSensorId(), getTargetUserId(), mLockoutTracker, mLockoutResetDispatcher, getBiometricContext().getAuthSessionCoordinator(), mBiometricStrength, getRequestId()); mCallback.onClientFinished(this, true /* success */); diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java index 54e66eb4cca4..3e5c59914913 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java @@ -21,16 +21,20 @@ import android.annotation.Nullable; import android.content.Context; import android.content.pm.UserInfo; import android.hardware.biometrics.BiometricsProtoEnums; +import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.ITestSession; import android.hardware.biometrics.ITestSessionCallback; +import android.hardware.biometrics.common.ComponentInfo; import android.hardware.biometrics.face.IFace; import android.hardware.biometrics.face.ISession; +import android.hardware.biometrics.face.SensorProps; import android.hardware.face.FaceManager; import android.hardware.face.FaceSensorPropertiesInternal; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; +import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; import android.util.Slog; @@ -38,6 +42,7 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.FrameworkStatsLog; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.SensorServiceStateProto; import com.android.server.biometrics.SensorStateProto; import com.android.server.biometrics.UserStateProto; @@ -49,12 +54,15 @@ import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.ErrorConsumer; import com.android.server.biometrics.sensors.LockoutCache; import com.android.server.biometrics.sensors.LockoutResetDispatcher; +import com.android.server.biometrics.sensors.LockoutTracker; import com.android.server.biometrics.sensors.StartUserClient; import com.android.server.biometrics.sensors.StopUserClient; import com.android.server.biometrics.sensors.UserAwareBiometricScheduler; import com.android.server.biometrics.sensors.face.FaceUtils; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -71,25 +79,53 @@ public class Sensor { @NonNull private final IBinder mToken; @NonNull private final Handler mHandler; @NonNull private final FaceSensorPropertiesInternal mSensorProperties; - @NonNull private final UserAwareBiometricScheduler mScheduler; - @NonNull private final LockoutCache mLockoutCache; + @NonNull private BiometricScheduler mScheduler; + @Nullable private LockoutTracker mLockoutTracker; @NonNull private final Map<Integer, Long> mAuthenticatorIds; - @NonNull private final Supplier<AidlSession> mLazySession; + @NonNull private Supplier<AidlSession> mLazySession; @Nullable AidlSession mCurrentSession; + @NonNull BiometricContext mBiometricContext; Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context, @NonNull Handler handler, @NonNull FaceSensorPropertiesInternal sensorProperties, @NonNull LockoutResetDispatcher lockoutResetDispatcher, - @NonNull BiometricContext biometricContext, AidlSession session) { + @NonNull BiometricContext biometricContext, @Nullable AidlSession session) { mTag = tag; mProvider = provider; mContext = context; mToken = new Binder(); mHandler = handler; mSensorProperties = sensorProperties; - mScheduler = new UserAwareBiometricScheduler(tag, + mBiometricContext = biometricContext; + mAuthenticatorIds = new HashMap<>(); + } + + Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context, + @NonNull Handler handler, @NonNull FaceSensorPropertiesInternal sensorProperties, + @NonNull LockoutResetDispatcher lockoutResetDispatcher, + @NonNull BiometricContext biometricContext) { + this(tag, provider, context, handler, sensorProperties, lockoutResetDispatcher, + biometricContext, null); + } + + public Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context, + @NonNull Handler handler, @NonNull SensorProps prop, + @NonNull LockoutResetDispatcher lockoutResetDispatcher, + @NonNull BiometricContext biometricContext, + boolean resetLockoutRequiresChallenge) { + this(tag, provider, context, handler, + getFaceSensorPropertiesInternal(prop, resetLockoutRequiresChallenge), + lockoutResetDispatcher, biometricContext, null); + } + + /** + * Initialize biometric scheduler, lockout tracker and session for the sensor. + */ + public void init(LockoutResetDispatcher lockoutResetDispatcher, + FaceProvider provider) { + mScheduler = new UserAwareBiometricScheduler(mTag, BiometricScheduler.SENSOR_TYPE_FACE, null /* gestureAvailabilityDispatcher */, () -> mCurrentSession != null ? mCurrentSession.getUserId() : UserHandle.USER_NULL, new UserAwareBiometricScheduler.UserSwitchCallback() { @@ -98,7 +134,7 @@ public class Sensor { public StopUserClient<?> getStopUserClient(int userId) { return new FaceStopUserClient(mContext, mLazySession, mToken, userId, mSensorProperties.sensorId, - BiometricLogger.ofUnknown(mContext), biometricContext, + BiometricLogger.ofUnknown(mContext), mBiometricContext, () -> mCurrentSession = null); } @@ -107,13 +143,36 @@ public class Sensor { public StartUserClient<?, ?> getStartUserClient(int newUserId) { final int sensorId = mSensorProperties.sensorId; - final AidlResponseHandler resultController = new AidlResponseHandler( - mContext, mScheduler, sensorId, newUserId, - mLockoutCache, lockoutResetDispatcher, - biometricContext.getAuthSessionCoordinator(), () -> { - Slog.e(mTag, "Got ERROR_HW_UNAVAILABLE"); - mCurrentSession = null; - }); + final AidlResponseHandler resultController; + if (Flags.deHidl()) { + resultController = new AidlResponseHandler( + mContext, mScheduler, sensorId, newUserId, + mLockoutTracker, lockoutResetDispatcher, + mBiometricContext.getAuthSessionCoordinator(), () -> {}, + new AidlResponseHandler.AidlResponseHandlerCallback() { + @Override + public void onEnrollSuccess() { + mProvider.scheduleLoadAuthenticatorIdsForUser(sensorId, + newUserId); + mProvider.scheduleInvalidationRequest(sensorId, + newUserId); + } + + @Override + public void onHardwareUnavailable() { + Slog.e(mTag, "Face sensor hardware unavailable."); + mCurrentSession = null; + } + }); + } else { + resultController = new AidlResponseHandler( + mContext, mScheduler, sensorId, newUserId, + mLockoutTracker, lockoutResetDispatcher, + mBiometricContext.getAuthSessionCoordinator(), () -> { + Slog.e(mTag, "Got ERROR_HW_UNAVAILABLE"); + mCurrentSession = null; + }); + } final StartUserClient.UserStartedCallback<ISession> userStartedCallback = (userIdStarted, newSession, halInterfaceVersion) -> { @@ -136,32 +195,42 @@ public class Sensor { return new FaceStartUserClient(mContext, provider::getHalInstance, mToken, newUserId, mSensorProperties.sensorId, - BiometricLogger.ofUnknown(mContext), biometricContext, + BiometricLogger.ofUnknown(mContext), mBiometricContext, resultController, userStartedCallback); } }); - mLockoutCache = new LockoutCache(); - mAuthenticatorIds = new HashMap<>(); mLazySession = () -> mCurrentSession != null ? mCurrentSession : null; + mLockoutTracker = new LockoutCache(); } - Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context, - @NonNull Handler handler, @NonNull FaceSensorPropertiesInternal sensorProperties, - @NonNull LockoutResetDispatcher lockoutResetDispatcher, - @NonNull BiometricContext biometricContext) { - this(tag, provider, context, handler, sensorProperties, lockoutResetDispatcher, - biometricContext, null); + private static FaceSensorPropertiesInternal getFaceSensorPropertiesInternal(SensorProps prop, + boolean resetLockoutRequiresChallenge) { + final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); + if (prop.commonProps.componentInfo != null) { + for (ComponentInfo info : prop.commonProps.componentInfo) { + componentInfo.add(new ComponentInfoInternal(info.componentId, + info.hardwareVersion, info.firmwareVersion, info.serialNumber, + info.softwareVersion)); + } + } + final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal( + prop.commonProps.sensorId, prop.commonProps.sensorStrength, + prop.commonProps.maxEnrollmentsPerUser, componentInfo, prop.sensorType, + prop.supportsDetectInteraction, prop.halControlsPreview, + resetLockoutRequiresChallenge); + + return internalProp; } - @NonNull Supplier<AidlSession> getLazySession() { + @NonNull public Supplier<AidlSession> getLazySession() { return mLazySession; } - @NonNull FaceSensorPropertiesInternal getSensorProperties() { + @NonNull protected FaceSensorPropertiesInternal getSensorProperties() { return mSensorProperties; } - @VisibleForTesting @Nullable AidlSession getSessionForUser(int userId) { + @VisibleForTesting @Nullable protected AidlSession getSessionForUser(int userId) { if (mCurrentSession != null && mCurrentSession.getUserId() == userId) { return mCurrentSession; } else { @@ -174,15 +243,18 @@ public class Sensor { mProvider, this); } - @NonNull BiometricScheduler getScheduler() { + @NonNull public BiometricScheduler getScheduler() { return mScheduler; } - @NonNull LockoutCache getLockoutCache() { - return mLockoutCache; + @NonNull protected LockoutTracker getLockoutTracker(boolean forAuth) { + if (forAuth) { + return null; + } + return mLockoutTracker; } - @NonNull Map<Integer, Long> getAuthenticatorIds() { + @NonNull protected Map<Integer, Long> getAuthenticatorIds() { return mAuthenticatorIds; } @@ -253,4 +325,49 @@ public class Sensor { mScheduler.reset(); mCurrentSession = null; } + + protected BiometricContext getBiometricContext() { + return mBiometricContext; + } + + protected Handler getHandler() { + return mHandler; + } + + protected Context getContext() { + return mContext; + } + + /** + * Schedules FaceUpdateActiveUserClient for user id. + */ + public void scheduleFaceUpdateActiveUserClient(int userId) {} + + /** + * Returns true if the sensor hardware is detected. + */ + public boolean isHardwareDetected(String halInstanceName) { + if (mTestHalEnabled) { + return true; + } + return ServiceManager.checkService(IFace.DESCRIPTOR + "/" + halInstanceName) != null; + } + + /** + * Returns lockout mode of this sensor. + */ + @LockoutTracker.LockoutMode + public int getLockoutModeForUser(int userId) { + return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId, + Utils.getCurrentStrength(mSensorProperties.sensorId)); + } + + public void setScheduler(BiometricScheduler scheduler) { + mScheduler = scheduler; + } + + public void setLazySession( + Supplier<AidlSession> lazySession) { + mLazySession = lazySession; + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceUpdateActiveUserClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceUpdateActiveUserClient.java index 8385c3fa7103..0c34d702184a 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceUpdateActiveUserClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceUpdateActiveUserClient.java @@ -27,13 +27,14 @@ import com.android.server.biometrics.BiometricsProto; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.ClientMonitorCallback; -import com.android.server.biometrics.sensors.HalClientMonitor; +import com.android.server.biometrics.sensors.StartUserClient; +import com.android.server.biometrics.sensors.face.aidl.AidlSession; import java.io.File; import java.util.Map; import java.util.function.Supplier; -public class FaceUpdateActiveUserClient extends HalClientMonitor<IBiometricsFace> { +public class FaceUpdateActiveUserClient extends StartUserClient<IBiometricsFace, AidlSession> { private static final String TAG = "FaceUpdateActiveUserClient"; private static final String FACE_DATA_DIR = "facedata"; @@ -45,8 +46,18 @@ public class FaceUpdateActiveUserClient extends HalClientMonitor<IBiometricsFace int sensorId, @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, boolean hasEnrolledBiometrics, @NonNull Map<Integer, Long> authenticatorIds) { - super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner, - 0 /* cookie */, sensorId, logger, biometricContext); + this(context, lazyDaemon, (newUserId, newUser, halInterfaceVersion) -> {}, + userId, owner, sensorId, logger, biometricContext, hasEnrolledBiometrics, + authenticatorIds); + } + + FaceUpdateActiveUserClient(@NonNull Context context, + @NonNull Supplier<IBiometricsFace> lazyDaemon, UserStartedCallback userStartedCallback, + int userId, @NonNull String owner, int sensorId, @NonNull BiometricLogger logger, + @NonNull BiometricContext biometricContext, boolean hasEnrolledBiometrics, + @NonNull Map<Integer, Long> authenticatorIds) { + super(context, lazyDaemon, null /* token */, userId, sensorId, logger, biometricContext, + userStartedCallback); mHasEnrolledBiometrics = hasEnrolledBiometrics; mAuthenticatorIds = authenticatorIds; } @@ -77,6 +88,7 @@ public class FaceUpdateActiveUserClient extends HalClientMonitor<IBiometricsFace daemon.setActiveUser(getTargetUserId(), storePath.getAbsolutePath()); mAuthenticatorIds.put(getTargetUserId(), mHasEnrolledBiometrics ? daemon.getAuthenticatorId().value : 0L); + mUserStartedCallback.onUserStarted(getTargetUserId(), null, 0); mCallback.onClientFinished(this, true /* success */); } catch (RemoteException e) { Slog.e(TAG, "Failed to setActiveUser: " + e); diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlCallbackConverter.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlCallbackConverter.java index 36a9790d2d4b..7a574cecc0b1 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlCallbackConverter.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlCallbackConverter.java @@ -112,4 +112,8 @@ public class HidlToAidlCallbackConverter extends IBiometricsFaceClientCallback.S void onAuthenticatorIdRetrieved(long authenticatorId) { mAidlResponseHandler.onAuthenticatorIdRetrieved(authenticatorId); } + + void onUnsupportedClientScheduled() { + mAidlResponseHandler.onUnsupportedClientScheduled(); + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapter.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapter.java new file mode 100644 index 000000000000..6355cb57a752 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapter.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 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. + */ + +package com.android.server.biometrics.sensors.face.hidl; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.UserInfo; +import android.hardware.biometrics.face.SensorProps; +import android.hardware.biometrics.face.V1_0.IBiometricsFace; +import android.os.Handler; +import android.os.IHwBinder; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.AuthSessionCoordinator; +import com.android.server.biometrics.sensors.BiometricScheduler; +import com.android.server.biometrics.sensors.LockoutResetDispatcher; +import com.android.server.biometrics.sensors.LockoutTracker; +import com.android.server.biometrics.sensors.StartUserClient; +import com.android.server.biometrics.sensors.face.FaceUtils; +import com.android.server.biometrics.sensors.face.LockoutHalImpl; +import com.android.server.biometrics.sensors.face.aidl.AidlResponseHandler; +import com.android.server.biometrics.sensors.face.aidl.AidlSession; +import com.android.server.biometrics.sensors.face.aidl.FaceProvider; +import com.android.server.biometrics.sensors.face.aidl.Sensor; + +/** + * Convert HIDL sensor configurations to an AIDL Sensor. + */ +public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRecipient{ + + private static final String TAG = "HidlToAidlSensorAdapter"; + + private IBiometricsFace mDaemon; + private AidlSession mSession; + private int mCurrentUserId = UserHandle.USER_NULL; + private final Runnable mInternalCleanupAndGetFeatureRunnable; + private final FaceProvider mFaceProvider; + private final LockoutResetDispatcher mLockoutResetDispatcher; + private final AuthSessionCoordinator mAuthSessionCoordinator; + private final AidlResponseHandler.AidlResponseHandlerCallback mAidlResponseHandlerCallback; + private final StartUserClient.UserStartedCallback<AidlSession> mUserStartedCallback = + (newUserId, newUser, halInterfaceVersion) -> { + if (newUserId != mCurrentUserId) { + handleUserChanged(newUserId); + } + }; + private LockoutHalImpl mLockoutTracker; + + public HidlToAidlSensorAdapter(@NonNull String tag, + @NonNull FaceProvider provider, + @NonNull Context context, + @NonNull Handler handler, + @NonNull SensorProps prop, + @NonNull LockoutResetDispatcher lockoutResetDispatcher, + @NonNull BiometricContext biometricContext, + boolean resetLockoutRequiresChallenge, + @NonNull Runnable internalCleanupAndGetFeatureRunnable) { + this(tag, provider, context, handler, prop, lockoutResetDispatcher, biometricContext, + resetLockoutRequiresChallenge, internalCleanupAndGetFeatureRunnable, + new AuthSessionCoordinator(), null /* daemon */, + null /* onEnrollSuccessCallback */); + } + + @VisibleForTesting + HidlToAidlSensorAdapter(@NonNull String tag, + @NonNull FaceProvider provider, + @NonNull Context context, + @NonNull Handler handler, + @NonNull SensorProps prop, + @NonNull LockoutResetDispatcher lockoutResetDispatcher, + @NonNull BiometricContext biometricContext, + boolean resetLockoutRequiresChallenge, + @NonNull Runnable internalCleanupAndGetFeatureRunnable, + @NonNull AuthSessionCoordinator authSessionCoordinator, + @Nullable IBiometricsFace daemon, + @Nullable AidlResponseHandler.AidlResponseHandlerCallback aidlResponseHandlerCallback) { + super(tag, provider, context, handler, prop, lockoutResetDispatcher, biometricContext, + resetLockoutRequiresChallenge); + mInternalCleanupAndGetFeatureRunnable = internalCleanupAndGetFeatureRunnable; + mFaceProvider = provider; + mLockoutResetDispatcher = lockoutResetDispatcher; + mAuthSessionCoordinator = authSessionCoordinator; + mDaemon = daemon; + mAidlResponseHandlerCallback = aidlResponseHandlerCallback == null + ? new AidlResponseHandler.AidlResponseHandlerCallback() { + @Override + public void onEnrollSuccess() { + scheduleFaceUpdateActiveUserClient(mCurrentUserId); + } + + @Override + public void onHardwareUnavailable() { + mDaemon = null; + mCurrentUserId = UserHandle.USER_NULL; + } + } : aidlResponseHandlerCallback; + } + + @Override + public void scheduleFaceUpdateActiveUserClient(int userId) { + getScheduler().scheduleClientMonitor(getFaceUpdateActiveUserClient(userId)); + } + + @Override + public void serviceDied(long cookie) { + Slog.d(TAG, "HAL died."); + mDaemon = null; + } + + @Override + public boolean isHardwareDetected(String halInstanceName) { + return getIBiometricsFace() != null; + } + + @Override + @LockoutTracker.LockoutMode + public int getLockoutModeForUser(int userId) { + return mLockoutTracker.getLockoutModeForUser(userId); + } + + @Override + public void init(LockoutResetDispatcher lockoutResetDispatcher, + FaceProvider provider) { + setScheduler(new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE, + null /* gestureAvailabilityTracker */)); + setLazySession(this::getSession); + mLockoutTracker = new LockoutHalImpl(); + } + + @Override + @VisibleForTesting @Nullable protected AidlSession getSessionForUser(int userId) { + if (mSession != null && mSession.getUserId() == userId) { + return mSession; + } else { + return null; + } + } + + @Override + protected LockoutTracker getLockoutTracker(boolean forAuth) { + return mLockoutTracker; + } + + @NonNull AidlSession getSession() { + if (mDaemon != null && mSession != null) { + return mSession; + } else { + return mSession = new AidlSession(getContext(), this::getIBiometricsFace, + mCurrentUserId, getAidlResponseHandler()); + } + } + + private AidlResponseHandler getAidlResponseHandler() { + return new AidlResponseHandler(getContext(), getScheduler(), getSensorProperties().sensorId, + mCurrentUserId, mLockoutTracker, mLockoutResetDispatcher, + mAuthSessionCoordinator, () -> {}, mAidlResponseHandlerCallback); + } + + private IBiometricsFace getIBiometricsFace() { + if (mFaceProvider.getTestHalEnabled()) { + final TestHal testHal = new TestHal(getContext(), getSensorProperties().sensorId); + testHal.setCallback(new HidlToAidlCallbackConverter(getAidlResponseHandler())); + return testHal; + } + + if (mDaemon != null) { + return mDaemon; + } + + Slog.d(TAG, "Daemon was null, reconnecting, current operation: " + + getScheduler().getCurrentClient()); + + try { + mDaemon = IBiometricsFace.getService(); + } catch (java.util.NoSuchElementException e) { + // Service doesn't exist or cannot be opened. + Slog.w(TAG, "NoSuchElementException", e); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to get face HAL", e); + } + + if (mDaemon == null) { + Slog.w(TAG, "Face HAL not available"); + return null; + } + + mDaemon.asBinder().linkToDeath(this, 0 /* flags */); + + scheduleLoadAuthenticatorIds(); + mInternalCleanupAndGetFeatureRunnable.run(); + return mDaemon; + } + + @VisibleForTesting void handleUserChanged(int newUserId) { + Slog.d(TAG, "User changed. Current user is " + newUserId); + mSession = null; + mCurrentUserId = newUserId; + } + + private void scheduleLoadAuthenticatorIds() { + // Note that this can be performed on the scheduler (as opposed to being done immediately + // when the HAL is (re)loaded, since + // 1) If this is truly the first time it's being performed (e.g. system has just started), + // this will be run very early and way before any applications need to generate keys. + // 2) If this is being performed to refresh the authenticatorIds (e.g. HAL crashed and has + // just been reloaded), the framework already has a cache of the authenticatorIds. This + // is safe because authenticatorIds only change when A) new template has been enrolled, + // or B) all templates are removed. + getHandler().post(() -> { + for (UserInfo user : UserManager.get(getContext()).getAliveUsers()) { + final int targetUserId = user.id; + if (!getAuthenticatorIds().containsKey(targetUserId)) { + scheduleFaceUpdateActiveUserClient(targetUserId); + } + } + }); + } + + private FaceUpdateActiveUserClient getFaceUpdateActiveUserClient(int userId) { + return new FaceUpdateActiveUserClient(getContext(), this::getIBiometricsFace, + mUserStartedCallback, userId, TAG, getSensorProperties().sensorId, + BiometricLogger.ofUnknown(getContext()), getBiometricContext(), + !FaceUtils.getInstance(getSensorProperties().sensorId).getBiometricsForUser( + getContext(), userId).isEmpty(), + getAuthenticatorIds()); + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/AidlToHidlAdapter.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapter.java index 489b213677dd..5daf2d4fbcf4 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/AidlToHidlAdapter.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapter.java @@ -47,34 +47,37 @@ import java.util.List; import java.util.function.Supplier; /** - * Adapter to convert AIDL-specific interface {@link ISession} methods to HIDL implementation. + * Adapter to convert HIDL methods into AIDL interface {@link ISession}. */ -public class AidlToHidlAdapter implements ISession { +public class HidlToAidlSessionAdapter implements ISession { + + private static final String TAG = "HidlToAidlSessionAdapter"; - private final String TAG = "AidlToHidlAdapter"; private static final int CHALLENGE_TIMEOUT_SEC = 600; @DurationMillisLong private static final int GENERATE_CHALLENGE_REUSE_INTERVAL_MILLIS = 60 * 1000; @DurationMillisLong private static final int GENERATE_CHALLENGE_COUNTER_TTL_MILLIS = CHALLENGE_TIMEOUT_SEC * 1000; private static final int INVALID_VALUE = -1; + @VisibleForTesting static final int ENROLL_TIMEOUT_SEC = 75; + private final Clock mClock; private final List<Long> mGeneratedChallengeCount = new ArrayList<>(); - @VisibleForTesting static final int ENROLL_TIMEOUT_SEC = 75; + private final int mUserId; + private final Context mContext; + private long mGenerateChallengeCreatedAt = INVALID_VALUE; private long mGenerateChallengeResult = INVALID_VALUE; @NonNull private Supplier<IBiometricsFace> mSession; - private final int mUserId; private HidlToAidlCallbackConverter mHidlToAidlCallbackConverter; - private final Context mContext; private int mFeature = INVALID_VALUE; - public AidlToHidlAdapter(Context context, Supplier<IBiometricsFace> session, + public HidlToAidlSessionAdapter(Context context, Supplier<IBiometricsFace> session, int userId, AidlResponseHandler aidlResponseHandler) { this(context, session, userId, aidlResponseHandler, Clock.systemUTC()); } - AidlToHidlAdapter(Context context, Supplier<IBiometricsFace> session, int userId, + HidlToAidlSessionAdapter(Context context, Supplier<IBiometricsFace> session, int userId, AidlResponseHandler aidlResponseHandler, Clock clock) { mSession = session; mUserId = userId; @@ -83,42 +86,11 @@ public class AidlToHidlAdapter implements ISession { setCallback(aidlResponseHandler); } - private void setCallback(AidlResponseHandler aidlResponseHandler) { - mHidlToAidlCallbackConverter = new HidlToAidlCallbackConverter(aidlResponseHandler); - try { - mSession.get().setCallback(mHidlToAidlCallbackConverter); - } catch (RemoteException e) { - Slog.d(TAG, "Failed to set callback"); - } - } - @Override public IBinder asBinder() { return null; } - private boolean isGeneratedChallengeCacheValid() { - return mGenerateChallengeCreatedAt != INVALID_VALUE - && mGenerateChallengeResult != INVALID_VALUE - && mClock.millis() - mGenerateChallengeCreatedAt - < GENERATE_CHALLENGE_REUSE_INTERVAL_MILLIS; - } - - private void incrementChallengeCount() { - mGeneratedChallengeCount.add(0, mClock.millis()); - } - - private int decrementChallengeCount() { - final long now = mClock.millis(); - // ignore values that are old in case generate/revoke calls are not matched - // this doesn't ensure revoke if calls are mismatched but it keeps the list from growing - mGeneratedChallengeCount.removeIf(x -> now - x > GENERATE_CHALLENGE_COUNTER_TTL_MILLIS); - if (!mGeneratedChallengeCount.isEmpty()) { - mGeneratedChallengeCount.remove(0); - } - return mGeneratedChallengeCount.size(); - } - @Override public void generateChallenge() throws RemoteException { incrementChallengeCount(); @@ -150,7 +122,7 @@ public class AidlToHidlAdapter implements ISession { @Override public EnrollmentStageConfig[] getEnrollmentConfig(byte enrollmentType) throws RemoteException { - //unsupported in HIDL + Slog.e(TAG, "getEnrollmentConfig unsupported in HIDL"); return null; } @@ -244,19 +216,6 @@ public class AidlToHidlAdapter implements ISession { } } - private int getFaceId() { - FaceManager faceManager = mContext.getSystemService(FaceManager.class); - List<Face> faces = faceManager.getEnrolledFaces(mUserId); - if (faces.isEmpty()) { - Slog.d(TAG, "No faces to get feature from."); - mHidlToAidlCallbackConverter.onError(0 /* deviceId */, mUserId, - BiometricFaceConstants.FACE_ERROR_NOT_ENROLLED, 0 /* vendorCode */); - return INVALID_VALUE; - } - - return faces.get(0).getBiometricId(); - } - @Override public void getAuthenticatorId() throws RemoteException { long authenticatorId = mSession.get().getAuthenticatorId().value; @@ -265,7 +224,8 @@ public class AidlToHidlAdapter implements ISession { @Override public void invalidateAuthenticatorId() throws RemoteException { - //unsupported in HIDL + Slog.e(TAG, "invalidateAuthenticatorId unsupported in HIDL"); + mHidlToAidlCallbackConverter.onUnsupportedClientScheduled(); } @Override @@ -279,47 +239,105 @@ public class AidlToHidlAdapter implements ISession { @Override public void close() throws RemoteException { - //Unsupported in HIDL + Slog.e(TAG, "close unsupported in HIDL"); } @Override public ICancellationSignal authenticateWithContext(long operationId, OperationContext context) throws RemoteException { - //Unsupported in HIDL - return null; + Slog.e(TAG, "authenticateWithContext unsupported in HIDL"); + return authenticate(operationId); } @Override public ICancellationSignal enrollWithContext(HardwareAuthToken hat, byte type, byte[] features, NativeHandle previewSurface, OperationContext context) throws RemoteException { - //Unsupported in HIDL - return null; + Slog.e(TAG, "enrollWithContext unsupported in HIDL"); + return enroll(hat, type, features, previewSurface); } @Override public ICancellationSignal detectInteractionWithContext(OperationContext context) throws RemoteException { - //Unsupported in HIDL - return null; + Slog.e(TAG, "detectInteractionWithContext unsupported in HIDL"); + return detectInteraction(); } @Override public void onContextChanged(OperationContext context) throws RemoteException { - //Unsupported in HIDL + Slog.e(TAG, "onContextChanged unsupported in HIDL"); } @Override public int getInterfaceVersion() throws RemoteException { - //Unsupported in HIDL + Slog.e(TAG, "getInterfaceVersion unsupported in HIDL"); return 0; } @Override public String getInterfaceHash() throws RemoteException { + Slog.e(TAG, "getInterfaceHash unsupported in HIDL"); + return null; + } + + @Override + public ICancellationSignal enrollWithOptions(FaceEnrollOptions options) { //Unsupported in HIDL return null; } + private boolean isGeneratedChallengeCacheValid() { + return mGenerateChallengeCreatedAt != INVALID_VALUE + && mGenerateChallengeResult != INVALID_VALUE + && mClock.millis() - mGenerateChallengeCreatedAt + < GENERATE_CHALLENGE_REUSE_INTERVAL_MILLIS; + } + + private void incrementChallengeCount() { + mGeneratedChallengeCount.add(0, mClock.millis()); + } + + private int decrementChallengeCount() { + final long now = mClock.millis(); + // ignore values that are old in case generate/revoke calls are not matched + // this doesn't ensure revoke if calls are mismatched but it keeps the list from growing + mGeneratedChallengeCount.removeIf(x -> now - x > GENERATE_CHALLENGE_COUNTER_TTL_MILLIS); + if (!mGeneratedChallengeCount.isEmpty()) { + mGeneratedChallengeCount.remove(0); + } + return mGeneratedChallengeCount.size(); + } + + private void setCallback(AidlResponseHandler aidlResponseHandler) { + mHidlToAidlCallbackConverter = new HidlToAidlCallbackConverter(aidlResponseHandler); + try { + if (mSession.get() != null) { + long halId = mSession.get().setCallback(mHidlToAidlCallbackConverter).value; + Slog.d(TAG, "Face HAL ready, HAL ID: " + halId); + if (halId == 0) { + Slog.d(TAG, "Unable to set HIDL callback."); + } + } else { + Slog.e(TAG, "Unable to set HIDL callback. HIDL daemon is null."); + } + } catch (RemoteException e) { + Slog.d(TAG, "Failed to set callback"); + } + } + + private int getFaceId() { + FaceManager faceManager = mContext.getSystemService(FaceManager.class); + List<Face> faces = faceManager.getEnrolledFaces(mUserId); + if (faces.isEmpty()) { + Slog.d(TAG, "No faces to get feature from."); + mHidlToAidlCallbackConverter.onError(0 /* deviceId */, mUserId, + BiometricFaceConstants.FACE_ERROR_NOT_ENROLLED, 0 /* vendorCode */); + return INVALID_VALUE; + } + + return faces.get(0).getBiometricId(); + } + /** * Cancellation in HIDL occurs for the entire session, instead of a specific client. */ @@ -345,10 +363,4 @@ public class AidlToHidlAdapter implements ISession { return null; } } - - @Override - public ICancellationSignal enrollWithOptions(FaceEnrollOptions options) { - //Unsupported in HIDL - return null; - } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java index 83b306b07c27..e01d672d7e34 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java @@ -45,9 +45,11 @@ import android.hardware.biometrics.ITestSession; import android.hardware.biometrics.ITestSessionCallback; import android.hardware.biometrics.fingerprint.IFingerprint; import android.hardware.biometrics.fingerprint.PointerContext; +import android.hardware.biometrics.fingerprint.SensorProps; import android.hardware.fingerprint.Fingerprint; import android.hardware.fingerprint.FingerprintAuthenticateOptions; import android.hardware.fingerprint.FingerprintManager; +import android.hardware.fingerprint.FingerprintSensorConfigurations; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.FingerprintServiceReceiver; import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback; @@ -80,6 +82,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DumpUtils; import com.android.internal.widget.LockPatternUtils; import com.android.server.SystemService; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.Utils; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.sensors.AuthenticationStateListeners; @@ -127,6 +130,8 @@ public class FingerprintService extends SystemService { @NonNull private final Function<String, FingerprintProvider> mFingerprintProvider; @NonNull + private final FingerprintProviderFunction mFingerprintProviderFunction; + @NonNull private final BiometricStateCallback<ServiceProvider, FingerprintSensorPropertiesInternal> mBiometricStateCallback; @NonNull @@ -136,6 +141,11 @@ public class FingerprintService extends SystemService { @NonNull private final FingerprintServiceRegistry mRegistry; + interface FingerprintProviderFunction { + FingerprintProvider getFingerprintProvider(Pair<String, SensorProps[]> filteredSensorProp, + boolean resetLockoutRequiresHardwareAuthToken); + } + /** Receives the incoming binder calls from FingerprintManager. */ @VisibleForTesting final IFingerprintService.Stub mServiceWrapper = new IFingerprintService.Stub() { @@ -874,6 +884,18 @@ public class FingerprintService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override // Binder call + public void registerAuthenticatorsLegacy( + @NonNull FingerprintSensorConfigurations fingerprintSensorConfigurations) { + super.registerAuthenticatorsLegacy_enforcePermission(); + if (!fingerprintSensorConfigurations.hasSensorConfigurations()) { + Slog.d(TAG, "No fingerprint sensors available."); + return; + } + mRegistry.registerAll(() -> getProviders(fingerprintSensorConfigurations)); + } + + @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) + @Override // Binder call public void registerAuthenticators( @NonNull List<FingerprintSensorPropertiesInternal> hidlSensors) { super.registerAuthenticators_enforcePermission(); @@ -1021,7 +1043,8 @@ public class FingerprintService extends SystemService { () -> IBiometricService.Stub.asInterface( ServiceManager.getService(Context.BIOMETRIC_SERVICE)), () -> ServiceManager.getDeclaredInstances(IFingerprint.DESCRIPTOR), - null /* fingerprintProvider */); + null /* fingerprintProvider */, + null /* fingerprintProviderFunction */); } @VisibleForTesting @@ -1029,7 +1052,8 @@ public class FingerprintService extends SystemService { BiometricContext biometricContext, Supplier<IBiometricService> biometricServiceSupplier, Supplier<String[]> aidlInstanceNameSupplier, - Function<String, FingerprintProvider> fingerprintProvider) { + Function<String, FingerprintProvider> fingerprintProvider, + FingerprintProviderFunction fingerprintProviderFunction) { super(context); mBiometricContext = biometricContext; mAidlInstanceNameSupplier = aidlInstanceNameSupplier; @@ -1049,7 +1073,8 @@ public class FingerprintService extends SystemService { return new FingerprintProvider(getContext(), mBiometricStateCallback, mAuthenticationStateListeners, fp.getSensorProps(), name, mLockoutResetDispatcher, - mGestureAvailabilityDispatcher, mBiometricContext); + mGestureAvailabilityDispatcher, mBiometricContext, + true /* resetLockoutRequiresHardwareAuthToken */); } catch (RemoteException e) { Slog.e(TAG, "Remote exception in getSensorProps: " + fqName); } @@ -1059,6 +1084,22 @@ public class FingerprintService extends SystemService { return null; }; + if (Flags.deHidl()) { + mFingerprintProviderFunction = fingerprintProviderFunction == null + ? (filteredSensorProps, resetLockoutRequiresHardwareAuthToken) -> + new FingerprintProvider( + getContext(), mBiometricStateCallback, + mAuthenticationStateListeners, + filteredSensorProps.second, + filteredSensorProps.first, mLockoutResetDispatcher, + mGestureAvailabilityDispatcher, + mBiometricContext, + resetLockoutRequiresHardwareAuthToken) + : fingerprintProviderFunction; + } else { + mFingerprintProviderFunction = + (filteredSensorProps, resetLockoutRequiresHardwareAuthToken) -> null; + } mHandler = new Handler(Looper.getMainLooper()); mRegistry = new FingerprintServiceRegistry(mServiceWrapper, biometricServiceSupplier); mRegistry.addAllRegisteredCallback(new IFingerprintAuthenticatorsRegisteredCallback.Stub() { @@ -1070,6 +1111,44 @@ public class FingerprintService extends SystemService { }); } + @NonNull + private List<ServiceProvider> getProviders(@NonNull FingerprintSensorConfigurations + fingerprintSensorConfigurations) { + final List<ServiceProvider> providers = new ArrayList<>(); + final Pair<String, SensorProps[]> filteredSensorProps = filterAvailableHalInstances( + fingerprintSensorConfigurations); + providers.add(mFingerprintProviderFunction.getFingerprintProvider(filteredSensorProps, + fingerprintSensorConfigurations.getResetLockoutRequiresHardwareAuthToken())); + + return providers; + } + + @NonNull + private Pair<String, SensorProps[]> filterAvailableHalInstances( + FingerprintSensorConfigurations fingerprintSensorConfigurations) { + Pair<String, SensorProps[]> finalSensorPair = + fingerprintSensorConfigurations.getSensorPair(); + if (fingerprintSensorConfigurations.isSingleSensorConfigurationPresent()) { + return finalSensorPair; + } + + final Pair<String, SensorProps[]> virtualSensorPropsPair = fingerprintSensorConfigurations + .getSensorPairForInstance("virtual"); + if (Utils.isVirtualEnabled(getContext())) { + if (virtualSensorPropsPair != null) { + return virtualSensorPropsPair; + } else { + Slog.e(TAG, "Could not find virtual interface while it is enabled"); + return finalSensorPair; + } + } else { + if (virtualSensorPropsPair != null) { + return fingerprintSensorConfigurations.getSensorPairNotForInstance("virtual"); + } + } + return finalSensorPair; + } + private Pair<List<FingerprintSensorPropertiesInternal>, List<String>> filterAvailableHalInstances( @NonNull List<FingerprintSensorPropertiesInternal> hidlInstances, diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlResponseHandler.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlResponseHandler.java index 4a019436cf6f..bd21cf4002b0 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlResponseHandler.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlResponseHandler.java @@ -25,6 +25,7 @@ import android.hardware.fingerprint.Fingerprint; import android.hardware.keymaster.HardwareAuthToken; import android.util.Slog; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.HardwareAuthTokenUtils; import com.android.server.biometrics.Utils; import com.android.server.biometrics.sensors.AcquisitionClient; @@ -34,9 +35,9 @@ import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.EnumerateConsumer; import com.android.server.biometrics.sensors.ErrorConsumer; -import com.android.server.biometrics.sensors.LockoutCache; import com.android.server.biometrics.sensors.LockoutConsumer; import com.android.server.biometrics.sensors.LockoutResetDispatcher; +import com.android.server.biometrics.sensors.LockoutTracker; import com.android.server.biometrics.sensors.RemovalConsumer; import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils; @@ -59,6 +60,21 @@ public class AidlResponseHandler extends ISessionCallback.Stub { void onHardwareUnavailable(); } + /** + * Interface to send results to the AidlResponseHandler's owner. + */ + public interface AidlResponseHandlerCallback { + /** + * Invoked when enrollment is successful. + */ + void onEnrollSuccess(); + + /** + * Invoked when the HAL sends ERROR_HW_UNAVAILABLE. + */ + void onHardwareUnavailable(); + } + private static final String TAG = "AidlResponseHandler"; @NonNull @@ -68,28 +84,49 @@ public class AidlResponseHandler extends ISessionCallback.Stub { private final int mSensorId; private final int mUserId; @NonNull - private final LockoutCache mLockoutCache; + private final LockoutTracker mLockoutTracker; @NonNull private final LockoutResetDispatcher mLockoutResetDispatcher; @NonNull private final AuthSessionCoordinator mAuthSessionCoordinator; @NonNull private final HardwareUnavailableCallback mHardwareUnavailableCallback; + @NonNull + private final AidlResponseHandlerCallback mAidlResponseHandlerCallback; public AidlResponseHandler(@NonNull Context context, @NonNull BiometricScheduler scheduler, int sensorId, int userId, - @NonNull LockoutCache lockoutTracker, + @NonNull LockoutTracker lockoutTracker, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull AuthSessionCoordinator authSessionCoordinator, @NonNull HardwareUnavailableCallback hardwareUnavailableCallback) { + this(context, scheduler, sensorId, userId, lockoutTracker, lockoutResetDispatcher, + authSessionCoordinator, hardwareUnavailableCallback, + new AidlResponseHandlerCallback() { + @Override + public void onEnrollSuccess() {} + + @Override + public void onHardwareUnavailable() {} + }); + } + + public AidlResponseHandler(@NonNull Context context, + @NonNull BiometricScheduler scheduler, int sensorId, int userId, + @NonNull LockoutTracker lockoutTracker, + @NonNull LockoutResetDispatcher lockoutResetDispatcher, + @NonNull AuthSessionCoordinator authSessionCoordinator, + @NonNull HardwareUnavailableCallback hardwareUnavailableCallback, + @NonNull AidlResponseHandlerCallback aidlResponseHandlerCallback) { mContext = context; mScheduler = scheduler; mSensorId = sensorId; mUserId = userId; - mLockoutCache = lockoutTracker; + mLockoutTracker = lockoutTracker; mLockoutResetDispatcher = lockoutResetDispatcher; mAuthSessionCoordinator = authSessionCoordinator; mHardwareUnavailableCallback = hardwareUnavailableCallback; + mAidlResponseHandlerCallback = aidlResponseHandlerCallback; } @Override @@ -105,27 +142,26 @@ public class AidlResponseHandler extends ISessionCallback.Stub { @Override public void onChallengeGenerated(long challenge) { handleResponse(FingerprintGenerateChallengeClient.class, (c) -> c.onChallengeGenerated( - mSensorId, mUserId, challenge), null); + mSensorId, mUserId, challenge)); } @Override public void onChallengeRevoked(long challenge) { handleResponse(FingerprintRevokeChallengeClient.class, (c) -> c.onChallengeRevoked( - challenge), null); + challenge)); } /** * Handles acquired messages sent by the HAL (specifically for HIDL HAL). */ public void onAcquired(int acquiredInfo, int vendorCode) { - handleResponse(AcquisitionClient.class, (c) -> c.onAcquired(acquiredInfo, vendorCode), - null); + handleResponse(AcquisitionClient.class, (c) -> c.onAcquired(acquiredInfo, vendorCode)); } @Override public void onAcquired(byte info, int vendorCode) { handleResponse(AcquisitionClient.class, (c) -> c.onAcquired( - AidlConversionUtils.toFrameworkAcquiredInfo(info), vendorCode), null); + AidlConversionUtils.toFrameworkAcquiredInfo(info), vendorCode)); } /** @@ -135,9 +171,13 @@ public class AidlResponseHandler extends ISessionCallback.Stub { handleResponse(ErrorConsumer.class, (c) -> { c.onError(error, vendorCode); if (error == Error.HW_UNAVAILABLE) { - mHardwareUnavailableCallback.onHardwareUnavailable(); + if (Flags.deHidl()) { + mAidlResponseHandlerCallback.onHardwareUnavailable(); + } else { + mHardwareUnavailableCallback.onHardwareUnavailable(); + } } - }, null); + }); } @Override @@ -158,8 +198,12 @@ public class AidlResponseHandler extends ISessionCallback.Stub { .getUniqueName(mContext, currentUserId); final Fingerprint fingerprint = new Fingerprint(name, currentUserId, enrollmentId, mSensorId); - handleResponse(FingerprintEnrollClient.class, (c) -> c.onEnrollResult(fingerprint, - remaining), null); + handleResponse(FingerprintEnrollClient.class, (c) -> { + c.onEnrollResult(fingerprint, remaining); + if (remaining == 0) { + mAidlResponseHandlerCallback.onEnrollSuccess(); + } + }); } @Override @@ -184,13 +228,12 @@ public class AidlResponseHandler extends ISessionCallback.Stub { @Override public void onLockoutTimed(long durationMillis) { - handleResponse(LockoutConsumer.class, (c) -> c.onLockoutTimed(durationMillis), - null); + handleResponse(LockoutConsumer.class, (c) -> c.onLockoutTimed(durationMillis)); } @Override public void onLockoutPermanent() { - handleResponse(LockoutConsumer.class, LockoutConsumer::onLockoutPermanent, null); + handleResponse(LockoutConsumer.class, LockoutConsumer::onLockoutPermanent); } @Override @@ -198,7 +241,7 @@ public class AidlResponseHandler extends ISessionCallback.Stub { handleResponse(FingerprintResetLockoutClient.class, FingerprintResetLockoutClient::onLockoutCleared, (c) -> FingerprintResetLockoutClient.resetLocalLockoutStateToNone( - mSensorId, mUserId, mLockoutCache, mLockoutResetDispatcher, + mSensorId, mUserId, mLockoutTracker, mLockoutResetDispatcher, mAuthSessionCoordinator, Utils.getCurrentStrength(mSensorId), -1 /* requestId */)); } @@ -206,49 +249,74 @@ public class AidlResponseHandler extends ISessionCallback.Stub { @Override public void onInteractionDetected() { handleResponse(FingerprintDetectClient.class, - FingerprintDetectClient::onInteractionDetected, null); + FingerprintDetectClient::onInteractionDetected); } @Override public void onEnrollmentsEnumerated(int[] enrollmentIds) { if (enrollmentIds.length > 0) { for (int i = 0; i < enrollmentIds.length; i++) { - final Fingerprint fp = new Fingerprint("", enrollmentIds[i], mSensorId); - int finalI = i; - handleResponse(EnumerateConsumer.class, (c) -> c.onEnumerationResult(fp, - enrollmentIds.length - finalI - 1), null); + onEnrollmentEnumerated(enrollmentIds[i], + enrollmentIds.length - i - 1 /* remaining */); } } else { - handleResponse(EnumerateConsumer.class, (c) -> c.onEnumerationResult(null, - 0), null); + handleResponse(EnumerateConsumer.class, (c) -> c.onEnumerationResult( + null /* identifier */, + 0 /* remaining */)); } } + /** + * Handle enumerated fingerprint. + */ + public void onEnrollmentEnumerated(int enrollmentId, int remaining) { + final Fingerprint fp = new Fingerprint("", enrollmentId, mSensorId); + handleResponse(EnumerateConsumer.class, (c) -> c.onEnumerationResult(fp, remaining)); + } + + /** + * Handle removal of fingerprint. + */ + public void onEnrollmentRemoved(int enrollmentId, int remaining) { + final Fingerprint fp = new Fingerprint("", enrollmentId, mSensorId); + handleResponse(RemovalConsumer.class, (c) -> c.onRemoved(fp, remaining)); + } + @Override public void onEnrollmentsRemoved(int[] enrollmentIds) { if (enrollmentIds.length > 0) { for (int i = 0; i < enrollmentIds.length; i++) { - final Fingerprint fp = new Fingerprint("", enrollmentIds[i], mSensorId); - int finalI = i; - handleResponse(RemovalConsumer.class, (c) -> c.onRemoved(fp, - enrollmentIds.length - finalI - 1), null); + onEnrollmentRemoved(enrollmentIds[i], enrollmentIds.length - i - 1 /* remaining */); } } else { - handleResponse(RemovalConsumer.class, (c) -> c.onRemoved(null, 0), - null); + handleResponse(RemovalConsumer.class, (c) -> c.onRemoved(null /* identifier */, + 0 /* remaining */)); } } @Override public void onAuthenticatorIdRetrieved(long authenticatorId) { handleResponse(FingerprintGetAuthenticatorIdClient.class, - (c) -> c.onAuthenticatorIdRetrieved(authenticatorId), null); + (c) -> c.onAuthenticatorIdRetrieved(authenticatorId)); } @Override public void onAuthenticatorIdInvalidated(long newAuthenticatorId) { handleResponse(FingerprintInvalidationClient.class, (c) -> c.onAuthenticatorIdInvalidated( - newAuthenticatorId), null); + newAuthenticatorId)); + } + + /** + * Handle clients which are not supported in HIDL HAL. + */ + public <T extends BaseClientMonitor> void onUnsupportedClientScheduled(Class<T> className) { + Slog.e(TAG, className + " is not supported in the HAL."); + handleResponse(className, (c) -> c.cancel()); + } + + private <T> void handleResponse(@NonNull Class<T> className, + @NonNull Consumer<T> action) { + handleResponse(className, action, null /* alternateAction */); } private <T> void handleResponse(@NonNull Class<T> className, diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlSession.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlSession.java index 299a310caee9..8ff105baa981 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlSession.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlSession.java @@ -20,7 +20,7 @@ import android.annotation.NonNull; import android.hardware.biometrics.fingerprint.ISession; import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint; -import com.android.server.biometrics.sensors.fingerprint.hidl.AidlToHidlAdapter; +import com.android.server.biometrics.sensors.fingerprint.hidl.HidlToAidlSessionAdapter; import java.util.function.Supplier; @@ -45,7 +45,7 @@ public class AidlSession { public AidlSession(@NonNull Supplier<IBiometricsFingerprint> session, int userId, AidlResponseHandler aidlResponseHandler) { - mSession = new AidlToHidlAdapter(session, userId, aidlResponseHandler); + mSession = new HidlToAidlSessionAdapter(session, userId, aidlResponseHandler); mHalInterfaceVersion = 0; mUserId = userId; mAidlResponseHandler = aidlResponseHandler; @@ -62,7 +62,7 @@ public class AidlSession { } /** The HAL callback, which should only be used in tests {@See BiometricTestSessionImpl}. */ - AidlResponseHandler getHalSessionCallback() { + public AidlResponseHandler getHalSessionCallback() { return mAidlResponseHandler; } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGetAuthenticatorIdClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGetAuthenticatorIdClient.java index ea1a622c36ab..03539690c0a8 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGetAuthenticatorIdClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGetAuthenticatorIdClient.java @@ -30,7 +30,7 @@ import com.android.server.biometrics.sensors.HalClientMonitor; import java.util.Map; import java.util.function.Supplier; -class FingerprintGetAuthenticatorIdClient extends HalClientMonitor<AidlSession> { +public class FingerprintGetAuthenticatorIdClient extends HalClientMonitor<AidlSession> { private static final String TAG = "FingerprintGetAuthenticatorIdClient"; diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java index 032ab87196f8..88a11d9c0ceb 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java @@ -59,6 +59,7 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; import com.android.server.biometrics.AuthenticationStatsBroadcastReceiver; import com.android.server.biometrics.AuthenticationStatsCollector; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.Utils; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; @@ -73,6 +74,7 @@ import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback; import com.android.server.biometrics.sensors.InvalidationRequesterClient; import com.android.server.biometrics.sensors.LockoutResetDispatcher; +import com.android.server.biometrics.sensors.LockoutTracker; import com.android.server.biometrics.sensors.PerformanceTracker; import com.android.server.biometrics.sensors.SensorList; import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils; @@ -80,6 +82,7 @@ import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDisp import com.android.server.biometrics.sensors.fingerprint.PowerPressHandler; import com.android.server.biometrics.sensors.fingerprint.ServiceProvider; import com.android.server.biometrics.sensors.fingerprint.Udfps; +import com.android.server.biometrics.sensors.fingerprint.hidl.HidlToAidlSensorAdapter; import org.json.JSONArray; import org.json.JSONException; @@ -165,10 +168,12 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @NonNull SensorProps[] props, @NonNull String halInstanceName, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, - @NonNull BiometricContext biometricContext) { + @NonNull BiometricContext biometricContext, + boolean resetLockoutRequiresHardwareAuthToken) { this(context, biometricStateCallback, authenticationStateListeners, props, halInstanceName, lockoutResetDispatcher, gestureAvailabilityDispatcher, biometricContext, - null /* daemon */); + null /* daemon */, resetLockoutRequiresHardwareAuthToken, + false /* testHalEnabled */); } @VisibleForTesting FingerprintProvider(@NonNull Context context, @@ -178,7 +183,9 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, @NonNull BiometricContext biometricContext, - IFingerprint daemon) { + @Nullable IFingerprint daemon, + boolean resetLockoutRequiresHardwareAuthToken, + boolean testHalEnabled) { mContext = context; mBiometricStateCallback = biometricStateCallback; mAuthenticationStateListeners = authenticationStateListeners; @@ -191,62 +198,136 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mBiometricContext = biometricContext; mAuthSessionCoordinator = mBiometricContext.getAuthSessionCoordinator(); mDaemon = daemon; + mTestHalEnabled = testHalEnabled; - AuthenticationStatsBroadcastReceiver mBroadcastReceiver = - new AuthenticationStatsBroadcastReceiver( - mContext, - BiometricsProtoEnums.MODALITY_FINGERPRINT, - (AuthenticationStatsCollector collector) -> { - Slog.d(getTag(), "Initializing AuthenticationStatsCollector"); - mAuthenticationStatsCollector = collector; - }); - - final List<SensorLocationInternal> workaroundLocations = getWorkaroundSensorProps(context); + initAuthenticationBroadcastReceiver(); + initSensors(resetLockoutRequiresHardwareAuthToken, props, gestureAvailabilityDispatcher); + } - for (SensorProps prop : props) { - final int sensorId = prop.commonProps.sensorId; + private void initAuthenticationBroadcastReceiver() { + new AuthenticationStatsBroadcastReceiver( + mContext, + BiometricsProtoEnums.MODALITY_FINGERPRINT, + (AuthenticationStatsCollector collector) -> { + Slog.d(getTag(), "Initializing AuthenticationStatsCollector"); + mAuthenticationStatsCollector = collector; + }); + } - final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); - if (prop.commonProps.componentInfo != null) { - for (ComponentInfo info : prop.commonProps.componentInfo) { - componentInfo.add(new ComponentInfoInternal(info.componentId, - info.hardwareVersion, info.firmwareVersion, info.serialNumber, - info.softwareVersion)); + private void initSensors(boolean resetLockoutRequiresHardwareAuthToken, SensorProps[] props, + GestureAvailabilityDispatcher gestureAvailabilityDispatcher) { + if (Flags.deHidl()) { + if (!resetLockoutRequiresHardwareAuthToken) { + Slog.d(getTag(), "Adding HIDL configs"); + for (SensorProps sensorConfig: props) { + addHidlSensors(sensorConfig, gestureAvailabilityDispatcher, + resetLockoutRequiresHardwareAuthToken); + } + } else { + Slog.d(getTag(), "Adding AIDL configs"); + final List<SensorLocationInternal> workaroundLocations = + getWorkaroundSensorProps(mContext); + for (SensorProps prop : props) { + addAidlSensors(prop, gestureAvailabilityDispatcher, workaroundLocations, + resetLockoutRequiresHardwareAuthToken); } } - - final FingerprintSensorPropertiesInternal internalProp = - new FingerprintSensorPropertiesInternal(prop.commonProps.sensorId, - prop.commonProps.sensorStrength, - prop.commonProps.maxEnrollmentsPerUser, - componentInfo, - prop.sensorType, - prop.halControlsIllumination, - true /* resetLockoutRequiresHardwareAuthToken */, - !workaroundLocations.isEmpty() ? workaroundLocations : - Arrays.stream(prop.sensorLocations).map(location -> - new SensorLocationInternal( - location.display, - location.sensorLocationX, - location.sensorLocationY, - location.sensorRadius)) - .collect(Collectors.toList())); - final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext, mHandler, - internalProp, lockoutResetDispatcher, gestureAvailabilityDispatcher, - mBiometricContext); - final int sessionUserId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL : - sensor.getLazySession().get().getUserId(); - mFingerprintSensors.addSensor(sensorId, sensor, sessionUserId, - new SynchronousUserSwitchObserver() { - @Override - public void onUserSwitching(int newUserId) { - scheduleInternalCleanup(sensorId, newUserId, null /* callback */); - } - }); - Slog.d(getTag(), "Added: " + internalProp); + } else { + final List<SensorLocationInternal> workaroundLocations = + getWorkaroundSensorProps(mContext); + + for (SensorProps prop : props) { + final int sensorId = prop.commonProps.sensorId; + final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); + if (prop.commonProps.componentInfo != null) { + for (ComponentInfo info : prop.commonProps.componentInfo) { + componentInfo.add(new ComponentInfoInternal(info.componentId, + info.hardwareVersion, info.firmwareVersion, info.serialNumber, + info.softwareVersion)); + } + } + final FingerprintSensorPropertiesInternal internalProp = + new FingerprintSensorPropertiesInternal(prop.commonProps.sensorId, + prop.commonProps.sensorStrength, + prop.commonProps.maxEnrollmentsPerUser, + componentInfo, + prop.sensorType, + prop.halControlsIllumination, + true /* resetLockoutRequiresHardwareAuthToken */, + !workaroundLocations.isEmpty() ? workaroundLocations : + Arrays.stream(prop.sensorLocations).map( + location -> new SensorLocationInternal( + location.display, + location.sensorLocationX, + location.sensorLocationY, + location.sensorRadius)) + .collect(Collectors.toList())); + final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext, + mHandler, internalProp, mLockoutResetDispatcher, + gestureAvailabilityDispatcher, mBiometricContext); + sensor.init(gestureAvailabilityDispatcher, + mLockoutResetDispatcher); + final int sessionUserId = + sensor.getLazySession().get() == null ? UserHandle.USER_NULL : + sensor.getLazySession().get().getUserId(); + mFingerprintSensors.addSensor(sensorId, sensor, sessionUserId, + new SynchronousUserSwitchObserver() { + @Override + public void onUserSwitching(int newUserId) { + scheduleInternalCleanup(sensorId, newUserId, null /* callback */); + } + }); + Slog.d(getTag(), "Added: " + mFingerprintSensors.get(sensorId).toString()); + } } } + private void addHidlSensors(@NonNull SensorProps prop, + @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, + boolean resetLockoutRequiresHardwareAuthToken) { + final int sensorId = prop.commonProps.sensorId; + final Sensor sensor = new HidlToAidlSensorAdapter(getTag() + "/" + + sensorId, this, mContext, mHandler, + prop, mLockoutResetDispatcher, gestureAvailabilityDispatcher, + mBiometricContext, resetLockoutRequiresHardwareAuthToken, + () -> scheduleInternalCleanup(sensorId, ActivityManager.getCurrentUser(), + null /* callback */)); + sensor.init(gestureAvailabilityDispatcher, mLockoutResetDispatcher); + final int sessionUserId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL : + sensor.getLazySession().get().getUserId(); + mFingerprintSensors.addSensor(sensorId, sensor, sessionUserId, + new SynchronousUserSwitchObserver() { + @Override + public void onUserSwitching(int newUserId) { + scheduleInternalCleanup(sensorId, newUserId, null /* callback */); + } + }); + Slog.d(getTag(), "Added: " + mFingerprintSensors.get(sensorId).toString()); + } + + private void addAidlSensors(@NonNull SensorProps prop, + @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, + List<SensorLocationInternal> workaroundLocations, + boolean resetLockoutRequiresHardwareAuthToken) { + final int sensorId = prop.commonProps.sensorId; + final Sensor sensor = new Sensor(getTag() + "/" + sensorId, + this, mContext, mHandler, + prop, mLockoutResetDispatcher, gestureAvailabilityDispatcher, + mBiometricContext, workaroundLocations, + resetLockoutRequiresHardwareAuthToken); + sensor.init(gestureAvailabilityDispatcher, mLockoutResetDispatcher); + final int sessionUserId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL : + sensor.getLazySession().get().getUserId(); + mFingerprintSensors.addSensor(sensorId, sensor, sessionUserId, + new SynchronousUserSwitchObserver() { + @Override + public void onUserSwitching(int newUserId) { + scheduleInternalCleanup(sensorId, newUserId, null /* callback */); + } + }); + Slog.d(getTag(), "Added: " + mFingerprintSensors.get(sensorId).toString()); + } + private String getTag() { return "FingerprintProvider/" + mHalInstanceName; } @@ -351,7 +432,10 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi } } - private void scheduleLoadAuthenticatorIdsForUser(int sensorId, int userId) { + /** + * Schedules FingerprintGetAuthenticatorIdClient for specific sensor and user. + */ + protected void scheduleLoadAuthenticatorIdsForUser(int sensorId, int userId) { mHandler.post(() -> { final FingerprintGetAuthenticatorIdClient client = new FingerprintGetAuthenticatorIdClient(mContext, @@ -387,8 +471,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext, hardwareAuthToken, - mFingerprintSensors.get(sensorId).getLockoutCache(), mLockoutResetDispatcher, - Utils.getCurrentStrength(sensorId)); + mFingerprintSensors.get(sensorId).getLockoutTracker(false /* forAuth */), + mLockoutResetDispatcher, Utils.getCurrentStrength(sensorId)); scheduleForSensor(sensorId, client); }); } @@ -443,18 +527,23 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mFingerprintSensors.get(sensorId).getSensorProperties(), mUdfpsOverlayController, mSidefpsController, mAuthenticationStateListeners, maxTemplatesPerUser, enrollReason); - scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback( - mBiometricStateCallback, new ClientMonitorCallback() { - @Override - public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, - boolean success) { - ClientMonitorCallback.super.onClientFinished(clientMonitor, success); - if (success) { - scheduleLoadAuthenticatorIdsForUser(sensorId, userId); - scheduleInvalidationRequest(sensorId, userId); - } - } - })); + if (Flags.deHidl()) { + scheduleForSensor(sensorId, client, mBiometricStateCallback); + } else { + scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback( + mBiometricStateCallback, new ClientMonitorCallback() { + @Override + public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, + boolean success) { + ClientMonitorCallback.super.onClientFinished( + clientMonitor, success); + if (success) { + scheduleLoadAuthenticatorIdsForUser(sensorId, userId); + scheduleInvalidationRequest(sensorId, userId); + } + } + })); + } }); return id; } @@ -497,6 +586,13 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi final int userId = options.getUserId(); final int sensorId = options.getSensorId(); final boolean isStrongBiometric = Utils.isStrongBiometric(sensorId); + final LockoutTracker lockoutTracker; + if (Flags.deHidl()) { + lockoutTracker = mFingerprintSensors.get(sensorId) + .getLockoutTracker(true /* forAuth */); + } else { + lockoutTracker = null; + } final FingerprintAuthenticationClient client = new FingerprintAuthenticationClient( mContext, mFingerprintSensors.get(sensorId).getLazySession(), token, requestId, callback, operationId, restricted, options, cookie, @@ -510,7 +606,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mFingerprintSensors.get(sensorId).getSensorProperties(), mHandler, Utils.getCurrentStrength(sensorId), SystemClock.elapsedRealtimeClock(), - null /* lockoutTracker */); + lockoutTracker); scheduleForSensor(sensorId, client, new ClientMonitorCallback() { @Override @@ -636,6 +732,9 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @Override public boolean isHardwareDetected(int sensorId) { + if (Flags.deHidl()) { + return mFingerprintSensors.get(sensorId).isHardwareDetected(mHalInstanceName); + } return hasHalInstance(); } @@ -674,8 +773,12 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @Override public int getLockoutModeForUser(int sensorId, int userId) { - return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId, - Utils.getCurrentStrength(sensorId)); + if (Flags.deHidl()) { + return mFingerprintSensors.get(sensorId).getLockoutModeForUser(userId); + } else { + return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId, + Utils.getCurrentStrength(sensorId)); + } } @Override @@ -829,6 +932,10 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mTestHalEnabled = enabled; } + public boolean getTestHalEnabled() { + return mTestHalEnabled; + } + // TODO(b/174868353): workaround for gaps in HAL interface (remove and get directly from HAL) // reads values via an overlay instead of querying the HAL @NonNull diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java index ec225a60d54b..387ae12147ae 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java @@ -60,7 +60,8 @@ public class FingerprintResetLockoutClient extends HalClientMonitor<AidlSession> @Authenticators.Types int biometricStrength) { super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner, 0 /* cookie */, sensorId, biometricLogger, biometricContext); - mHardwareAuthToken = HardwareAuthTokenUtils.toHardwareAuthToken(hardwareAuthToken); + mHardwareAuthToken = hardwareAuthToken == null ? null : + HardwareAuthTokenUtils.toHardwareAuthToken(hardwareAuthToken); mLockoutCache = lockoutTracker; mLockoutResetDispatcher = lockoutResetDispatcher; mBiometricStrength = biometricStrength; diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java index 893cb8f9b4fc..dd887bb05c12 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java @@ -21,21 +21,28 @@ import android.annotation.Nullable; import android.content.Context; import android.content.pm.UserInfo; import android.hardware.biometrics.BiometricsProtoEnums; +import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.ITestSession; import android.hardware.biometrics.ITestSessionCallback; +import android.hardware.biometrics.SensorLocationInternal; +import android.hardware.biometrics.common.ComponentInfo; +import android.hardware.biometrics.fingerprint.IFingerprint; import android.hardware.biometrics.fingerprint.ISession; +import android.hardware.biometrics.fingerprint.SensorProps; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; +import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; import android.util.Slog; import android.util.proto.ProtoOutputStream; import com.android.internal.util.FrameworkStatsLog; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.SensorServiceStateProto; import com.android.server.biometrics.SensorStateProto; import com.android.server.biometrics.UserStateProto; @@ -48,15 +55,20 @@ import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.ErrorConsumer; import com.android.server.biometrics.sensors.LockoutCache; import com.android.server.biometrics.sensors.LockoutResetDispatcher; +import com.android.server.biometrics.sensors.LockoutTracker; import com.android.server.biometrics.sensors.StartUserClient; import com.android.server.biometrics.sensors.StopUserClient; import com.android.server.biometrics.sensors.UserAwareBiometricScheduler; import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils; import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.function.Supplier; +import java.util.stream.Collectors; /** * Maintains the state of a single sensor within an instance of the @@ -73,15 +85,17 @@ public class Sensor { @NonNull private final IBinder mToken; @NonNull private final Handler mHandler; @NonNull private final FingerprintSensorPropertiesInternal mSensorProperties; - @NonNull private final UserAwareBiometricScheduler mScheduler; - @NonNull private final LockoutCache mLockoutCache; + @NonNull private BiometricScheduler mScheduler; + @NonNull private LockoutTracker mLockoutTracker; @NonNull private final Map<Integer, Long> mAuthenticatorIds; + @NonNull private final BiometricContext mBiometricContext; @Nullable AidlSession mCurrentSession; - @NonNull private final Supplier<AidlSession> mLazySession; + @NonNull private Supplier<AidlSession> mLazySession; - Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, @NonNull Context context, - @NonNull Handler handler, @NonNull FingerprintSensorPropertiesInternal sensorProperties, + public Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, + @NonNull Context context, @NonNull Handler handler, + @NonNull FingerprintSensorPropertiesInternal sensorProperties, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, @NonNull BiometricContext biometricContext, AidlSession session) { @@ -91,8 +105,38 @@ public class Sensor { mToken = new Binder(); mHandler = handler; mSensorProperties = sensorProperties; - mLockoutCache = new LockoutCache(); - mScheduler = new UserAwareBiometricScheduler(tag, + mBiometricContext = biometricContext; + mAuthenticatorIds = new HashMap<>(); + mCurrentSession = session; + } + + Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, @NonNull Context context, + @NonNull Handler handler, @NonNull FingerprintSensorPropertiesInternal sensorProperties, + @NonNull LockoutResetDispatcher lockoutResetDispatcher, + @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, + @NonNull BiometricContext biometricContext) { + this(tag, provider, context, handler, sensorProperties, lockoutResetDispatcher, + gestureAvailabilityDispatcher, biometricContext, null); + } + + Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, @NonNull Context context, + @NonNull Handler handler, @NonNull SensorProps sensorProp, + @NonNull LockoutResetDispatcher lockoutResetDispatcher, + @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, + @NonNull BiometricContext biometricContext, + @NonNull List<SensorLocationInternal> workaroundLocation, + boolean resetLockoutRequiresHardwareAuthToken) { + this(tag, provider, context, handler, getFingerprintSensorPropertiesInternal(sensorProp, + workaroundLocation, resetLockoutRequiresHardwareAuthToken), + lockoutResetDispatcher, gestureAvailabilityDispatcher, biometricContext, null); + } + + /** + * Initialize biometric scheduler, lockout tracker and session for the sensor. + */ + public void init(GestureAvailabilityDispatcher gestureAvailabilityDispatcher, + LockoutResetDispatcher lockoutResetDispatcher) { + mScheduler = new UserAwareBiometricScheduler(mTag, BiometricScheduler.sensorTypeFromFingerprintProperties(mSensorProperties), gestureAvailabilityDispatcher, () -> mCurrentSession != null ? mCurrentSession.getUserId() : UserHandle.USER_NULL, @@ -102,7 +146,7 @@ public class Sensor { public StopUserClient<?> getStopUserClient(int userId) { return new FingerprintStopUserClient(mContext, mLazySession, mToken, userId, mSensorProperties.sensorId, - BiometricLogger.ofUnknown(mContext), biometricContext, + BiometricLogger.ofUnknown(mContext), mBiometricContext, () -> mCurrentSession = null); } @@ -111,13 +155,38 @@ public class Sensor { public StartUserClient<?, ?> getStartUserClient(int newUserId) { final int sensorId = mSensorProperties.sensorId; - final AidlResponseHandler resultController = new AidlResponseHandler( - mContext, mScheduler, sensorId, newUserId, - mLockoutCache, lockoutResetDispatcher, - biometricContext.getAuthSessionCoordinator(), () -> { - Slog.e(mTag, "Got ERROR_HW_UNAVAILABLE"); - mCurrentSession = null; - }); + final AidlResponseHandler resultController; + + if (Flags.deHidl()) { + resultController = new AidlResponseHandler( + mContext, mScheduler, sensorId, newUserId, + mLockoutTracker, lockoutResetDispatcher, + mBiometricContext.getAuthSessionCoordinator(), () -> {}, + new AidlResponseHandler.AidlResponseHandlerCallback() { + @Override + public void onEnrollSuccess() { + mProvider.scheduleLoadAuthenticatorIdsForUser(sensorId, + newUserId); + mProvider.scheduleInvalidationRequest(sensorId, + newUserId); + } + + @Override + public void onHardwareUnavailable() { + Slog.e(mTag, + "Fingerprint sensor hardware unavailable."); + mCurrentSession = null; + } + }); + } else { + resultController = new AidlResponseHandler( + mContext, mScheduler, sensorId, newUserId, + mLockoutTracker, lockoutResetDispatcher, + mBiometricContext.getAuthSessionCoordinator(), () -> { + Slog.e(mTag, "Got ERROR_HW_UNAVAILABLE"); + mCurrentSession = null; + }); + } final StartUserClient.UserStartedCallback<ISession> userStartedCallback = (userIdStarted, newSession, halInterfaceVersion) -> { @@ -133,40 +202,58 @@ public class Sensor { + "sensor: " + sensorId + ", user: " + userIdStarted); - provider.scheduleInvalidationRequest(sensorId, + mProvider.scheduleInvalidationRequest(sensorId, userIdStarted); } }; - return new FingerprintStartUserClient(mContext, provider::getHalInstance, + return new FingerprintStartUserClient(mContext, mProvider::getHalInstance, mToken, newUserId, mSensorProperties.sensorId, - BiometricLogger.ofUnknown(mContext), biometricContext, + BiometricLogger.ofUnknown(mContext), mBiometricContext, resultController, userStartedCallback); } }); - mAuthenticatorIds = new HashMap<>(); + mLockoutTracker = new LockoutCache(); mLazySession = () -> mCurrentSession != null ? mCurrentSession : null; - mCurrentSession = session; } - Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, @NonNull Context context, - @NonNull Handler handler, @NonNull FingerprintSensorPropertiesInternal sensorProperties, - @NonNull LockoutResetDispatcher lockoutResetDispatcher, - @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, - @NonNull BiometricContext biometricContext) { - this(tag, provider, context, handler, sensorProperties, lockoutResetDispatcher, - gestureAvailabilityDispatcher, biometricContext, null); + protected static FingerprintSensorPropertiesInternal getFingerprintSensorPropertiesInternal( + SensorProps prop, List<SensorLocationInternal> workaroundLocations, + boolean resetLockoutRequiresHardwareAuthToken) { + final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); + if (prop.commonProps.componentInfo != null) { + for (ComponentInfo info : prop.commonProps.componentInfo) { + componentInfo.add(new ComponentInfoInternal(info.componentId, + info.hardwareVersion, info.firmwareVersion, info.serialNumber, + info.softwareVersion)); + } + } + return new FingerprintSensorPropertiesInternal(prop.commonProps.sensorId, + prop.commonProps.sensorStrength, + prop.commonProps.maxEnrollmentsPerUser, + componentInfo, + prop.sensorType, + prop.halControlsIllumination, + resetLockoutRequiresHardwareAuthToken, + !workaroundLocations.isEmpty() ? workaroundLocations : + Arrays.stream(prop.sensorLocations).map(location -> + new SensorLocationInternal( + location.display, + location.sensorLocationX, + location.sensorLocationY, + location.sensorRadius)) + .collect(Collectors.toList())); } - @NonNull Supplier<AidlSession> getLazySession() { + @NonNull public Supplier<AidlSession> getLazySession() { return mLazySession; } - @NonNull FingerprintSensorPropertiesInternal getSensorProperties() { + @NonNull public FingerprintSensorPropertiesInternal getSensorProperties() { return mSensorProperties; } - @Nullable AidlSession getSessionForUser(int userId) { + @Nullable protected AidlSession getSessionForUser(int userId) { if (mCurrentSession != null && mCurrentSession.getUserId() == userId) { return mCurrentSession; } else { @@ -180,15 +267,18 @@ public class Sensor { biometricStateCallback, mProvider, this); } - @NonNull BiometricScheduler getScheduler() { + @NonNull public BiometricScheduler getScheduler() { return mScheduler; } - @NonNull LockoutCache getLockoutCache() { - return mLockoutCache; + @NonNull protected LockoutTracker getLockoutTracker(boolean forAuth) { + if (forAuth) { + return null; + } + return mLockoutTracker; } - @NonNull Map<Integer, Long> getAuthenticatorIds() { + @NonNull public Map<Integer, Long> getAuthenticatorIds() { return mAuthenticatorIds; } @@ -262,4 +352,49 @@ public class Sensor { mScheduler.reset(); mCurrentSession = null; } + + @NonNull protected Handler getHandler() { + return mHandler; + } + + @NonNull protected Context getContext() { + return mContext; + } + + /** + * Returns true if the sensor hardware is detected. + */ + protected boolean isHardwareDetected(String halInstance) { + if (mTestHalEnabled) { + return true; + } + return (ServiceManager.checkService(IFingerprint.DESCRIPTOR + "/" + halInstance) + != null); + } + + @NonNull protected BiometricContext getBiometricContext() { + return mBiometricContext; + } + + /** + * Returns lockout mode of this sensor. + */ + @LockoutTracker.LockoutMode + public int getLockoutModeForUser(int userId) { + return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId, + Utils.getCurrentStrength(mSensorProperties.sensorId)); + } + + public void setScheduler(BiometricScheduler scheduler) { + mScheduler = scheduler; + } + + public void setLazySession( + Supplier<AidlSession> lazySession) { + mLazySession = lazySession; + } + + public FingerprintProvider getProvider() { + return mProvider; + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java index a4e602553101..5c5b9928f57a 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java @@ -29,7 +29,8 @@ import com.android.server.biometrics.BiometricsProto; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.ClientMonitorCallback; -import com.android.server.biometrics.sensors.HalClientMonitor; +import com.android.server.biometrics.sensors.StartUserClient; +import com.android.server.biometrics.sensors.fingerprint.aidl.AidlSession; import java.io.File; import java.util.Map; @@ -38,7 +39,8 @@ import java.util.function.Supplier; /** * Sets the HAL's current active user, and updates the framework's authenticatorId cache. */ -public class FingerprintUpdateActiveUserClient extends HalClientMonitor<IBiometricsFingerprint> { +public class FingerprintUpdateActiveUserClient extends + StartUserClient<IBiometricsFingerprint, AidlSession> { private static final String TAG = "FingerprintUpdateActiveUserClient"; private static final String FP_DATA_DIR = "fpdata"; @@ -53,11 +55,24 @@ public class FingerprintUpdateActiveUserClient extends HalClientMonitor<IBiometr @NonNull Supplier<IBiometricsFingerprint> lazyDaemon, int userId, @NonNull String owner, int sensorId, @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, - Supplier<Integer> currentUserId, + @NonNull Supplier<Integer> currentUserId, boolean hasEnrolledBiometrics, @NonNull Map<Integer, Long> authenticatorIds, boolean forceUpdateAuthenticatorId) { - super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner, - 0 /* cookie */, sensorId, logger, biometricContext); + this(context, lazyDaemon, userId, owner, sensorId, logger, biometricContext, currentUserId, + hasEnrolledBiometrics, authenticatorIds, forceUpdateAuthenticatorId, + (newUserId, newUser, halInterfaceVersion) -> {}); + } + + FingerprintUpdateActiveUserClient(@NonNull Context context, + @NonNull Supplier<IBiometricsFingerprint> lazyDaemon, int userId, + @NonNull String owner, int sensorId, + @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, + @NonNull Supplier<Integer> currentUserId, + boolean hasEnrolledBiometrics, @NonNull Map<Integer, Long> authenticatorIds, + boolean forceUpdateAuthenticatorId, + @NonNull UserStartedCallback<AidlSession> userStartedCallback) { + super(context, lazyDaemon, null /* token */, userId, sensorId, logger, biometricContext, + userStartedCallback); mCurrentUserId = currentUserId; mForceUpdateAuthenticatorId = forceUpdateAuthenticatorId; mHasEnrolledBiometrics = hasEnrolledBiometrics; @@ -70,6 +85,7 @@ public class FingerprintUpdateActiveUserClient extends HalClientMonitor<IBiometr if (mCurrentUserId.get() == getTargetUserId() && !mForceUpdateAuthenticatorId) { Slog.d(TAG, "Already user: " + mCurrentUserId + ", returning"); + mUserStartedCallback.onUserStarted(getTargetUserId(), null, 0); callback.onClientFinished(this, true /* success */); return; } @@ -119,6 +135,7 @@ public class FingerprintUpdateActiveUserClient extends HalClientMonitor<IBiometr getFreshDaemon().setActiveGroup(targetId, mDirectory.getAbsolutePath()); mAuthenticatorIds.put(targetId, mHasEnrolledBiometrics ? getFreshDaemon().getAuthenticatorId() : 0L); + mUserStartedCallback.onUserStarted(targetId, null, 0); mCallback.onClientFinished(this, true /* success */); } catch (RemoteException e) { Slog.e(TAG, "Failed to setActiveGroup: " + e); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlCallbackConverter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlCallbackConverter.java index c3e5cbe7daf4..e9a48e79d245 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlCallbackConverter.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlCallbackConverter.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.hardware.biometrics.fingerprint.V2_2.IBiometricsFingerprintClientCallback; import com.android.server.biometrics.HardwareAuthTokenUtils; +import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.fingerprint.aidl.AidlResponseHandler; import java.util.ArrayList; @@ -73,12 +74,12 @@ public class HidlToAidlCallbackConverter extends IBiometricsFingerprintClientCal @Override public void onRemoved(long deviceId, int fingerId, int groupId, int remaining) { - mAidlResponseHandler.onEnrollmentsRemoved(new int[]{fingerId}); + mAidlResponseHandler.onEnrollmentRemoved(fingerId, remaining); } @Override public void onEnumerate(long deviceId, int fingerId, int groupId, int remaining) { - mAidlResponseHandler.onEnrollmentsEnumerated(new int[]{fingerId}); + mAidlResponseHandler.onEnrollmentEnumerated(fingerId, remaining); } void onChallengeGenerated(long challenge) { @@ -92,4 +93,8 @@ public class HidlToAidlCallbackConverter extends IBiometricsFingerprintClientCal void onResetLockout() { mAidlResponseHandler.onLockoutCleared(); } + + <T extends BaseClientMonitor> void unsupportedClientScheduled(Class<T> className) { + mAidlResponseHandler.onUnsupportedClientScheduled(className); + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java new file mode 100644 index 000000000000..0bb6141583d5 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 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. + */ + +package com.android.server.biometrics.sensors.fingerprint.hidl; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.UserInfo; +import android.hardware.biometrics.fingerprint.SensorProps; +import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint; +import android.os.Handler; +import android.os.IHwBinder; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.AuthSessionCoordinator; +import com.android.server.biometrics.sensors.BiometricScheduler; +import com.android.server.biometrics.sensors.ClientMonitorCallback; +import com.android.server.biometrics.sensors.LockoutResetDispatcher; +import com.android.server.biometrics.sensors.LockoutTracker; +import com.android.server.biometrics.sensors.StartUserClient; +import com.android.server.biometrics.sensors.StopUserClient; +import com.android.server.biometrics.sensors.UserAwareBiometricScheduler; +import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils; +import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher; +import com.android.server.biometrics.sensors.fingerprint.aidl.AidlResponseHandler; +import com.android.server.biometrics.sensors.fingerprint.aidl.AidlSession; +import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvider; +import com.android.server.biometrics.sensors.fingerprint.aidl.Sensor; + +import java.util.ArrayList; + +/** + * Convert HIDL sensor configurations to an AIDL Sensor. + */ +public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRecipient { + private static final String TAG = "HidlToAidlSensorAdapter"; + + private final Runnable mInternalCleanupRunnable; + private final LockoutResetDispatcher mLockoutResetDispatcher; + private LockoutFrameworkImpl mLockoutTracker; + private final AuthSessionCoordinator mAuthSessionCoordinator; + private final AidlResponseHandler.AidlResponseHandlerCallback mAidlResponseHandlerCallback; + private int mCurrentUserId = UserHandle.USER_NULL; + private IBiometricsFingerprint mDaemon; + private AidlSession mSession; + + private final StartUserClient.UserStartedCallback<AidlSession> mUserStartedCallback = + (newUserId, newUser, halInterfaceVersion) -> { + if (mCurrentUserId != newUserId) { + handleUserChanged(newUserId); + } + }; + + public HidlToAidlSensorAdapter(@NonNull String tag, @NonNull FingerprintProvider provider, + @NonNull Context context, @NonNull Handler handler, + @NonNull SensorProps prop, + @NonNull LockoutResetDispatcher lockoutResetDispatcher, + @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, + @NonNull BiometricContext biometricContext, + boolean resetLockoutRequiresHardwareAuthToken, + @NonNull Runnable internalCleanupRunnable) { + this(tag, provider, context, handler, prop, lockoutResetDispatcher, + gestureAvailabilityDispatcher, biometricContext, + resetLockoutRequiresHardwareAuthToken, internalCleanupRunnable, + new AuthSessionCoordinator(), null /* daemon */, + null /* onEnrollSuccessCallback */); + } + + @VisibleForTesting + HidlToAidlSensorAdapter(@NonNull String tag, @NonNull FingerprintProvider provider, + @NonNull Context context, @NonNull Handler handler, + @NonNull SensorProps prop, + @NonNull LockoutResetDispatcher lockoutResetDispatcher, + @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, + @NonNull BiometricContext biometricContext, + boolean resetLockoutRequiresHardwareAuthToken, + @NonNull Runnable internalCleanupRunnable, + @NonNull AuthSessionCoordinator authSessionCoordinator, + @Nullable IBiometricsFingerprint daemon, + @Nullable AidlResponseHandler.AidlResponseHandlerCallback aidlResponseHandlerCallback) { + super(tag, provider, context, handler, getFingerprintSensorPropertiesInternal(prop, + new ArrayList<>(), resetLockoutRequiresHardwareAuthToken), + lockoutResetDispatcher, + gestureAvailabilityDispatcher, + biometricContext, null /* session */); + mLockoutResetDispatcher = lockoutResetDispatcher; + mInternalCleanupRunnable = internalCleanupRunnable; + mAuthSessionCoordinator = authSessionCoordinator; + mDaemon = daemon; + mAidlResponseHandlerCallback = aidlResponseHandlerCallback == null + ? new AidlResponseHandler.AidlResponseHandlerCallback() { + @Override + public void onEnrollSuccess() { + getScheduler() + .scheduleClientMonitor(getFingerprintUpdateActiveUserClient( + mCurrentUserId, true /* forceUpdateAuthenticatorIds */)); + } + + @Override + public void onHardwareUnavailable() { + mDaemon = null; + mSession = null; + mCurrentUserId = UserHandle.USER_NULL; + } + } : aidlResponseHandlerCallback; + } + + @Override + public void serviceDied(long cookie) { + Slog.d(TAG, "HAL died."); + mSession = null; + mDaemon = null; + } + + @Override + @LockoutTracker.LockoutMode + public int getLockoutModeForUser(int userId) { + return mLockoutTracker.getLockoutModeForUser(userId); + } + + @Override + public void init(GestureAvailabilityDispatcher gestureAvailabilityDispatcher, + LockoutResetDispatcher lockoutResetDispatcher) { + setLazySession(this::getSession); + setScheduler(new UserAwareBiometricScheduler(TAG, + BiometricScheduler.sensorTypeFromFingerprintProperties(getSensorProperties()), + gestureAvailabilityDispatcher, () -> mCurrentUserId, getUserSwitchCallback())); + mLockoutTracker = new LockoutFrameworkImpl(getContext(), + userId -> mLockoutResetDispatcher.notifyLockoutResetCallbacks( + getSensorProperties().sensorId)); + } + + @Override + @Nullable + protected AidlSession getSessionForUser(int userId) { + if (mSession != null && mSession.getUserId() == userId) { + return mSession; + } else { + return null; + } + } + + @Override + protected boolean isHardwareDetected(String halInstance) { + return getIBiometricsFingerprint() != null; + } + + @NonNull + @Override + protected LockoutTracker getLockoutTracker(boolean forAuth) { + return mLockoutTracker; + } + + private synchronized AidlSession getSession() { + if (mSession != null && mDaemon != null) { + return mSession; + } else { + return mSession = new AidlSession(this::getIBiometricsFingerprint, + mCurrentUserId, getAidlResponseHandler()); + } + } + + private AidlResponseHandler getAidlResponseHandler() { + return new AidlResponseHandler(getContext(), + getScheduler(), + getSensorProperties().sensorId, + mCurrentUserId, + mLockoutTracker, + mLockoutResetDispatcher, + mAuthSessionCoordinator, + () -> {}, mAidlResponseHandlerCallback); + } + + @VisibleForTesting IBiometricsFingerprint getIBiometricsFingerprint() { + if (getProvider().getTestHalEnabled()) { + final TestHal testHal = new TestHal(getContext(), getSensorProperties().sensorId); + testHal.setNotify(new HidlToAidlCallbackConverter(getAidlResponseHandler())); + return testHal; + } + + if (mDaemon != null) { + return mDaemon; + } + + try { + mDaemon = IBiometricsFingerprint.getService(); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to get fingerprint HAL", e); + } catch (java.util.NoSuchElementException e) { + // Service doesn't exist or cannot be opened. + Slog.w(TAG, "NoSuchElementException", e); + } + + if (mDaemon == null) { + Slog.w(TAG, "Fingerprint HAL not available"); + return null; + } + + mDaemon.asBinder().linkToDeath(this, 0 /* flags */); + + Slog.d(TAG, "Fingerprint HAL ready"); + + scheduleLoadAuthenticatorIds(); + mInternalCleanupRunnable.run(); + return mDaemon; + } + + private UserAwareBiometricScheduler.UserSwitchCallback getUserSwitchCallback() { + return new UserAwareBiometricScheduler.UserSwitchCallback() { + @NonNull + @Override + public StopUserClient<?> getStopUserClient(int userId) { + return new StopUserClient<IBiometricsFingerprint>(getContext(), + HidlToAidlSensorAdapter.this::getIBiometricsFingerprint, + null /* token */, userId, getSensorProperties().sensorId, + BiometricLogger.ofUnknown(getContext()), getBiometricContext(), + () -> { + mCurrentUserId = UserHandle.USER_NULL; + mSession = null; + }) { + @Override + public void start(@NonNull ClientMonitorCallback callback) { + super.start(callback); + startHalOperation(); + } + + @Override + protected void startHalOperation() { + onUserStopped(); + } + + @Override + public void unableToStart() { + getCallback().onClientFinished(this, false /* success */); + } + }; + } + + @NonNull + @Override + public StartUserClient<?, ?> getStartUserClient(int newUserId) { + return getFingerprintUpdateActiveUserClient(newUserId, + false /* forceUpdateAuthenticatorId */); + } + }; + } + + private FingerprintUpdateActiveUserClient getFingerprintUpdateActiveUserClient(int newUserId, + boolean forceUpdateAuthenticatorIds) { + return new FingerprintUpdateActiveUserClient(getContext(), + this::getIBiometricsFingerprint, newUserId, TAG, + getSensorProperties().sensorId, BiometricLogger.ofUnknown(getContext()), + getBiometricContext(), () -> mCurrentUserId, + !FingerprintUtils.getInstance(getSensorProperties().sensorId) + .getBiometricsForUser(getContext(), + newUserId).isEmpty(), getAuthenticatorIds(), forceUpdateAuthenticatorIds, + mUserStartedCallback); + } + + private void scheduleLoadAuthenticatorIds() { + getHandler().post(() -> { + for (UserInfo user : UserManager.get(getContext()).getAliveUsers()) { + final int targetUserId = user.id; + if (!getAuthenticatorIds().containsKey(targetUserId)) { + getScheduler().scheduleClientMonitor(getFingerprintUpdateActiveUserClient( + targetUserId, true /* forceUpdateAuthenticatorIds */)); + } + } + }); + } + + @VisibleForTesting void handleUserChanged(int newUserId) { + Slog.d(TAG, "User changed. Current user is " + newUserId); + mSession = null; + mCurrentUserId = newUserId; + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/AidlToHidlAdapter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java index b48d232e65af..2fc00e126354 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/AidlToHidlAdapter.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java @@ -25,20 +25,25 @@ import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint; import android.hardware.keymaster.HardwareAuthToken; import android.os.IBinder; import android.os.RemoteException; +import android.util.Log; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.server.biometrics.HardwareAuthTokenUtils; import com.android.server.biometrics.sensors.fingerprint.UdfpsHelper; import com.android.server.biometrics.sensors.fingerprint.aidl.AidlResponseHandler; +import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintGetAuthenticatorIdClient; +import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintInvalidationClient; import java.util.function.Supplier; /** - * Adapter to convert AIDL-specific interface {@link ISession} methods to HIDL implementation. + * Adapter to convert HIDL methods into AIDL interface {@link ISession}. */ -public class AidlToHidlAdapter implements ISession { - private final String TAG = "AidlToHidlAdapter"; +public class HidlToAidlSessionAdapter implements ISession { + + private final String TAG = "HidlToAidlSessionAdapter"; + @VisibleForTesting static final int ENROLL_TIMEOUT_SEC = 60; @NonNull @@ -46,22 +51,13 @@ public class AidlToHidlAdapter implements ISession { private final int mUserId; private HidlToAidlCallbackConverter mHidlToAidlCallbackConverter; - public AidlToHidlAdapter(Supplier<IBiometricsFingerprint> session, int userId, + public HidlToAidlSessionAdapter(Supplier<IBiometricsFingerprint> session, int userId, AidlResponseHandler aidlResponseHandler) { mSession = session; mUserId = userId; setCallback(aidlResponseHandler); } - private void setCallback(AidlResponseHandler aidlResponseHandler) { - mHidlToAidlCallbackConverter = new HidlToAidlCallbackConverter(aidlResponseHandler); - try { - mSession.get().setNotify(mHidlToAidlCallbackConverter); - } catch (RemoteException e) { - Slog.d(TAG, "Failed to set callback"); - } - } - @Override public IBinder asBinder() { return null; @@ -125,12 +121,16 @@ public class AidlToHidlAdapter implements ISession { @Override public void getAuthenticatorId() throws RemoteException { - //Unsupported in HIDL + Log.e(TAG, "getAuthenticatorId unsupported in HIDL"); + mHidlToAidlCallbackConverter.unsupportedClientScheduled( + FingerprintGetAuthenticatorIdClient.class); } @Override public void invalidateAuthenticatorId() throws RemoteException { - //Unsupported in HIDL + Log.e(TAG, "invalidateAuthenticatorId unsupported in HIDL"); + mHidlToAidlCallbackConverter.unsupportedClientScheduled( + FingerprintInvalidationClient.class); } @Override @@ -140,72 +140,92 @@ public class AidlToHidlAdapter implements ISession { @Override public void close() throws RemoteException { - //Unsupported in HIDL + Log.e(TAG, "close unsupported in HIDL"); } @Override public void onUiReady() throws RemoteException { - //Unsupported in HIDL + Log.e(TAG, "onUiReady unsupported in HIDL"); } @Override public ICancellationSignal authenticateWithContext(long operationId, OperationContext context) throws RemoteException { - //Unsupported in HIDL - return null; + Log.e(TAG, "authenticateWithContext unsupported in HIDL"); + return authenticate(operationId); } @Override public ICancellationSignal enrollWithContext(HardwareAuthToken hat, OperationContext context) throws RemoteException { - //Unsupported in HIDL - return null; + Log.e(TAG, "enrollWithContext unsupported in HIDL"); + return enroll(hat); } @Override public ICancellationSignal detectInteractionWithContext(OperationContext context) throws RemoteException { - //Unsupported in HIDL - return null; + Log.e(TAG, "enrollWithContext unsupported in HIDL"); + return detectInteraction(); } @Override public void onPointerDownWithContext(PointerContext context) throws RemoteException { - //Unsupported in HIDL + Log.e(TAG, "onPointerDownWithContext unsupported in HIDL"); + onPointerDown(context.pointerId, (int) context.x, (int) context.y, context.minor, + context.major); } @Override public void onPointerUpWithContext(PointerContext context) throws RemoteException { - //Unsupported in HIDL + Log.e(TAG, "onPointerUpWithContext unsupported in HIDL"); + onPointerUp(context.pointerId); } @Override public void onContextChanged(OperationContext context) throws RemoteException { - //Unsupported in HIDL + Log.e(TAG, "onContextChanged unsupported in HIDL"); } @Override public void onPointerCancelWithContext(PointerContext context) throws RemoteException { - //Unsupported in HIDL + Log.e(TAG, "onPointerCancelWithContext unsupported in HIDL"); } @Override public void setIgnoreDisplayTouches(boolean shouldIgnore) throws RemoteException { - //Unsupported in HIDL + Log.e(TAG, "setIgnoreDisplayTouches unsupported in HIDL"); } @Override public int getInterfaceVersion() throws RemoteException { - //Unsupported in HIDL + Log.e(TAG, "getInterfaceVersion unsupported in HIDL"); return 0; } @Override public String getInterfaceHash() throws RemoteException { - //Unsupported in HIDL + Log.e(TAG, "getInterfaceHash unsupported in HIDL"); return null; } + private void setCallback(AidlResponseHandler aidlResponseHandler) { + mHidlToAidlCallbackConverter = new HidlToAidlCallbackConverter(aidlResponseHandler); + try { + if (mSession.get() != null) { + long halId = mSession.get().setNotify(mHidlToAidlCallbackConverter); + Slog.d(TAG, "Fingerprint HAL ready, HAL ID: " + halId); + if (halId == 0) { + Slog.d(TAG, "Unable to set HIDL callback."); + } + } else { + Slog.e(TAG, "Unable to set HIDL callback. HIDL daemon is null."); + } + } catch (RemoteException e) { + Slog.d(TAG, "Failed to set callback"); + } + } + private class Cancellation extends ICancellationSignal.Stub { Cancellation() {} diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java index 0730c672acd9..2f77275890dd 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java @@ -18,6 +18,7 @@ package com.android.server.biometrics.sensors.fingerprint.hidl; import static android.Manifest.permission.RESET_FINGERPRINT_LOCKOUT; +import android.annotation.NonNull; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; @@ -31,15 +32,18 @@ import android.util.Slog; import android.util.SparseBooleanArray; import android.util.SparseIntArray; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.biometrics.sensors.LockoutTracker; +import java.util.function.Function; + /** * Tracks and enforces biometric lockout for biometric sensors that do not support lockout in the * HAL. */ public class LockoutFrameworkImpl implements LockoutTracker { - private static final String TAG = "LockoutTracker"; + private static final String TAG = "LockoutFrameworkImpl"; private static final String ACTION_LOCKOUT_RESET = "com.android.server.biometrics.sensors.fingerprint.ACTION_LOCKOUT_RESET"; private static final int MAX_FAILED_ATTEMPTS_LOCKOUT_TIMED = 5; @@ -65,22 +69,32 @@ public class LockoutFrameworkImpl implements LockoutTracker { void onLockoutReset(int userId); } - private final Context mContext; private final LockoutResetCallback mLockoutResetCallback; private final SparseBooleanArray mTimedLockoutCleared; private final SparseIntArray mFailedAttempts; private final AlarmManager mAlarmManager; private final LockoutReceiver mLockoutReceiver; private final Handler mHandler; + private final Function<Integer, PendingIntent> mLockoutResetIntent; + + public LockoutFrameworkImpl(@NonNull Context context, + @NonNull LockoutResetCallback lockoutResetCallback) { + this(context, lockoutResetCallback, (userId) -> PendingIntent.getBroadcast(context, userId, + new Intent(ACTION_LOCKOUT_RESET).putExtra(KEY_LOCKOUT_RESET_USER, userId), + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)); + } - public LockoutFrameworkImpl(Context context, LockoutResetCallback lockoutResetCallback) { - mContext = context; + @VisibleForTesting + LockoutFrameworkImpl(@NonNull Context context, + @NonNull LockoutResetCallback lockoutResetCallback, + @NonNull Function<Integer, PendingIntent> lockoutResetIntent) { mLockoutResetCallback = lockoutResetCallback; mTimedLockoutCleared = new SparseBooleanArray(); mFailedAttempts = new SparseIntArray(); mAlarmManager = context.getSystemService(AlarmManager.class); mLockoutReceiver = new LockoutReceiver(); mHandler = new Handler(Looper.getMainLooper()); + mLockoutResetIntent = lockoutResetIntent; context.registerReceiver(mLockoutReceiver, new IntentFilter(ACTION_LOCKOUT_RESET), RESET_FINGERPRINT_LOCKOUT, null /* handler */, Context.RECEIVER_EXPORTED); @@ -129,34 +143,18 @@ public class LockoutFrameworkImpl implements LockoutTracker { return LOCKOUT_NONE; } - /** - * Clears lockout for Fingerprint HIDL HAL - */ @Override - public void setLockoutModeForUser(int userId, int mode) { - mFailedAttempts.put(userId, 0); - mTimedLockoutCleared.put(userId, true); - // If we're asked to reset failed attempts externally (i.e. from Keyguard), - // the alarm might still be pending; remove it. - cancelLockoutResetForUser(userId); - mLockoutResetCallback.onLockoutReset(userId); - } + public void setLockoutModeForUser(int userId, int mode) {} private void cancelLockoutResetForUser(int userId) { - mAlarmManager.cancel(getLockoutResetIntentForUser(userId)); + mAlarmManager.cancel(mLockoutResetIntent.apply(userId)); } private void scheduleLockoutResetForUser(int userId) { mHandler.post(() -> { mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, - SystemClock.elapsedRealtime() + FAIL_LOCKOUT_TIMEOUT_MS, - getLockoutResetIntentForUser(userId)); + SystemClock.elapsedRealtime() + FAIL_LOCKOUT_TIMEOUT_MS, + mLockoutResetIntent.apply(userId)); }); } - - private PendingIntent getLockoutResetIntentForUser(int userId) { - return PendingIntent.getBroadcast(mContext, userId, - new Intent(ACTION_LOCKOUT_RESET).putExtra(KEY_LOCKOUT_RESET_USER, userId), - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - } } diff --git a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java index acd253b38b3d..f2ffd4d3f1d9 100644 --- a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java +++ b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java @@ -35,7 +35,6 @@ import android.util.Slog; import android.util.Spline; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.display.BrightnessSynchronizer; import com.android.internal.display.BrightnessUtils; import com.android.internal.util.Preconditions; import com.android.server.display.utils.Plog; @@ -97,28 +96,17 @@ public abstract class BrightnessMappingStrategy { float[] brightnessLevels = null; float[] luxLevels = null; switch (mode) { - case AUTO_BRIGHTNESS_MODE_DEFAULT: + case AUTO_BRIGHTNESS_MODE_DEFAULT -> { brightnessLevelsNits = displayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(); luxLevels = displayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(); - brightnessLevels = displayDeviceConfig.getAutoBrightnessBrighteningLevels(); - if (brightnessLevels == null || brightnessLevels.length == 0) { - // Load the old configuration in the range [0, 255]. The values need to be - // normalized to the range [0, 1]. - int[] brightnessLevelsInt = resources.getIntArray( - com.android.internal.R.array.config_autoBrightnessLcdBacklightValues); - brightnessLevels = new float[brightnessLevelsInt.length]; - for (int i = 0; i < brightnessLevels.length; i++) { - brightnessLevels[i] = normalizeAbsoluteBrightness(brightnessLevelsInt[i]); - } - } - break; - case AUTO_BRIGHTNESS_MODE_IDLE: + } + case AUTO_BRIGHTNESS_MODE_IDLE -> { brightnessLevelsNits = getFloatArray(resources.obtainTypedArray( com.android.internal.R.array.config_autoBrightnessDisplayValuesNitsIdle)); luxLevels = getLuxLevels(resources.getIntArray( com.android.internal.R.array.config_autoBrightnessLevelsIdle)); - break; + } } // Display independent, mode independent values @@ -426,11 +414,6 @@ public abstract class BrightnessMappingStrategy { } } - // Normalize entire brightness range to 0 - 1. - protected static float normalizeAbsoluteBrightness(int brightness) { - return BrightnessSynchronizer.brightnessIntToFloat(brightness); - } - private Pair<float[], float[]> insertControlPoint( float[] luxLevels, float[] brightnessLevels, float lux, float brightness) { final int idx = findInsertionPoint(luxLevels, lux); diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index d97127c91fbf..7d22a87065b4 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -49,6 +49,7 @@ import com.android.server.display.config.BrightnessThresholds; import com.android.server.display.config.BrightnessThrottlingMap; import com.android.server.display.config.BrightnessThrottlingPoint; import com.android.server.display.config.Density; +import com.android.server.display.config.DisplayBrightnessMappingConfig; import com.android.server.display.config.DisplayBrightnessPoint; import com.android.server.display.config.DisplayConfiguration; import com.android.server.display.config.DisplayQuirks; @@ -57,7 +58,6 @@ import com.android.server.display.config.HdrBrightnessData; import com.android.server.display.config.HighBrightnessMode; import com.android.server.display.config.IntegerArray; import com.android.server.display.config.LuxThrottling; -import com.android.server.display.config.LuxToBrightnessMapping; import com.android.server.display.config.NitsMap; import com.android.server.display.config.NonNegativeFloatToFloatPoint; import com.android.server.display.config.Point; @@ -313,6 +313,21 @@ import javax.xml.datatype.DatatypeConfigurationException; * 1000 * </darkeningLightDebounceIdleMillis> * <luxToBrightnessMapping> + * <mode>default</mode> + * <map> + * <point> + * <first>0</first> + * <second>0.2</second> + * </point> + * <point> + * <first>80</first> + * <second>0.3</second> + * </point> + * </map> + * </luxToBrightnessMapping> + * <luxToBrightnessMapping> + * <mode>doze</mode> + * <setting>dim</setting> * <map> * <point> * <first>0</first> @@ -634,36 +649,8 @@ public class DisplayDeviceConfig { // for the corresponding values above private float[] mBrightness; - /** - * Array of desired screen brightness in nits corresponding to the lux values - * in the mBrightnessLevelsLux array. The display brightness is defined as the - * measured brightness of an all-white image. The brightness values must be non-negative and - * non-decreasing. This must be overridden in platform specific overlays - */ - private float[] mBrightnessLevelsNits; - - /** - * Array of desired screen brightness corresponding to the lux values - * in the mBrightnessLevelsLux array. The brightness values must be non-negative and - * non-decreasing. They must be between {@link PowerManager.BRIGHTNESS_MIN} and - * {@link PowerManager.BRIGHTNESS_MAX}. This must be overridden in platform specific overlays - */ - private float[] mBrightnessLevels; - - /** - * Array of light sensor lux values to define our levels for auto-brightness support. - * - * The first lux value is always 0. - * - * The control points must be strictly increasing. Each control point corresponds to an entry - * in the brightness values arrays. For example, if lux == luxLevels[1] (second element - * of the levels array) then the brightness will be determined by brightnessLevels[1] (second - * element of the brightness values array). - * - * Spline interpolation is used to determine the auto-brightness values for lux levels between - * these control points. - */ - private float[] mBrightnessLevelsLux; + @Nullable + private DisplayBrightnessMappingConfig mDisplayBrightnessMapping; private float mBacklightMinimum = Float.NaN; private float mBacklightMaximum = Float.NaN; @@ -1604,24 +1591,57 @@ public class DisplayDeviceConfig { } /** - * @return Auto brightness brightening ambient lux levels + * @return The default auto-brightness brightening ambient lux levels */ public float[] getAutoBrightnessBrighteningLevelsLux() { - return mBrightnessLevelsLux; + if (mDisplayBrightnessMapping == null) { + return null; + } + return mDisplayBrightnessMapping.getLuxArray(); + } + + /** + * @param mode The auto-brightness mode + * @param setting The brightness setting + * @return Auto brightness brightening ambient lux levels for the specified mode and setting + */ + public float[] getAutoBrightnessBrighteningLevelsLux(String mode, String setting) { + if (mDisplayBrightnessMapping == null) { + return null; + } + return mDisplayBrightnessMapping.getLuxArray(mode, setting); } /** * @return Auto brightness brightening nits levels */ public float[] getAutoBrightnessBrighteningLevelsNits() { - return mBrightnessLevelsNits; + if (mDisplayBrightnessMapping == null) { + return null; + } + return mDisplayBrightnessMapping.getNitsArray(); } /** - * @return Auto brightness brightening levels + * @return The default auto-brightness brightening levels */ public float[] getAutoBrightnessBrighteningLevels() { - return mBrightnessLevels; + if (mDisplayBrightnessMapping == null) { + return null; + } + return mDisplayBrightnessMapping.getBrightnessArray(); + } + + /** + * @param mode The auto-brightness mode + * @param setting The brightness setting + * @return Auto brightness brightening backlight levels for the specified mode and setting + */ + public float[] getAutoBrightnessBrighteningLevels(String mode, String setting) { + if (mDisplayBrightnessMapping == null) { + return null; + } + return mDisplayBrightnessMapping.getBrightnessArray(mode, setting); } /** @@ -1875,9 +1895,7 @@ public class DisplayDeviceConfig { + mAutoBrightnessBrighteningLightDebounceIdle + ", mAutoBrightnessDarkeningLightDebounceIdle= " + mAutoBrightnessDarkeningLightDebounceIdle - + ", mBrightnessLevelsLux= " + Arrays.toString(mBrightnessLevelsLux) - + ", mBrightnessLevelsNits= " + Arrays.toString(mBrightnessLevelsNits) - + ", mBrightnessLevels= " + Arrays.toString(mBrightnessLevels) + + ", mDisplayBrightnessMapping= " + mDisplayBrightnessMapping + ", mDdcAutoBrightnessAvailable= " + mDdcAutoBrightnessAvailable + ", mAutoBrightnessAvailable= " + mAutoBrightnessAvailable + "\n" @@ -2568,7 +2586,8 @@ public class DisplayDeviceConfig { // Idle must be called after interactive, since we fall back to it if needed. loadAutoBrightnessBrighteningLightDebounceIdle(autoBrightness); loadAutoBrightnessDarkeningLightDebounceIdle(autoBrightness); - loadAutoBrightnessDisplayBrightnessMapping(autoBrightness); + mDisplayBrightnessMapping = new DisplayBrightnessMappingConfig(mContext, mFlags, + autoBrightness, mBacklightToBrightnessSpline); loadEnableAutoBrightness(autoBrightness); } @@ -2633,38 +2652,6 @@ public class DisplayDeviceConfig { } } - /** - * Loads the auto-brightness display brightness mappings. Internally, this takes care of - * loading the value from the display config, and if not present, falls back to config.xml. - */ - private void loadAutoBrightnessDisplayBrightnessMapping(AutoBrightness autoBrightnessConfig) { - if (mFlags.areAutoBrightnessModesEnabled() && autoBrightnessConfig != null - && autoBrightnessConfig.getLuxToBrightnessMapping() != null) { - LuxToBrightnessMapping mapping = autoBrightnessConfig.getLuxToBrightnessMapping(); - final int size = mapping.getMap().getPoint().size(); - mBrightnessLevels = new float[size]; - mBrightnessLevelsLux = new float[size]; - for (int i = 0; i < size; i++) { - float backlight = mapping.getMap().getPoint().get(i).getSecond().floatValue(); - mBrightnessLevels[i] = mBacklightToBrightnessSpline.interpolate(backlight); - mBrightnessLevelsLux[i] = mapping.getMap().getPoint().get(i).getFirst() - .floatValue(); - } - if (size > 0 && mBrightnessLevelsLux[0] != 0) { - throw new IllegalArgumentException( - "The first lux value in the display brightness mapping must be 0"); - } - } else { - mBrightnessLevelsNits = getFloatArray(mContext.getResources() - .obtainTypedArray(com.android.internal.R.array - .config_autoBrightnessDisplayValuesNits), PowerManager - .BRIGHTNESS_OFF_FLOAT); - mBrightnessLevelsLux = getLuxLevels(mContext.getResources() - .getIntArray(com.android.internal.R.array - .config_autoBrightnessLevels)); - } - } - private void loadAutoBrightnessAvailableFromConfigXml() { mAutoBrightnessAvailable = mContext.getResources().getBoolean( R.bool.config_automatic_brightness_available); @@ -2977,7 +2964,8 @@ public class DisplayDeviceConfig { } private void loadAutoBrightnessConfigsFromConfigXml() { - loadAutoBrightnessDisplayBrightnessMapping(null /*AutoBrightnessConfig*/); + mDisplayBrightnessMapping = new DisplayBrightnessMappingConfig(mContext, mFlags, + /* autoBrightnessConfig= */ null, mBacklightToBrightnessSpline); } private void loadBrightnessChangeThresholdsFromXml() { @@ -3347,7 +3335,12 @@ public class DisplayDeviceConfig { return vals; } - private static float[] getLuxLevels(int[] lux) { + /** + * @param lux The lux array + * @return The lux array with 0 appended at the beginning - the first lux value should always + * be 0 + */ + public static float[] getLuxLevels(int[] lux) { // The first control point is implicit and always at 0 lux. float[] levels = new float[lux.length + 1]; for (int i = 0; i < lux.length; i++) { diff --git a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java index 1bd556bdcc4f..4e341a9c19b4 100644 --- a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java +++ b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java @@ -56,6 +56,15 @@ public class DisplayOffloadSessionImpl implements DisplayManagerInternal.Display } } + @Override + public boolean blockScreenOn(Runnable unblocker) { + if (mDisplayOffloader == null) { + return false; + } + mDisplayOffloader.onBlockingScreenOn(unblocker); + return true; + } + /** * Start the offload session. The method returns if the session is already active. * @return Whether the session was started successfully diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java index 52c53f3d658e..6d09cc9d37ba 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController2.java +++ b/services/core/java/com/android/server/display/DisplayPowerController2.java @@ -126,6 +126,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal // To enable these logs, run: // 'adb shell setprop persist.log.tag.DisplayPowerController2 DEBUG && adb reboot' private static final boolean DEBUG = DebugUtils.isDebuggable(TAG); + private static final String SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME = + "Screen on blocked by displayoffload"; // If true, uses the color fade on animation. // We might want to turn this off if we cannot get a guarantee that the screen @@ -155,6 +157,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal private static final int MSG_SET_DWBC_COLOR_OVERRIDE = 15; private static final int MSG_SET_DWBC_LOGGING_ENABLED = 16; private static final int MSG_SET_BRIGHTNESS_FROM_OFFLOAD = 17; + private static final int MSG_OFFLOADING_SCREEN_ON_UNBLOCKED = 18; @@ -339,6 +342,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal // we are waiting for a callback to release it and unblock the screen. private ScreenOnUnblocker mPendingScreenOnUnblocker; private ScreenOffUnblocker mPendingScreenOffUnblocker; + private Runnable mPendingScreenOnUnblockerByDisplayOffload; // True if we were in the process of turning off the screen. // This allows us to recover more gracefully from situations where we abort @@ -348,10 +352,15 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal // The elapsed real time when the screen on was blocked. private long mScreenOnBlockStartRealTime; private long mScreenOffBlockStartRealTime; + private long mScreenOnBlockByDisplayOffloadStartRealTime; // Screen state we reported to policy. Must be one of REPORTED_TO_POLICY_* fields. private int mReportedScreenStateToPolicy = REPORTED_TO_POLICY_UNREPORTED; + // Used to deduplicate the displayoffload blocking screen on logic. One block per turning on. + // This value is reset when screen on is reported or the blocking is cancelled. + private boolean mScreenTurningOnWasBlockedByDisplayOffload; + // If the last recorded screen state was dozing or not. private boolean mDozing; @@ -472,7 +481,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal private boolean mBootCompleted; private final DisplayManagerFlags mFlags; - private DisplayManagerInternal.DisplayOffloadSession mDisplayOffloadSession; + private DisplayOffloadSession mDisplayOffloadSession; /** * Creates the display power controller. @@ -772,6 +781,10 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal @Override public void setDisplayOffloadSession(DisplayOffloadSession session) { + if (session == mDisplayOffloadSession) { + return; + } + unblockScreenOnByDisplayOffload(); mDisplayOffloadSession = session; } @@ -1735,6 +1748,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal // reporting the display is ready because we only need to ensure the screen is in the // right power state even as it continues to converge on the desired brightness. final boolean ready = mPendingScreenOnUnblocker == null + && mPendingScreenOnUnblockerByDisplayOffload == null && (!mColorFadeEnabled || (!mColorFadeOnAnimator.isStarted() && !mColorFadeOffAnimator.isStarted())) && mPowerState.waitUntilClean(mCleanListener); @@ -1983,15 +1997,69 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal } } + private void blockScreenOnByDisplayOffload(DisplayOffloadSession displayOffloadSession) { + if (mPendingScreenOnUnblockerByDisplayOffload != null || displayOffloadSession == null) { + return; + } + mScreenTurningOnWasBlockedByDisplayOffload = true; + + Trace.asyncTraceBegin( + Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME, 0); + mScreenOnBlockByDisplayOffloadStartRealTime = SystemClock.elapsedRealtime(); + + mPendingScreenOnUnblockerByDisplayOffload = + () -> onDisplayOffloadUnblockScreenOn(displayOffloadSession); + if (!displayOffloadSession.blockScreenOn(mPendingScreenOnUnblockerByDisplayOffload)) { + mPendingScreenOnUnblockerByDisplayOffload = null; + long delay = + SystemClock.elapsedRealtime() - mScreenOnBlockByDisplayOffloadStartRealTime; + Slog.w(mTag, "Tried blocking screen on for offloading but failed. So, end trace after " + + delay + " ms."); + Trace.asyncTraceEnd( + Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME, 0); + return; + } + Slog.i(mTag, "Blocking screen on for offloading."); + } + + private void onDisplayOffloadUnblockScreenOn(DisplayOffloadSession displayOffloadSession) { + Message msg = mHandler.obtainMessage(MSG_OFFLOADING_SCREEN_ON_UNBLOCKED, + displayOffloadSession); + mHandler.sendMessage(msg); + } + + private void unblockScreenOnByDisplayOffload() { + if (mPendingScreenOnUnblockerByDisplayOffload == null) { + return; + } + mPendingScreenOnUnblockerByDisplayOffload = null; + long delay = SystemClock.elapsedRealtime() - mScreenOnBlockByDisplayOffloadStartRealTime; + Slog.i(mTag, "Unblocked screen on for offloading after " + delay + " ms"); + Trace.asyncTraceEnd( + Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME, 0); + } + private boolean setScreenState(int state) { return setScreenState(state, false /*reportOnly*/); } private boolean setScreenState(int state, boolean reportOnly) { final boolean isOff = (state == Display.STATE_OFF); + final boolean isOn = (state == Display.STATE_ON); + final boolean changed = mPowerState.getScreenState() != state; - if (mPowerState.getScreenState() != state - || mReportedScreenStateToPolicy == REPORTED_TO_POLICY_UNREPORTED) { + // If the screen is turning on, give displayoffload a chance to do something before the + // screen actually turns on. + // TODO(b/316941732): add tests for this displayoffload screen-on blocker. + if (isOn && changed && !mScreenTurningOnWasBlockedByDisplayOffload) { + blockScreenOnByDisplayOffload(mDisplayOffloadSession); + } else if (!isOn && mScreenTurningOnWasBlockedByDisplayOffload) { + // No longer turning screen on, so unblock previous screen on blocking immediately. + unblockScreenOnByDisplayOffload(); + mScreenTurningOnWasBlockedByDisplayOffload = false; + } + + if (changed || mReportedScreenStateToPolicy == REPORTED_TO_POLICY_UNREPORTED) { // If we are trying to turn screen off, give policy a chance to do something before we // actually turn the screen off. if (isOff && !mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()) { @@ -2007,8 +2075,9 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal } } - if (!reportOnly && mPowerState.getScreenState() != state - && readyToUpdateDisplayState()) { + if (!reportOnly && changed && readyToUpdateDisplayState() + && mPendingScreenOffUnblocker == null + && mPendingScreenOnUnblockerByDisplayOffload == null) { Trace.traceCounter(Trace.TRACE_TAG_POWER, "ScreenState", state); String propertyKey = "debug.tracing.screen_state"; @@ -2060,12 +2129,16 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal } // Return true if the screen isn't blocked. - return mPendingScreenOnUnblocker == null; + return mPendingScreenOnUnblocker == null + && mPendingScreenOnUnblockerByDisplayOffload == null; } private void setReportedScreenState(int state) { Trace.traceCounter(Trace.TRACE_TAG_POWER, "ReportedScreenStateToPolicy", state); mReportedScreenStateToPolicy = state; + if (state == REPORTED_TO_POLICY_SCREEN_ON) { + mScreenTurningOnWasBlockedByDisplayOffload = false; + } } private void loadAmbientLightSensor() { @@ -2813,6 +2886,12 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal updatePowerState(); } break; + case MSG_OFFLOADING_SCREEN_ON_UNBLOCKED: + if (mDisplayOffloadSession == msg.obj) { + unblockScreenOnByDisplayOffload(); + updatePowerState(); + } + break; case MSG_CONFIGURE_BRIGHTNESS: BrightnessConfiguration brightnessConfiguration = (BrightnessConfiguration) msg.obj; diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index 22898a65c5de..25576ce9efd6 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -25,6 +25,7 @@ import android.content.Context; import android.content.res.Resources; import android.hardware.display.DisplayManagerInternal.DisplayOffloadSession; import android.hardware.sidekick.SidekickInternal; +import android.media.MediaDrm; import android.os.Build; import android.os.Handler; import android.os.IBinder; @@ -242,6 +243,10 @@ final class LocalDisplayAdapter extends DisplayAdapter { private SurfaceControl.DisplayMode mActiveSfDisplayMode; // The active display vsync period in SurfaceFlinger private float mActiveRenderFrameRate; + // The current HDCP level supported by the display, 0 indicates unset + // values are defined in hardware/interfaces/drm/aidl/android/hardware/drm/HdcpLevel.aidl + private int mConnectedHdcpLevel; + private DisplayEventReceiver.FrameRateOverride[] mFrameRateOverrides = new DisplayEventReceiver.FrameRateOverride[0]; @@ -675,8 +680,9 @@ final class LocalDisplayAdapter extends DisplayAdapter { mInfo.yDpi = mActiveSfDisplayMode.yDpi; mInfo.deviceProductInfo = mStaticDisplayInfo.deviceProductInfo; - // Assume that all built-in displays that have secure output (eg. HDCP) also - // support compositing from gralloc protected buffers. + if (mConnectedHdcpLevel != 0) { + mStaticDisplayInfo.secure = mConnectedHdcpLevel >= MediaDrm.HDCP_V1; + } if (mStaticDisplayInfo.secure) { mInfo.flags = DisplayDeviceInfo.FLAG_SECURE | DisplayDeviceInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS; @@ -1093,6 +1099,12 @@ final class LocalDisplayAdapter extends DisplayAdapter { } } + public void onHdcpLevelsChangedLocked(int connectedLevel, int maxLevel) { + if (updateHdcpLevelsLocked(connectedLevel, maxLevel)) { + updateDeviceInfoLocked(); + } + } + public boolean updateActiveModeLocked(int activeSfModeId, float renderFrameRate) { if (mActiveSfDisplayMode.id == activeSfModeId && mActiveRenderFrameRate == renderFrameRate) { @@ -1118,6 +1130,22 @@ final class LocalDisplayAdapter extends DisplayAdapter { return true; } + public boolean updateHdcpLevelsLocked(int connectedLevel, int maxLevel) { + if (connectedLevel > maxLevel) { + Slog.w(TAG, "HDCP connected level: " + connectedLevel + + " is larger than max level: " + maxLevel + + ", ignoring request."); + return false; + } + + if (mConnectedHdcpLevel == connectedLevel) { + return false; + } + + mConnectedHdcpLevel = connectedLevel; + return true; + } + public void requestColorModeLocked(int colorMode) { if (mActiveColorMode == colorMode) { return; @@ -1387,6 +1415,7 @@ final class LocalDisplayAdapter extends DisplayAdapter { long renderPeriod); void onFrameRateOverridesChanged(long timestampNanos, long physicalDisplayId, DisplayEventReceiver.FrameRateOverride[] overrides); + void onHdcpLevelsChanged(long physicalDisplayId, int connectedLevel, int maxLevel); } @@ -1420,6 +1449,11 @@ final class LocalDisplayAdapter extends DisplayAdapter { DisplayEventReceiver.FrameRateOverride[] overrides) { mListener.onFrameRateOverridesChanged(timestampNanos, physicalDisplayId, overrides); } + + @Override + public void onHdcpLevelsChanged(long physicalDisplayId, int connectedLevel, int maxLevel) { + mListener.onHdcpLevelsChanged(physicalDisplayId, connectedLevel, maxLevel); + } } private final class LocalDisplayEventListener implements DisplayEventListener { @@ -1489,6 +1523,26 @@ final class LocalDisplayAdapter extends DisplayAdapter { device.onFrameRateOverridesChanged(overrides); } } + + @Override + public void onHdcpLevelsChanged(long physicalDisplayId, int connectedLevel, int maxLevel) { + if (DEBUG) { + Slog.d(TAG, "onHdcpLevelsChanged(physicalDisplayId=" + physicalDisplayId + + ", connectedLevel=" + connectedLevel + ", maxLevel=" + maxLevel + ")"); + } + synchronized (getSyncRoot()) { + LocalDisplayDevice device = mDevices.get(physicalDisplayId); + if (device == null) { + if (DEBUG) { + Slog.d(TAG, "Received hdcp levels change for unhandled physical display: " + + "physicalDisplayId=" + physicalDisplayId); + } + return; + } + + device.onHdcpLevelsChangedLocked(connectedLevel, maxLevel); + } + } } @VisibleForTesting diff --git a/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java b/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java new file mode 100644 index 000000000000..21628503adcf --- /dev/null +++ b/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 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. + */ + +package com.android.server.display.config; + +import android.content.Context; +import android.os.PowerManager; +import android.util.Spline; + +import com.android.internal.display.BrightnessSynchronizer; +import com.android.server.display.DisplayDeviceConfig; +import com.android.server.display.feature.DisplayManagerFlags; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Provides a mapping between lux and brightness values in order to support auto-brightness. + */ +public class DisplayBrightnessMappingConfig { + + private static final String DEFAULT_BRIGHTNESS_MAPPING_KEY = "default_normal"; + + /** + * Array of desired screen brightness in nits corresponding to the lux values + * in the mBrightnessLevelsLuxMap.get(DEFAULT_ID) array. The display brightness is defined as + * the measured brightness of an all-white image. The brightness values must be non-negative and + * non-decreasing. This must be overridden in platform specific overlays + */ + private float[] mBrightnessLevelsNits; + + /** + * Map of arrays of desired screen brightness corresponding to the lux values + * in mBrightnessLevelsLuxMap, indexed by the auto-brightness mode and the brightness setting. + * The brightness values must be non-negative and non-decreasing. They must be between + * {@link PowerManager.BRIGHTNESS_MIN} and {@link PowerManager.BRIGHTNESS_MAX}. + * + * The keys are a concatenation of the auto-brightness mode and the brightness setting + * separated by an underscore, e.g. default_normal, default_dim, default_bright, doze_normal, + * doze_dim, doze_bright. + */ + private final Map<String, float[]> mBrightnessLevelsMap = new HashMap<>(); + + /** + * Map of arrays of light sensor lux values to define our levels for auto-brightness support, + * indexed by the auto-brightness mode and the brightness setting. + * + * The first lux value in every array is always 0. + * + * The control points must be strictly increasing. Each control point corresponds to an entry + * in the brightness values arrays. For example, if lux == luxLevels[1] (second element + * of the levels array) then the brightness will be determined by brightnessLevels[1] (second + * element of the brightness values array). + * + * Spline interpolation is used to determine the auto-brightness values for lux levels between + * these control points. + * + * The keys are a concatenation of the auto-brightness mode and the brightness setting + * separated by an underscore, e.g. default_normal, default_dim, default_bright, doze_normal, + * doze_dim, doze_bright. + */ + private final Map<String, float[]> mBrightnessLevelsLuxMap = new HashMap<>(); + + /** + * Loads the auto-brightness display brightness mappings. Internally, this takes care of + * loading the value from the display config, and if not present, falls back to config.xml. + */ + public DisplayBrightnessMappingConfig(Context context, DisplayManagerFlags flags, + AutoBrightness autoBrightnessConfig, Spline backlightToBrightnessSpline) { + if (flags.areAutoBrightnessModesEnabled() && autoBrightnessConfig != null + && autoBrightnessConfig.getLuxToBrightnessMapping() != null + && autoBrightnessConfig.getLuxToBrightnessMapping().size() > 0) { + for (LuxToBrightnessMapping mapping + : autoBrightnessConfig.getLuxToBrightnessMapping()) { + final int size = mapping.getMap().getPoint().size(); + float[] brightnessLevels = new float[size]; + float[] brightnessLevelsLux = new float[size]; + for (int i = 0; i < size; i++) { + float backlight = mapping.getMap().getPoint().get(i).getSecond().floatValue(); + brightnessLevels[i] = backlightToBrightnessSpline.interpolate(backlight); + brightnessLevelsLux[i] = mapping.getMap().getPoint().get(i).getFirst() + .floatValue(); + } + if (size == 0) { + throw new IllegalArgumentException( + "A display brightness mapping should not be empty"); + } + if (brightnessLevelsLux[0] != 0) { + throw new IllegalArgumentException( + "The first lux value in the display brightness mapping must be 0"); + } + + String key = (mapping.getMode() == null ? "default" : mapping.getMode()) + "_" + + (mapping.getSetting() == null ? "normal" : mapping.getSetting()); + if (mBrightnessLevelsMap.containsKey(key) + || mBrightnessLevelsLuxMap.containsKey(key)) { + throw new IllegalArgumentException( + "A display brightness mapping with key " + key + " already exists"); + } + mBrightnessLevelsMap.put(key, brightnessLevels); + mBrightnessLevelsLuxMap.put(key, brightnessLevelsLux); + } + } + + if (!mBrightnessLevelsMap.containsKey(DEFAULT_BRIGHTNESS_MAPPING_KEY) + || !mBrightnessLevelsLuxMap.containsKey(DEFAULT_BRIGHTNESS_MAPPING_KEY)) { + mBrightnessLevelsNits = DisplayDeviceConfig.getFloatArray(context.getResources() + .obtainTypedArray(com.android.internal.R.array + .config_autoBrightnessDisplayValuesNits), PowerManager + .BRIGHTNESS_OFF_FLOAT); + + float[] brightnessLevelsLux = DisplayDeviceConfig.getLuxLevels(context.getResources() + .getIntArray(com.android.internal.R.array + .config_autoBrightnessLevels)); + mBrightnessLevelsLuxMap.put(DEFAULT_BRIGHTNESS_MAPPING_KEY, brightnessLevelsLux); + + // Load the old configuration in the range [0, 255]. The values need to be normalized + // to the range [0, 1]. + int[] brightnessLevels = context.getResources().getIntArray( + com.android.internal.R.array.config_autoBrightnessLcdBacklightValues); + mBrightnessLevelsMap.put(DEFAULT_BRIGHTNESS_MAPPING_KEY, + brightnessArrayIntToFloat(brightnessLevels, backlightToBrightnessSpline)); + } + } + + /** + * @return The default auto-brightness brightening ambient lux levels + */ + public float[] getLuxArray() { + return mBrightnessLevelsLuxMap.get(DEFAULT_BRIGHTNESS_MAPPING_KEY); + } + + /** + * @param mode The auto-brightness mode + * @param setting The brightness setting + * @return Auto brightness brightening ambient lux levels for the specified mode and setting + */ + public float[] getLuxArray(String mode, String setting) { + return mBrightnessLevelsLuxMap.get(mode + "_" + setting); + } + + /** + * @return Auto brightness brightening nits levels + */ + public float[] getNitsArray() { + return mBrightnessLevelsNits; + } + + /** + * @return The default auto-brightness brightening levels + */ + public float[] getBrightnessArray() { + return mBrightnessLevelsMap.get(DEFAULT_BRIGHTNESS_MAPPING_KEY); + } + + /** + * @param mode The auto-brightness mode + * @param setting The brightness setting + * @return Auto brightness brightening ambient lux levels for the specified mode and setting + */ + public float[] getBrightnessArray(String mode, String setting) { + return mBrightnessLevelsMap.get(mode + "_" + setting); + } + + @Override + public String toString() { + StringBuilder brightnessLevelsLuxMapString = new StringBuilder("{"); + for (Map.Entry<String, float[]> entry : mBrightnessLevelsLuxMap.entrySet()) { + brightnessLevelsLuxMapString.append(entry.getKey()).append("=").append( + Arrays.toString(entry.getValue())).append(", "); + } + if (brightnessLevelsLuxMapString.length() > 2) { + brightnessLevelsLuxMapString.delete(brightnessLevelsLuxMapString.length() - 2, + brightnessLevelsLuxMapString.length()); + } + brightnessLevelsLuxMapString.append("}"); + + StringBuilder brightnessLevelsMapString = new StringBuilder("{"); + for (Map.Entry<String, float[]> entry : mBrightnessLevelsMap.entrySet()) { + brightnessLevelsMapString.append(entry.getKey()).append("=").append( + Arrays.toString(entry.getValue())).append(", "); + } + if (brightnessLevelsMapString.length() > 2) { + brightnessLevelsMapString.delete(brightnessLevelsMapString.length() - 2, + brightnessLevelsMapString.length()); + } + brightnessLevelsMapString.append("}"); + + return "mBrightnessLevelsNits= " + Arrays.toString(mBrightnessLevelsNits) + + ", mBrightnessLevelsLuxMap= " + brightnessLevelsLuxMapString + + ", mBrightnessLevelsMap= " + brightnessLevelsMapString; + } + + private float[] brightnessArrayIntToFloat(int[] brightnessInt, + Spline backlightToBrightnessSpline) { + float[] brightnessFloat = new float[brightnessInt.length]; + for (int i = 0; i < brightnessInt.length; i++) { + brightnessFloat[i] = backlightToBrightnessSpline.interpolate( + BrightnessSynchronizer.brightnessIntToFloat(brightnessInt[i])); + } + return brightnessFloat; + } +} diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java index 4089a81dfc20..dda50cab2cdd 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java @@ -167,13 +167,17 @@ public abstract class InputMethodManagerInternal { /** * Indicates that the IME window has re-parented to the new target when the IME control changed. + * + * @param displayId the display hosting the IME window */ - public abstract void onImeParentChanged(); + public abstract void onImeParentChanged(int displayId); /** - * Destroys the IME surface. + * Destroys the IME surface for the given display. + * + * @param displayId the display hosting the IME window */ - public abstract void removeImeSurface(); + public abstract void removeImeSurface(int displayId); /** * Updates the IME visibility, back disposition and show IME picker status for SystemUI. @@ -298,11 +302,11 @@ public abstract class InputMethodManagerInternal { } @Override - public void onImeParentChanged() { + public void onImeParentChanged(int displayId) { } @Override - public void removeImeSurface() { + public void removeImeSurface(int displayId) { } @Override diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 16e043cfb64d..0d29b7dca8d4 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -5671,7 +5671,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @Override - public void onImeParentChanged() { + public void onImeParentChanged(int displayId) { synchronized (ImfLock.class) { // Hide the IME method menu only when the IME surface parent is changed by the // input target changed, in case seeing the dialog dismiss flickering during @@ -5683,7 +5683,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @Override - public void removeImeSurface() { + public void removeImeSurface(int displayId) { mHandler.obtainMessage(MSG_REMOVE_IME_SURFACE).sendToTarget(); } diff --git a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java index 78c8cdee7845..403b421639cb 100644 --- a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java +++ b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java @@ -19,6 +19,7 @@ package com.android.server.location.gnss; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED; import android.content.Context; +import android.location.flags.Flags; import android.net.ConnectivityManager; import android.net.LinkAddress; import android.net.LinkProperties; @@ -48,6 +49,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; /** * Handles network connection requests and network state change updates for AGPS data download. @@ -91,6 +93,10 @@ class GnssNetworkConnectivityHandler { // network with SUPL connectivity or report an error. private static final int SUPL_NETWORK_REQUEST_TIMEOUT_MILLIS = 20 * 1000; + // If the chipset does not request to release a SUPL connection before the specified timeout in + // milliseconds, the connection will be automatically released. + private static final long SUPL_CONNECTION_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(1); + private static final int HASH_MAP_INITIAL_CAPACITY_TO_TRACK_CONNECTED_NETWORKS = 5; // Keeps track of networks and their state as notified by the network request callbacks. @@ -121,6 +127,8 @@ class GnssNetworkConnectivityHandler { private static final long WAKELOCK_TIMEOUT_MILLIS = 60 * 1000; private final PowerManager.WakeLock mWakeLock; + private final Object mSuplConnectionReleaseOnTimeoutToken = new Object(); + /** * Network attributes needed when updating HAL about network connectivity status changes. */ @@ -609,6 +617,13 @@ class GnssNetworkConnectivityHandler { mSuplConnectivityCallback, mHandler, SUPL_NETWORK_REQUEST_TIMEOUT_MILLIS); + if (Flags.releaseSuplConnectionOnTimeout()) { + // Schedule to release the SUPL connection after timeout + mHandler.removeCallbacksAndMessages(mSuplConnectionReleaseOnTimeoutToken); + mHandler.postDelayed(() -> handleReleaseSuplConnection(GPS_RELEASE_AGPS_DATA_CONN), + mSuplConnectionReleaseOnTimeoutToken, + SUPL_CONNECTION_TIMEOUT_MILLIS); + } } catch (RuntimeException e) { Log.e(TAG, "Failed to request network.", e); mSuplConnectivityCallback = null; @@ -639,6 +654,10 @@ class GnssNetworkConnectivityHandler { Log.d(TAG, message); } + if (Flags.releaseSuplConnectionOnTimeout()) { + // Remove pending task to avoid releasing an incorrect connection + mHandler.removeCallbacksAndMessages(mSuplConnectionReleaseOnTimeoutToken); + } if (mAGpsDataConnectionState == AGPS_DATA_CONNECTION_CLOSED) { return; } diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java index 2da1a689f69f..66e61c076030 100644 --- a/services/core/java/com/android/server/notification/ConditionProviders.java +++ b/services/core/java/com/android/server/notification/ConditionProviders.java @@ -234,7 +234,7 @@ public class ConditionProviders extends ManagedServices { if (pkgList != null && (pkgList.length > 0)) { for (String pkgName : pkgList) { try { - inm.removeAutomaticZenRules(pkgName); + inm.removeAutomaticZenRules(pkgName, /* fromUser= */ false); inm.setNotificationPolicyAccessGranted(pkgName, false); } catch (Exception e) { Slog.e(TAG, "Failed to clean up rules for " + pkgName, e); diff --git a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java index 885566693b9a..71a6b5ed0581 100644 --- a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java +++ b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java @@ -109,7 +109,6 @@ class DefaultDeviceEffectsApplier implements DeviceEffectsApplier { if (origin == ZenModeConfig.UPDATE_ORIGIN_INIT || origin == ZenModeConfig.UPDATE_ORIGIN_INIT_USER || origin == ZenModeConfig.UPDATE_ORIGIN_USER - || origin == ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI || !mPowerManager.isInteractive()) { unregisterScreenOffReceiver(); updateNightModeImmediately(useNightMode); diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 75d3dce55abd..135a467cc6b0 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -5343,13 +5343,14 @@ public class NotificationManagerService extends SystemService { } @Override - public void setZenMode(int mode, Uri conditionId, String reason) throws RemoteException { + public void setZenMode(int mode, Uri conditionId, String reason, boolean fromUser) { enforceSystemOrSystemUI("INotificationManager.setZenMode"); final int callingUid = Binder.getCallingUid(); final long identity = Binder.clearCallingIdentity(); + enforceUserOriginOnlyFromSystem(fromUser, "setZenMode"); + try { - mZenModeHelper.setManualZenMode(mode, conditionId, - ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, // Checked by enforce() + mZenModeHelper.setManualZenMode(mode, conditionId, computeZenOrigin(fromUser), reason, /* caller= */ null, callingUid); } finally { Binder.restoreCallingIdentity(identity); @@ -5380,7 +5381,8 @@ public class NotificationManagerService extends SystemService { } @Override - public String addAutomaticZenRule(AutomaticZenRule automaticZenRule, String pkg) { + public String addAutomaticZenRule(AutomaticZenRule automaticZenRule, String pkg, + boolean fromUser) { validateAutomaticZenRule(automaticZenRule); checkCallerIsSameApp(pkg); if (automaticZenRule.getZenPolicy() != null @@ -5389,6 +5391,7 @@ public class NotificationManagerService extends SystemService { + "INTERRUPTION_FILTER_PRIORITY filters"); } enforcePolicyAccess(Binder.getCallingUid(), "addAutomaticZenRule"); + enforceUserOriginOnlyFromSystem(fromUser, "addAutomaticZenRule"); // If the calling app is the system (from any user), take the package name from the // rule's owner rather than from the caller's package. @@ -5400,24 +5403,18 @@ public class NotificationManagerService extends SystemService { } return mZenModeHelper.addAutomaticZenRule(rulePkg, automaticZenRule, - // TODO: b/308670715: Distinguish origin properly (e.g. USER if creating a rule - // manually in Settings). - isCallerSystemOrSystemUi() ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI - : ZenModeConfig.UPDATE_ORIGIN_APP, - "addAutomaticZenRule", Binder.getCallingUid()); + computeZenOrigin(fromUser), "addAutomaticZenRule", Binder.getCallingUid()); } @Override - public boolean updateAutomaticZenRule(String id, AutomaticZenRule automaticZenRule) { + public boolean updateAutomaticZenRule(String id, AutomaticZenRule automaticZenRule, + boolean fromUser) throws RemoteException { validateAutomaticZenRule(automaticZenRule); enforcePolicyAccess(Binder.getCallingUid(), "updateAutomaticZenRule"); + enforceUserOriginOnlyFromSystem(fromUser, "updateAutomaticZenRule"); - // TODO: b/308670715: Distinguish origin properly (e.g. USER if updating a rule - // manually in Settings). return mZenModeHelper.updateAutomaticZenRule(id, automaticZenRule, - isCallerSystemOrSystemUi() ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI - : ZenModeConfig.UPDATE_ORIGIN_APP, - "updateAutomaticZenRule", Binder.getCallingUid()); + computeZenOrigin(fromUser), "updateAutomaticZenRule", Binder.getCallingUid()); } private void validateAutomaticZenRule(AutomaticZenRule rule) { @@ -5445,27 +5442,24 @@ public class NotificationManagerService extends SystemService { } @Override - public boolean removeAutomaticZenRule(String id) throws RemoteException { + public boolean removeAutomaticZenRule(String id, boolean fromUser) throws RemoteException { Objects.requireNonNull(id, "Id is null"); // Verify that they can modify zen rules. enforcePolicyAccess(Binder.getCallingUid(), "removeAutomaticZenRule"); + enforceUserOriginOnlyFromSystem(fromUser, "removeAutomaticZenRule"); - // TODO: b/308670715: Distinguish origin properly (e.g. USER if removing a rule - // manually in Settings). - return mZenModeHelper.removeAutomaticZenRule(id, - isCallerSystemOrSystemUi() ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI - : ZenModeConfig.UPDATE_ORIGIN_APP, + return mZenModeHelper.removeAutomaticZenRule(id, computeZenOrigin(fromUser), "removeAutomaticZenRule", Binder.getCallingUid()); } @Override - public boolean removeAutomaticZenRules(String packageName) throws RemoteException { + public boolean removeAutomaticZenRules(String packageName, boolean fromUser) + throws RemoteException { Objects.requireNonNull(packageName, "Package name is null"); enforceSystemOrSystemUI("removeAutomaticZenRules"); + enforceUserOriginOnlyFromSystem(fromUser, "removeAutomaticZenRules"); - return mZenModeHelper.removeAutomaticZenRules(packageName, - isCallerSystemOrSystemUi() ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI - : ZenModeConfig.UPDATE_ORIGIN_APP, + return mZenModeHelper.removeAutomaticZenRules(packageName, computeZenOrigin(fromUser), packageName + "|removeAutomaticZenRules", Binder.getCallingUid()); } @@ -5478,28 +5472,54 @@ public class NotificationManagerService extends SystemService { } @Override - public void setAutomaticZenRuleState(String id, Condition condition) { + public void setAutomaticZenRuleState(String id, Condition condition, boolean fromUser) { Objects.requireNonNull(id, "id is null"); Objects.requireNonNull(condition, "Condition is null"); condition.validate(); enforcePolicyAccess(Binder.getCallingUid(), "setAutomaticZenRuleState"); - // TODO: b/308670715: Distinguish origin properly (e.g. USER if toggling a rule - // manually in Settings). - mZenModeHelper.setAutomaticZenRuleState(id, condition, - isCallerSystemOrSystemUi() ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI - : ZenModeConfig.UPDATE_ORIGIN_APP, + if (android.app.Flags.modesApi()) { + if (fromUser != (condition.source == Condition.SOURCE_USER_ACTION)) { + throw new IllegalArgumentException(String.format( + "Mismatch between fromUser (%s) and condition.source (%s)", + fromUser, Condition.sourceToString(condition.source))); + } + } + + mZenModeHelper.setAutomaticZenRuleState(id, condition, computeZenOrigin(fromUser), Binder.getCallingUid()); } + @ZenModeConfig.ConfigChangeOrigin + private int computeZenOrigin(boolean fromUser) { + // "fromUser" is introduced with MODES_API, so only consider it in that case. + // (Non-MODES_API behavior should also not depend at all on UPDATE_ORIGIN_USER). + if (android.app.Flags.modesApi() && fromUser) { + return ZenModeConfig.UPDATE_ORIGIN_USER; + } else if (isCallerSystemOrSystemUi()) { + return ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI; + } else { + return ZenModeConfig.UPDATE_ORIGIN_APP; + } + } + + private void enforceUserOriginOnlyFromSystem(boolean fromUser, String method) { + if (android.app.Flags.modesApi() + && fromUser + && !isCallerSystemOrSystemUiOrShell()) { + throw new SecurityException(String.format( + "Calling %s with fromUser == true is only allowed for system", method)); + } + } + @Override - public void setInterruptionFilter(String pkg, int filter) throws RemoteException { + public void setInterruptionFilter(String pkg, int filter, boolean fromUser) { enforcePolicyAccess(pkg, "setInterruptionFilter"); final int zen = NotificationManager.zenModeFromInterruptionFilter(filter, -1); if (zen == -1) throw new IllegalArgumentException("Invalid filter: " + filter); final int callingUid = Binder.getCallingUid(); - final boolean isSystemOrSystemUi = isCallerSystemOrSystemUi(); + enforceUserOriginOnlyFromSystem(fromUser, "setInterruptionFilter"); if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) { mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, callingUid, zen); @@ -5508,9 +5528,7 @@ public class NotificationManagerService extends SystemService { final long identity = Binder.clearCallingIdentity(); try { - mZenModeHelper.setManualZenMode(zen, null, - isSystemOrSystemUi ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI - : ZenModeConfig.UPDATE_ORIGIN_APP, + mZenModeHelper.setManualZenMode(zen, null, computeZenOrigin(fromUser), /* reason= */ "setInterruptionFilter", /* caller= */ pkg, callingUid); } finally { @@ -5825,10 +5843,11 @@ public class NotificationManagerService extends SystemService { * {@link Policy#PRIORITY_CATEGORY_MEDIA} from bypassing dnd */ @Override - public void setNotificationPolicy(String pkg, Policy policy) { + public void setNotificationPolicy(String pkg, Policy policy, boolean fromUser) { enforcePolicyAccess(pkg, "setNotificationPolicy"); + enforceUserOriginOnlyFromSystem(fromUser, "setNotificationPolicy"); int callingUid = Binder.getCallingUid(); - boolean isSystemOrSystemUi = isCallerSystemOrSystemUi(); + @ZenModeConfig.ConfigChangeOrigin int origin = computeZenOrigin(fromUser); boolean shouldApplyAsImplicitRule = android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid); @@ -5873,14 +5892,12 @@ public class NotificationManagerService extends SystemService { newVisualEffects, policy.priorityConversationSenders); if (shouldApplyAsImplicitRule) { - mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy, + origin); } else { ZenLog.traceSetNotificationPolicy(pkg, applicationInfo.targetSdkVersion, policy); - mZenModeHelper.setNotificationPolicy(policy, - isSystemOrSystemUi ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI - : ZenModeConfig.UPDATE_ORIGIN_APP, - callingUid); + mZenModeHelper.setNotificationPolicy(policy, origin, callingUid); } } catch (RemoteException e) { Slog.e(TAG, "Failed to set notification policy", e); @@ -7001,12 +7018,14 @@ public class NotificationManagerService extends SystemService { return false; } - final boolean hasBitmap = n.extras.containsKey(Notification.EXTRA_PICTURE); + final boolean hasBitmap = n.extras.containsKey(Notification.EXTRA_PICTURE) + && n.extras.getParcelable(Notification.EXTRA_PICTURE) != null; if (hasBitmap) { return true; } - final boolean hasIcon = n.extras.containsKey(Notification.EXTRA_PICTURE_ICON); + final boolean hasIcon = n.extras.containsKey(Notification.EXTRA_PICTURE_ICON) + && n.extras.getParcelable(Notification.EXTRA_PICTURE_ICON) != null; if (hasIcon) { return true; } @@ -7022,9 +7041,10 @@ public class NotificationManagerService extends SystemService { if (!isBigPictureWithBitmapOrIcon(r.getNotification())) { return; } - // Remove Notification object's reference to picture bitmap or URI - r.getNotification().extras.remove(Notification.EXTRA_PICTURE); - r.getNotification().extras.remove(Notification.EXTRA_PICTURE_ICON); + // Remove Notification object's reference to picture bitmap or URI. Leave the extras set to + // null to avoid crashing apps that came to expect them to be present but null. + r.getNotification().extras.putParcelable(Notification.EXTRA_PICTURE, null); + r.getNotification().extras.putParcelable(Notification.EXTRA_PICTURE_ICON, null); // Make Notification silent r.getNotification().flags |= FLAG_ONLY_ALERT_ONCE; diff --git a/services/core/java/com/android/server/notification/NotificationShellCmd.java b/services/core/java/com/android/server/notification/NotificationShellCmd.java index dc0cf4e09207..9f3104cbd7b0 100644 --- a/services/core/java/com/android/server/notification/NotificationShellCmd.java +++ b/services/core/java/com/android/server/notification/NotificationShellCmd.java @@ -117,7 +117,6 @@ public class NotificationShellCmd extends ShellCommand { private final NotificationManagerService mDirectService; private final INotificationManager mBinderService; private final PackageManager mPm; - private NotificationChannel mChannel; public NotificationShellCmd(NotificationManagerService service) { mDirectService = service; @@ -183,7 +182,13 @@ public class NotificationShellCmd extends ShellCommand { interruptionFilter = INTERRUPTION_FILTER_ALL; } final int filter = interruptionFilter; - mBinderService.setInterruptionFilter(callingPackage, filter); + if (android.app.Flags.modesApi()) { + mBinderService.setInterruptionFilter(callingPackage, filter, + /* fromUser= */ true); + } else { + mBinderService.setInterruptionFilter(callingPackage, filter, + /* fromUser= */ false); + } } break; case "allow_dnd": { diff --git a/services/core/java/com/android/server/notification/ZenModeEventLogger.java b/services/core/java/com/android/server/notification/ZenModeEventLogger.java index 87158cd6fe29..df570a02eba5 100644 --- a/services/core/java/com/android/server/notification/ZenModeEventLogger.java +++ b/services/core/java/com/android/server/notification/ZenModeEventLogger.java @@ -29,6 +29,7 @@ import android.content.pm.PackageManager; import android.os.Process; import android.service.notification.DNDPolicyProto; import android.service.notification.ZenModeConfig; +import android.service.notification.ZenModeConfig.ConfigChangeOrigin; import android.service.notification.ZenModeDiff; import android.service.notification.ZenPolicy; import android.util.ArrayMap; @@ -58,7 +59,7 @@ class ZenModeEventLogger { // mode change. ZenModeEventLogger.ZenStateChanges mChangeState = new ZenModeEventLogger.ZenStateChanges(); - private PackageManager mPm; + private final PackageManager mPm; ZenModeEventLogger(PackageManager pm) { mPm = pm; @@ -97,11 +98,11 @@ class ZenModeEventLogger { * @param newInfo ZenModeInfo after this change takes effect * @param callingUid the calling UID associated with the change; may be used to attribute the * change to a particular package or determine if this is a user action - * @param fromSystemOrSystemUi whether the calling UID is either system UID or system UI + * @param origin The origin of the Zen change. */ public final void maybeLogZenChange(ZenModeInfo prevInfo, ZenModeInfo newInfo, int callingUid, - boolean fromSystemOrSystemUi) { - mChangeState.init(prevInfo, newInfo, callingUid, fromSystemOrSystemUi); + @ConfigChangeOrigin int origin) { + mChangeState.init(prevInfo, newInfo, callingUid, origin); if (mChangeState.shouldLogChanges()) { maybeReassignCallingUid(); logChanges(); @@ -124,7 +125,7 @@ class ZenModeEventLogger { // We don't consider the manual rule in the old config because if a manual rule is turning // off with a call from system, that could easily be a user action to explicitly turn it off if (mChangeState.getChangedRuleType() == RULE_TYPE_MANUAL) { - if (!mChangeState.mFromSystemOrSystemUi + if (!mChangeState.isFromSystemOrSystemUi() || mChangeState.getNewManualRuleEnabler() == null) { return; } @@ -136,7 +137,7 @@ class ZenModeEventLogger { // - we've determined it's not a user action // - our current best guess is that the calling uid is system/sysui if (mChangeState.getChangedRuleType() == RULE_TYPE_AUTOMATIC) { - if (mChangeState.getIsUserAction() || !mChangeState.mFromSystemOrSystemUi) { + if (mChangeState.getIsUserAction() || !mChangeState.isFromSystemOrSystemUi()) { return; } @@ -221,10 +222,10 @@ class ZenModeEventLogger { ZenModeConfig mPrevConfig, mNewConfig; NotificationManager.Policy mPrevPolicy, mNewPolicy; int mCallingUid = Process.INVALID_UID; - boolean mFromSystemOrSystemUi = false; + @ConfigChangeOrigin int mOrigin = ZenModeConfig.UPDATE_ORIGIN_UNKNOWN; private void init(ZenModeInfo prevInfo, ZenModeInfo newInfo, int callingUid, - boolean fromSystemOrSystemUi) { + @ConfigChangeOrigin int origin) { // previous & new may be the same -- that would indicate that zen mode hasn't changed. mPrevZenMode = prevInfo.mZenMode; mNewZenMode = newInfo.mZenMode; @@ -233,7 +234,7 @@ class ZenModeEventLogger { mPrevPolicy = prevInfo.mPolicy; mNewPolicy = newInfo.mPolicy; mCallingUid = callingUid; - mFromSystemOrSystemUi = fromSystemOrSystemUi; + mOrigin = origin; } /** @@ -389,12 +390,16 @@ class ZenModeEventLogger { /** * Return our best guess as to whether the changes observed are due to a user action. - * Note that this won't be 100% accurate as we can't necessarily distinguish between a - * system uid call indicating "user interacted with Settings" vs "a system app changed - * something automatically". + * Note that this (before {@code MODES_API}) won't be 100% accurate as we can't necessarily + * distinguish between a system uid call indicating "user interacted with Settings" vs "a + * system app changed something automatically". */ boolean getIsUserAction() { - // Approach: + if (Flags.modesApi()) { + return mOrigin == ZenModeConfig.UPDATE_ORIGIN_USER; + } + + // Approach for pre-MODES_API: // - if manual rule turned on or off, the calling UID is system, and the new manual // rule does not have an enabler set, guess that this is likely to be a user action. // This may represent a system app turning on DND automatically, but we guess "user" @@ -419,13 +424,13 @@ class ZenModeEventLogger { switch (getChangedRuleType()) { case RULE_TYPE_MANUAL: // TODO(b/278888961): Distinguish the automatically-turned-off state - return mFromSystemOrSystemUi && (getNewManualRuleEnabler() == null); + return isFromSystemOrSystemUi() && (getNewManualRuleEnabler() == null); case RULE_TYPE_AUTOMATIC: for (ZenModeDiff.RuleDiff d : getChangedAutomaticRules().values()) { if (d.wasAdded() || d.wasRemoved()) { // If the change comes from system, a rule being added/removed indicates // a likely user action. From an app, it's harder to know for sure. - return mFromSystemOrSystemUi; + return isFromSystemOrSystemUi(); } ZenModeDiff.FieldDiff enabled = d.getDiffForField( ZenModeDiff.RuleDiff.FIELD_ENABLED); @@ -455,6 +460,13 @@ class ZenModeEventLogger { return false; } + boolean isFromSystemOrSystemUi() { + return mOrigin == ZenModeConfig.UPDATE_ORIGIN_INIT + || mOrigin == ZenModeConfig.UPDATE_ORIGIN_INIT_USER + || mOrigin == ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI + || mOrigin == ZenModeConfig.UPDATE_ORIGIN_RESTORE_BACKUP; + } + /** * Get the package UID associated with this change, which is just the calling UID for the * relevant method changes. This may get reset by ZenModeEventLogger, which has access to @@ -612,7 +624,7 @@ class ZenModeEventLogger { copy.mPrevPolicy = mPrevPolicy.copy(); copy.mNewPolicy = mNewPolicy.copy(); copy.mCallingUid = mCallingUid; - copy.mFromSystemOrSystemUi = mFromSystemOrSystemUi; + copy.mOrigin = mOrigin; return copy; } } diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 3f8b5952a1bc..0a46901a93d1 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -561,7 +561,7 @@ public class ZenModeHelper { * {@link Global#ZEN_MODE_IMPORTANT_INTERRUPTIONS}. */ void applyGlobalPolicyAsImplicitZenRule(String callingPkg, int callingUid, - NotificationManager.Policy policy) { + NotificationManager.Policy policy, @ConfigChangeOrigin int origin) { if (!android.app.Flags.modesApi()) { Log.wtf(TAG, "applyGlobalPolicyAsImplicitZenRule called with flag off!"); return; @@ -579,7 +579,7 @@ public class ZenModeHelper { } // TODO: b/308673679 - Keep user customization of this rule! rule.zenPolicy = ZenAdapters.notificationPolicyToZenPolicy(policy); - setConfigLocked(newConfig, /* triggeringComponent= */ null, UPDATE_ORIGIN_APP, + setConfigLocked(newConfig, /* triggeringComponent= */ null, origin, "applyGlobalPolicyAsImplicitZenRule", callingUid); } } @@ -1371,12 +1371,8 @@ public class ZenModeHelper { if (logZenModeEvents) { ZenModeEventLogger.ZenModeInfo newInfo = new ZenModeEventLogger.ZenModeInfo( mZenMode, mConfig, mConsolidatedPolicy); - boolean fromSystemOrSystemUi = origin == UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI - || origin == UPDATE_ORIGIN_INIT - || origin == UPDATE_ORIGIN_INIT_USER - || origin == UPDATE_ORIGIN_RESTORE_BACKUP; mZenModeEventLogger.maybeLogZenChange(prevInfo, newInfo, callingUid, - fromSystemOrSystemUi); + origin); } } diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index 2864a8b42445..dcfc855da855 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -132,6 +132,11 @@ public class PackageArchiver { private static final String EXTRA_INSTALLER_TITLE = "com.android.content.pm.extra.UNARCHIVE_INSTALLER_TITLE"; + private static final PorterDuffColorFilter OPACITY_LAYER_FILTER = + new PorterDuffColorFilter( + Color.argb(0.5f /* alpha */, 0f /* red */, 0f /* green */, 0f /* blue */), + PorterDuff.Mode.SRC_ATOP); + private final Context mContext; private final PackageManagerService mPm; @@ -746,11 +751,7 @@ public class PackageArchiver { return bitmap; } BitmapDrawable appIconDrawable = new BitmapDrawable(mContext.getResources(), bitmap); - PorterDuffColorFilter colorFilter = - new PorterDuffColorFilter( - Color.argb(0.32f /* alpha */, 0f /* red */, 0f /* green */, 0f /* blue */), - PorterDuff.Mode.SRC_ATOP); - appIconDrawable.setColorFilter(colorFilter); + appIconDrawable.setColorFilter(OPACITY_LAYER_FILTER); appIconDrawable.setBounds( 0 /* left */, 0 /* top */, diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java index 671e031b546b..3afba39ad4af 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -3291,7 +3291,7 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt final PermissionAllowlist permissionAllowlist = SystemConfig.getInstance().getPermissionAllowlist(); final String packageName = packageState.getPackageName(); - if (packageState.isVendor()) { + if (packageState.isVendor() || packageState.isOdm()) { return permissionAllowlist.getVendorPrivilegedAppAllowlistState(packageName, permissionName); } else if (packageState.isProduct()) { @@ -3386,7 +3386,7 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt // the permission's protectionLevel does not have the extra 'vendorPrivileged' // flag. if (allowed && isPrivilegedPermission && !bp.isVendorPrivileged() - && pkgSetting.isVendor()) { + && (pkgSetting.isVendor() || pkgSetting.isOdm())) { Slog.w(TAG, "Permission " + permissionName + " cannot be granted to privileged vendor apk " + pkg.getPackageName() + " because it isn't a 'vendorPrivileged' permission."); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 938ed2329ffd..e8b54d58c907 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -6865,13 +6865,13 @@ public class PhoneWindowManager implements WindowManagerPolicy { static class ButtonOverridePermissionChecker { boolean canAppOverrideSystemKey(Context context, int uid) { return PermissionChecker.checkPermissionForDataDelivery( - context, - OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW, - PID_UNKNOWN, - uid, - null, - null, - null) + context, + OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW, + PID_UNKNOWN, + uid, + null, + null, + null) == PERMISSION_GRANTED; } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index f3922f9917ba..1ce87a7b6af3 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -565,7 +565,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A boolean idle; // has the activity gone idle? boolean hasBeenLaunched;// has this activity ever been launched? boolean immersive; // immersive mode (don't interrupt if possible) - boolean forceNewConfig; // force re-create with new config next time boolean supportsEnterPipOnTaskSwitch; // This flag is set by the system to indicate that the // activity can enter picture in picture while pausing (only when switching to another task) // The PiP params used when deferring the entering of picture-in-picture. @@ -9600,7 +9599,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // configurations because there are cases (like moving a task to the root pinned task) where // the combine configurations are equal, but would otherwise differ in the override config mTmpConfig.setTo(mLastReportedConfiguration.getMergedConfiguration()); - if (getConfiguration().equals(mTmpConfig) && !forceNewConfig && !displayChanged) { + if (getConfiguration().equals(mTmpConfig) && !displayChanged) { ProtoLog.v(WM_DEBUG_CONFIGURATION, "Configuration & display " + "unchanged in %s", this); return true; @@ -9627,7 +9626,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return true; } - if (changes == 0 && !forceNewConfig) { + if (changes == 0) { ProtoLog.v(WM_DEBUG_CONFIGURATION, "Configuration no differences in %s", this); // There are no significant differences, so we won't relaunch but should still deliver @@ -9649,7 +9648,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // pick that up next time it starts. if (!attachedToProcess()) { ProtoLog.v(WM_DEBUG_CONFIGURATION, "Configuration doesn't matter not running %s", this); - forceNewConfig = false; return true; } @@ -9659,11 +9657,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A Integer.toHexString(changes), Integer.toHexString(info.getRealConfigChanged()), mLastReportedConfiguration); - if (shouldRelaunchLocked(changes, mTmpConfig) || forceNewConfig) { + if (shouldRelaunchLocked(changes, mTmpConfig)) { // Aha, the activity isn't handling the change, so DIE DIE DIE. configChangeFlags |= changes; startFreezingScreenLocked(globalChanges); - forceNewConfig = false; // Do not preserve window if it is freezing screen because the original window won't be // able to update drawn state that causes freeze timeout. preserveWindow &= isResizeOnlyChange(changes) && !mFreezingScreen; @@ -9883,7 +9880,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A try { ProtoLog.i(WM_DEBUG_STATES, "Moving to %s Relaunching %s callers=%s" , (andResume ? "RESUMED" : "PAUSED"), this, Debug.getCallers(6)); - forceNewConfig = false; final ClientTransactionItem callbackItem = ActivityRelaunchItem.obtain(token, pendingResults, pendingNewIntents, configChangeFlags, new MergedConfiguration(getProcessGlobalConfiguration(), diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java index 86be6ba5e6ad..7af494c296de 100644 --- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java +++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java @@ -33,6 +33,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider; +import com.android.window.flags.Flags; import java.io.File; import java.util.ArrayList; @@ -121,7 +122,8 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord // TODO remove when enabled static boolean isSnapshotEnabled() { - return SystemProperties.getInt("persist.wm.debug.activity_screenshot", 0) != 0; + return SystemProperties.getInt("persist.wm.debug.activity_screenshot", 0) != 0 + || Flags.activitySnapshotByDefault(); } static PersistInfoProvider createPersistInfoProvider( diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java index 1b45c1b4f3f1..e7621ffe8e3c 100644 --- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java +++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java @@ -224,7 +224,7 @@ class ActivityStartInterceptor { // before issuing the work challenge. return true; } - if (interceptLockedManagedProfileIfNeeded()) { + if (interceptLockedProfileIfNeeded()) { return true; } if (interceptHomeIfNeeded()) { @@ -378,7 +378,7 @@ class ActivityStartInterceptor { return true; } - private boolean interceptLockedManagedProfileIfNeeded() { + private boolean interceptLockedProfileIfNeeded() { final Intent interceptingIntent = interceptWithConfirmCredentialsIfNeeded(mAInfo, mUserId); if (interceptingIntent == null) { return false; diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 630b9e139456..cb2adbcf460a 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -579,30 +579,11 @@ class ActivityStarter { computeResolveFilterUid(callingUid, realCallingUid, filterCallingUid), realCallingPid); if (resolveInfo == null) { - final UserInfo userInfo = supervisor.getUserInfo(userId); - if (userInfo != null && userInfo.isManagedProfile()) { - // Special case for managed profiles, if attempting to launch non-cryto aware - // app in a locked managed profile from an unlocked parent allow it to resolve - // as user will be sent via confirm credentials to unlock the profile. - final UserManager userManager = UserManager.get(supervisor.mService.mContext); - boolean profileLockedAndParentUnlockingOrUnlocked = false; - final long token = Binder.clearCallingIdentity(); - try { - final UserInfo parent = userManager.getProfileParent(userId); - profileLockedAndParentUnlockingOrUnlocked = (parent != null) - && userManager.isUserUnlockingOrUnlocked(parent.id) - && !userManager.isUserUnlockingOrUnlocked(userId); - } finally { - Binder.restoreCallingIdentity(token); - } - if (profileLockedAndParentUnlockingOrUnlocked) { - resolveInfo = supervisor.resolveIntent(intent, resolvedType, userId, - PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, - computeResolveFilterUid(callingUid, realCallingUid, - filterCallingUid), realCallingPid); - } - } + // Special case for profiles: If attempting to launch non-crypto aware app in a + // locked profile or launch an app in a profile that is stopped by quiet mode from + // an unlocked parent, allow it to resolve as user will be sent via confirm + // credentials to unlock the profile. + resolveInfo = resolveIntentForLockedOrStoppedProfiles(supervisor); } // Collect information about the target of the Intent. @@ -616,6 +597,36 @@ class ActivityStarter { UserHandle.getUserId(activityInfo.applicationInfo.uid)); } } + + /** + * Resolve intent for locked or stopped profiles if the parent profile is unlocking or + * unlocked. + */ + ResolveInfo resolveIntentForLockedOrStoppedProfiles( + ActivityTaskSupervisor supervisor) { + final UserInfo userInfo = supervisor.getUserInfo(userId); + if (userInfo != null && userInfo.isProfile()) { + final UserManager userManager = UserManager.get(supervisor.mService.mContext); + boolean profileLockedAndParentUnlockingOrUnlocked = false; + final long token = Binder.clearCallingIdentity(); + try { + final UserInfo parent = userManager.getProfileParent(userId); + profileLockedAndParentUnlockingOrUnlocked = (parent != null) + && userManager.isUserUnlockingOrUnlocked(parent.id) + && !userManager.isUserUnlockingOrUnlocked(userId); + } finally { + Binder.restoreCallingIdentity(token); + } + if (profileLockedAndParentUnlockingOrUnlocked) { + return supervisor.resolveIntent(intent, resolvedType, userId, + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, + computeResolveFilterUid(callingUid, realCallingUid, + filterCallingUid), realCallingPid); + } + } + return null; + } } ActivityStarter(ActivityStartController controller, ActivityTaskManagerService service, @@ -2767,10 +2778,7 @@ class ActivityStarter { } } - // If the target task is not in the front, then we need to bring it to the front... - // except... well, with SINGLE_TASK_LAUNCH it's not entirely clear. We'd like to have - // the same behavior as if a new instance was being started, which means not bringing it - // to the front if the caller is not itself in the front. + // If the target task is not in the front, then we need to bring it to the front. final boolean differentTopTask; if (mTargetRootTask.getDisplayArea() == mPreferredTaskDisplayArea) { final Task focusRootTask = mTargetRootTask.mDisplayContent.getFocusedRootTask(); @@ -2787,49 +2795,47 @@ class ActivityStarter { if (differentTopTask && !avoidMoveToFront()) { mStartActivity.intent.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT); - if (mSourceRecord == null || inTopNonFinishingTask(mSourceRecord)) { - // We really do want to push this one into the user's face, right now. - if (mLaunchTaskBehind && mSourceRecord != null) { - intentActivity.setTaskToAffiliateWith(mSourceRecord.getTask()); - } - - if (intentActivity.isDescendantOf(mTargetRootTask)) { - // TODO(b/151572268): Figure out a better way to move tasks in above 2-levels - // tasks hierarchies. - if (mTargetRootTask != intentTask - && mTargetRootTask != intentTask.getParent().asTask()) { - intentTask.getParent().positionChildAt(POSITION_TOP, intentTask, - false /* includingParents */); - intentTask = intentTask.getParent().asTaskFragment().getTask(); - } - // If the activity is visible in multi-windowing mode, it may already be on - // the top (visible to user but not the global top), then the result code - // should be START_DELIVERED_TO_TOP instead of START_TASK_TO_FRONT. - final boolean wasTopOfVisibleRootTask = intentActivity.isVisibleRequested() - && intentActivity.inMultiWindowMode() - && intentActivity == mTargetRootTask.topRunningActivity() - && !intentActivity.mTransitionController.isTransientHide( - mTargetRootTask); - // We only want to move to the front, if we aren't going to launch on a - // different root task. If we launch on a different root task, we will put the - // task on top there. - // Defer resuming the top activity while moving task to top, since the - // current task-top activity may not be the activity that should be resumed. - mTargetRootTask.moveTaskToFront(intentTask, mNoAnimation, mOptions, - mStartActivity.appTimeTracker, DEFER_RESUME, - "bringingFoundTaskToFront"); - mMovedToFront = !wasTopOfVisibleRootTask; - } else if (intentActivity.getWindowingMode() != WINDOWING_MODE_PINNED) { - // Leaves reparenting pinned task operations to task organizer to make sure it - // dismisses pinned task properly. - // TODO(b/199997762): Consider leaving all reparent operation of organized tasks - // to task organizer. - intentTask.reparent(mTargetRootTask, ON_TOP, REPARENT_MOVE_ROOT_TASK_TO_FRONT, - ANIMATE, DEFER_RESUME, "reparentToTargetRootTask"); - mMovedToFront = true; + // We really do want to push this one into the user's face, right now. + if (mLaunchTaskBehind && mSourceRecord != null) { + intentActivity.setTaskToAffiliateWith(mSourceRecord.getTask()); + } + + if (intentActivity.isDescendantOf(mTargetRootTask)) { + // TODO(b/151572268): Figure out a better way to move tasks in above 2-levels + // tasks hierarchies. + if (mTargetRootTask != intentTask + && mTargetRootTask != intentTask.getParent().asTask()) { + intentTask.getParent().positionChildAt(POSITION_TOP, intentTask, + false /* includingParents */); + intentTask = intentTask.getParent().asTaskFragment().getTask(); } - mOptions = null; - } + // If the activity is visible in multi-windowing mode, it may already be on + // the top (visible to user but not the global top), then the result code + // should be START_DELIVERED_TO_TOP instead of START_TASK_TO_FRONT. + final boolean wasTopOfVisibleRootTask = intentActivity.isVisibleRequested() + && intentActivity.inMultiWindowMode() + && intentActivity == mTargetRootTask.topRunningActivity() + && !intentActivity.mTransitionController.isTransientHide( + mTargetRootTask); + // We only want to move to the front, if we aren't going to launch on a + // different root task. If we launch on a different root task, we will put the + // task on top there. + // Defer resuming the top activity while moving task to top, since the + // current task-top activity may not be the activity that should be resumed. + mTargetRootTask.moveTaskToFront(intentTask, mNoAnimation, mOptions, + mStartActivity.appTimeTracker, DEFER_RESUME, + "bringingFoundTaskToFront"); + mMovedToFront = !wasTopOfVisibleRootTask; + } else if (intentActivity.getWindowingMode() != WINDOWING_MODE_PINNED) { + // Leaves reparenting pinned task operations to task organizer to make sure it + // dismisses pinned task properly. + // TODO(b/199997762): Consider leaving all reparent operation of organized tasks + // to task organizer. + intentTask.reparent(mTargetRootTask, ON_TOP, REPARENT_MOVE_ROOT_TASK_TO_FRONT, + ANIMATE, DEFER_RESUME, "reparentToTargetRootTask"); + mMovedToFront = true; + } + mOptions = null; } if (differentTopTask) { logPIOnlyCreatorAllowsBAL(); @@ -2850,20 +2856,6 @@ class ActivityStarter { mRootWindowContainer.getDefaultTaskDisplayArea(), mTargetRootTask); } - private boolean inTopNonFinishingTask(ActivityRecord r) { - if (r == null || r.getTask() == null) { - return false; - } - - final Task rTask = r.getTask(); - final Task parent = rTask.getCreatedByOrganizerTask() != null - ? rTask.getCreatedByOrganizerTask() : r.getRootTask(); - final ActivityRecord topNonFinishingActivity = parent != null - ? parent.getTopNonFinishingActivity() : null; - - return topNonFinishingActivity != null && topNonFinishingActivity.getTask() == rTask; - } - private void resumeTargetRootTaskIfNeeded() { if (mDoResume) { final ActivityRecord next = mTargetRootTask.topRunningActivity( diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 908c49eb8580..dbae29bd37c9 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -62,6 +62,7 @@ import static android.provider.Settings.Global.DEVELOPMENT_FORCE_RESIZABLE_ACTIV import static android.provider.Settings.Global.DEVELOPMENT_FORCE_RTL; import static android.provider.Settings.Global.HIDE_ERROR_DIALOGS; import static android.provider.Settings.System.FONT_SCALE; +import static android.service.controls.flags.Flags.homePanelDream; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.TRANSIT_CHANGE; @@ -1501,14 +1502,19 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { a.exported = true; a.name = DreamActivity.class.getName(); a.enabled = true; - a.launchMode = ActivityInfo.LAUNCH_SINGLE_INSTANCE; a.persistableMode = ActivityInfo.PERSIST_NEVER; a.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; a.colorMode = ActivityInfo.COLOR_MODE_DEFAULT; a.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS; - a.resizeMode = RESIZE_MODE_UNRESIZEABLE; a.configChanges = 0xffffffff; + if (homePanelDream()) { + a.launchMode = ActivityInfo.LAUNCH_SINGLE_TASK; + } else { + a.resizeMode = RESIZE_MODE_UNRESIZEABLE; + a.launchMode = ActivityInfo.LAUNCH_SINGLE_INSTANCE; + } + final ActivityOptions options = ActivityOptions.makeBasic(); options.setLaunchActivityType(ACTIVITY_TYPE_DREAM); diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index e59601c69cd8..10efb9491bed 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -906,7 +906,6 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { } mService.getPackageManagerInternalLocked().notifyPackageUse( r.intent.getComponent().getPackageName(), NOTIFY_PACKAGE_USE_ACTIVITY); - r.forceNewConfig = false; mService.getAppWarningsLocked().onStartActivity(r); // Because we could be starting an Activity in the system process this may not go diff --git a/services/core/java/com/android/server/wm/CompatModePackages.java b/services/core/java/com/android/server/wm/CompatModePackages.java index 73bcc8d94252..1a8927ecf564 100644 --- a/services/core/java/com/android/server/wm/CompatModePackages.java +++ b/services/core/java/com/android/server/wm/CompatModePackages.java @@ -19,7 +19,6 @@ package com.android.server.wm; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; -import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS; import static com.android.server.wm.CompatScaleProvider.COMPAT_SCALE_MODE_SYSTEM_FIRST; import static com.android.server.wm.CompatScaleProvider.COMPAT_SCALE_MODE_SYSTEM_LAST; @@ -47,6 +46,7 @@ import android.util.AtomicFile; import android.util.DisplayMetrics; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.util.Xml; import com.android.internal.protolog.common.ProtoLog; @@ -60,6 +60,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -347,6 +348,7 @@ public final class CompatModePackages { private GameManagerInternal mGameManager; private final AtomicFile mFile; private final HashMap<String, Integer> mPackages = new HashMap<>(); + private final SparseBooleanArray mLegacyScreenCompatPackages = new SparseBooleanArray(); private final CompatHandler mHandler; private final SparseArray<CompatScaleProvider> mProviders = new SparseArray<>(); @@ -427,6 +429,7 @@ public final class CompatModePackages { mPackages.remove(packageName); scheduleWrite(); } + mLegacyScreenCompatPackages.delete(packageName.hashCode()); } public void handlePackageAddedLocked(String packageName, boolean updated) { @@ -458,6 +461,17 @@ public final class CompatModePackages { mHandler.sendMessageDelayed(msg, 10000); } + /** + * Returns {@code true} if the windows belonging to the package should be scaled with + * {@link DisplayContent#mCompatibleScreenScale}. + */ + boolean useLegacyScreenCompatMode(String packageName) { + if (mLegacyScreenCompatPackages.size() == 0) { + return false; + } + return mLegacyScreenCompatPackages.get(packageName.hashCode()); + } + public CompatibilityInfo compatibilityInfoForPackageLocked(ApplicationInfo ai) { final boolean forceCompat = getPackageCompatModeEnabledLocked(ai); final CompatScale compatScale = getCompatScaleFromProvider(ai.packageName, ai.uid); @@ -466,8 +480,18 @@ public final class CompatModePackages { : getCompatScale(ai.packageName, ai.uid, /* checkProvider= */ false); final float densityScale = compatScale != null ? compatScale.mDensityScaleFactor : appScale; final Configuration config = mService.getGlobalConfiguration(); - return new CompatibilityInfo(ai, config.screenLayout, config.smallestScreenWidthDp, - forceCompat, appScale, densityScale); + final CompatibilityInfo info = new CompatibilityInfo(ai, config.screenLayout, + config.smallestScreenWidthDp, forceCompat, appScale, densityScale); + // Ignore invalid info which may be a placeholder of isolated process. + if (ai.flags != 0 && ai.sourceDir != null) { + if (!info.supportsScreen() && !"android".equals(ai.packageName)) { + Slog.i(TAG, "Use legacy screen compat mode: " + ai.packageName); + mLegacyScreenCompatPackages.put(ai.packageName.hashCode(), true); + } else if (mLegacyScreenCompatPackages.size() > 0) { + mLegacyScreenCompatPackages.delete(ai.packageName.hashCode()); + } + } + return info; } float getCompatScale(String packageName, int uid) { @@ -718,14 +742,23 @@ public final class CompatModePackages { scheduleWrite(); - final Task rootTask = mService.getTopDisplayFocusedRootTask(); - ActivityRecord starting = rootTask.restartPackage(packageName); - + final ArrayList<WindowProcessController> restartedApps = new ArrayList<>(); + mService.mRootWindowContainer.forAllWindows(w -> { + final ActivityRecord ar = w.mActivityRecord; + if (ar != null) { + if (ar.packageName.equals(packageName) && !restartedApps.contains(ar.app)) { + ar.restartProcessIfVisible(); + restartedApps.add(ar.app); + } + } else if (w.getProcess().mInfo.packageName.equals(packageName)) { + w.updateGlobalScale(); + } + }, true /* traverseTopToBottom */); // Tell all processes that loaded this package about the change. SparseArray<WindowProcessController> pidMap = mService.mProcessMap.getPidMap(); for (int i = pidMap.size() - 1; i >= 0; i--) { final WindowProcessController app = pidMap.valueAt(i); - if (!app.containsPackage(packageName)) { + if (!app.containsPackage(packageName) || restartedApps.contains(app)) { continue; } try { @@ -737,14 +770,6 @@ public final class CompatModePackages { } catch (Exception e) { } } - - if (starting != null) { - starting.ensureActivityConfiguration(0 /* globalChanges */, - false /* preserveWindow */); - // And we need to make sure at this point that all other activities - // are made visible with the correct configuration. - rootTask.ensureActivitiesVisible(starting, 0, !PRESERVE_WINDOWS); - } } } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index ae10ce3690aa..f8dc9c79dda7 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -512,7 +512,11 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp */ private final DisplayMetrics mCompatDisplayMetrics = new DisplayMetrics(); - /** The desired scaling factor for compatible apps. */ + /** + * The desired scaling factor for compatible apps. It limits the size of the window to be + * original size ([320x480] x density). Used to scale window for applications running under + * legacy compatibility mode. + */ float mCompatibleScreenScale; /** @see #getCurrentOverrideConfigurationChanges */ @@ -4794,7 +4798,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp assignRelativeLayerForIme(getSyncTransaction(), true /* forceUpdate */); scheduleAnimation(); - mWmService.mH.post(() -> InputMethodManagerInternal.get().onImeParentChanged()); + mWmService.mH.post( + () -> InputMethodManagerInternal.get().onImeParentChanged(getDisplayId())); } else if (mImeControlTarget != null && mImeControlTarget == mImeLayeringTarget) { // Even if the IME surface parent is not changed, the layer target belonging to the // parent may have changes. Then attempt to reassign if the IME control target is @@ -7090,7 +7095,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } @Override - public void notifyInsetsControlChanged() { + public void notifyInsetsControlChanged(int displayId) { final InsetsStateController stateController = getInsetsStateController(); try { mRemoteInsetsController.insetsControlChanged(stateController.getRawInsetsState(), diff --git a/services/core/java/com/android/server/wm/InsetsControlTarget.java b/services/core/java/com/android/server/wm/InsetsControlTarget.java index 8ecbc177896c..b74eb56ebdca 100644 --- a/services/core/java/com/android/server/wm/InsetsControlTarget.java +++ b/services/core/java/com/android/server/wm/InsetsControlTarget.java @@ -29,8 +29,10 @@ interface InsetsControlTarget { /** * Notifies the control target that the insets control has changed. + * + * @param displayId the display hosting the window of this target */ - default void notifyInsetsControlChanged() { + default void notifyInsetsControlChanged(int displayId) { }; /** diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index 781567990235..3c556bf7b126 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -728,7 +728,7 @@ class InsetsPolicy { } @Override - public void notifyInsetsControlChanged() { + public void notifyInsetsControlChanged(int displayId) { mHandler.post(this); } diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java index c4d01291f558..6b9fcf411ce1 100644 --- a/services/core/java/com/android/server/wm/InsetsStateController.java +++ b/services/core/java/com/android/server/wm/InsetsStateController.java @@ -72,7 +72,7 @@ class InsetsStateController { }; private final InsetsControlTarget mEmptyImeControlTarget = new InsetsControlTarget() { @Override - public void notifyInsetsControlChanged() { + public void notifyInsetsControlChanged(int displayId) { InsetsSourceControl[] controls = getControlsForDispatch(this); if (controls == null) { return; @@ -80,7 +80,7 @@ class InsetsStateController { for (InsetsSourceControl control : controls) { if (control.getType() == WindowInsets.Type.ime()) { mDisplayContent.mWmService.mH.post(() -> - InputMethodManagerInternal.get().removeImeSurface()); + InputMethodManagerInternal.get().removeImeSurface(displayId)); } } } @@ -370,9 +370,10 @@ class InsetsStateController { provider.onSurfaceTransactionApplied(); } final ArraySet<InsetsControlTarget> newControlTargets = new ArraySet<>(); + int displayId = mDisplayContent.getDisplayId(); for (int i = mPendingControlChanged.size() - 1; i >= 0; i--) { final InsetsControlTarget controlTarget = mPendingControlChanged.valueAt(i); - controlTarget.notifyInsetsControlChanged(); + controlTarget.notifyInsetsControlChanged(displayId); if (mControlTargetProvidersMap.containsKey(controlTarget)) { // We only collect targets who get controls, not lose controls. newControlTargets.add(controlTarget); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 671acfc697e4..dbfcc22c6903 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -34,7 +34,6 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS; import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME; -import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT; import static android.content.pm.ActivityInfo.FLAG_RELINQUISH_TASK_IDENTITY; import static android.content.pm.ActivityInfo.FLAG_SHOW_FOR_ALL_USERS; import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_LANDSCAPE_ONLY; @@ -5917,22 +5916,6 @@ class Task extends TaskFragment { return activities; } - ActivityRecord restartPackage(String packageName) { - ActivityRecord starting = topRunningActivity(); - - // All activities that came from the package must be - // restarted as if there was a config change. - forAllActivities(r -> { - if (!r.info.packageName.equals(packageName)) return; - r.forceNewConfig = true; - if (starting != null && r == starting && r.isVisibleRequested()) { - r.startFreezingScreenLocked(CONFIG_SCREEN_LAYOUT); - } - }); - - return starting; - } - Task reuseOrCreateTask(ActivityInfo info, Intent intent, boolean toTop) { return reuseOrCreateTask(info, intent, null /*voiceSession*/, null /*voiceInteractor*/, toTop, null /*activity*/, null /*source*/, null /*options*/); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index f5f0dc6d7178..9c21e4c0121e 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -52,7 +52,6 @@ import static android.view.WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON; import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static android.view.WindowManager.LayoutParams.MATCH_PARENT; -import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NOT_MAGNIFIABLE; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_SYSTEM_APPLICATION_OVERLAY; @@ -1247,13 +1246,14 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP * @see ActivityRecord#hasSizeCompatBounds() */ boolean hasCompatScale() { - if ((mAttrs.privateFlags & PRIVATE_FLAG_COMPATIBLE_WINDOW) != 0) { - return true; - } if (mAttrs.type == TYPE_APPLICATION_STARTING) { // Exclude starting window because it is not displayed by the application. return false; } + if (mWmService.mAtmService.mCompatModePackages.useLegacyScreenCompatMode( + mSession.mProcess.mInfo.packageName)) { + return true; + } return mActivityRecord != null && mActivityRecord.hasSizeCompatBounds() || mOverrideScale != 1f; } @@ -3775,7 +3775,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } @Override - public void notifyInsetsControlChanged() { + public void notifyInsetsControlChanged(int displayId) { ProtoLog.d(WM_DEBUG_WINDOW_INSETS, "notifyInsetsControlChanged for %s ", this); if (mRemoved) { return; diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd index c625b1e1eef7..adbd3c9c5096 100644 --- a/services/core/xsd/display-device-config/display-device-config.xsd +++ b/services/core/xsd/display-device-config/display-device-config.xsd @@ -595,7 +595,7 @@ <!-- Sets the brightness mapping of the desired screen brightness to the corresponding lux for the current display --> <xs:element name="luxToBrightnessMapping" type="luxToBrightnessMapping" - minOccurs="0" maxOccurs="1"> + minOccurs="0" maxOccurs="unbounded"> <xs:annotation name="final"/> </xs:element> </xs:sequence> @@ -619,12 +619,20 @@ This is used in place of config_autoBrightnessLevels and config_autoBrightnessLcdBacklightValues defined in the config XML resource. + + On devices that allow users to choose from a set of predefined options in display + auto-brightness settings, multiple mappings for different modes and settings can be defined. + + If no mode is specified, the mapping will be used for the default mode. + If no setting is specified, the mapping will be used for the normal brightness setting. --> <xs:complexType name="luxToBrightnessMapping"> <xs:element name="map" type="nonNegativeFloatToFloatMap"> <xs:annotation name="nonnull"/> <xs:annotation name="final"/> </xs:element> + <xs:element name="mode" type="xs:string" minOccurs="0"/> + <xs:element name="setting" type="xs:string" minOccurs="0"/> </xs:complexType> <!-- Represents a point in the display brightness mapping, representing the lux level from the diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt index 8c8c1230f944..98c95edef237 100644 --- a/services/core/xsd/display-device-config/schema/current.txt +++ b/services/core/xsd/display-device-config/schema/current.txt @@ -8,13 +8,12 @@ package com.android.server.display.config { method public final java.math.BigInteger getDarkeningLightDebounceIdleMillis(); method public final java.math.BigInteger getDarkeningLightDebounceMillis(); method public boolean getEnabled(); - method public final com.android.server.display.config.LuxToBrightnessMapping getLuxToBrightnessMapping(); + method public final java.util.List<com.android.server.display.config.LuxToBrightnessMapping> getLuxToBrightnessMapping(); method public final void setBrighteningLightDebounceIdleMillis(java.math.BigInteger); method public final void setBrighteningLightDebounceMillis(java.math.BigInteger); method public final void setDarkeningLightDebounceIdleMillis(java.math.BigInteger); method public final void setDarkeningLightDebounceMillis(java.math.BigInteger); method public void setEnabled(boolean); - method public final void setLuxToBrightnessMapping(com.android.server.display.config.LuxToBrightnessMapping); } public class BlockingZoneConfig { @@ -220,7 +219,11 @@ package com.android.server.display.config { public class LuxToBrightnessMapping { ctor public LuxToBrightnessMapping(); method @NonNull public final com.android.server.display.config.NonNegativeFloatToFloatMap getMap(); + method public String getMode(); + method public String getSetting(); method public final void setMap(@NonNull com.android.server.display.config.NonNegativeFloatToFloatMap); + method public void setMode(String); + method public void setSetting(String); } public class NitsMap { diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt index f69f6283f968..022268df4a63 100644 --- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt @@ -1262,7 +1262,7 @@ class AppIdPermissionPolicy : SchemePolicy() { val apexModuleName = packageState.apexModuleName val packageName = packageState.packageName return when { - packageState.isVendor -> + packageState.isVendor || packageState.isOdm -> permissionAllowlist.getVendorPrivilegedAppAllowlistState( packageName, permissionName @@ -1471,12 +1471,15 @@ class AppIdPermissionPolicy : SchemePolicy() { // In any case, don't grant a privileged permission to privileged vendor apps, // if the permission's protectionLevel does not have the extra vendorPrivileged // flag. - if (packageState.isVendor && !permission.isVendorPrivileged) { + if ( + (packageState.isVendor || packageState.isOdm) && + !permission.isVendorPrivileged + ) { Slog.w( LOG_TAG, "Permission $permissionName cannot be granted to privileged" + - " vendor app $packageName because it isn't a vendorPrivileged" + - " permission" + " vendor (or odm) app $packageName because it isn't a" + + " vendorPrivileged permission" ) return false } diff --git a/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java index 189d9bbfe806..c5a1ba1f6758 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java @@ -107,20 +107,6 @@ public class BrightnessMappingStrategyTest { 468.5f, }; - private static final int[] DISPLAY_LEVELS_INT = { - 9, - 30, - 45, - 62, - 78, - 96, - 119, - 146, - 178, - 221, - 255 - }; - private static final float[] DISPLAY_LEVELS = { 0.03f, 0.11f, @@ -172,62 +158,23 @@ public class BrightnessMappingStrategyTest { DisplayWhiteBalanceController mMockDwbc; @Test - public void testSimpleStrategyMappingAtControlPoints_IntConfig() { - Resources res = createResources(DISPLAY_LEVELS_INT); - DisplayDeviceConfig ddc = createDdc(); + public void testSimpleStrategyMappingAtControlPoints() { + Resources res = createResources(); + DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevels(DISPLAY_LEVELS).build(); BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(res, ddc, - AUTO_BRIGHTNESS_MODE_DEFAULT, - mMockDwbc); - assertNotNull("BrightnessMappingStrategy should not be null", simple); - for (int i = 0; i < LUX_LEVELS.length; i++) { - final float expectedLevel = MathUtils.map(PowerManager.BRIGHTNESS_OFF + 1, - PowerManager.BRIGHTNESS_ON, PowerManager.BRIGHTNESS_MIN, - PowerManager.BRIGHTNESS_MAX, DISPLAY_LEVELS_INT[i]); - assertEquals(expectedLevel, - simple.getBrightness(LUX_LEVELS[i]), 0.0001f /*tolerance*/); - } - } - - @Test - public void testSimpleStrategyMappingBetweenControlPoints_IntConfig() { - Resources res = createResources(DISPLAY_LEVELS_INT); - DisplayDeviceConfig ddc = createDdc(); - BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(res, ddc, - AUTO_BRIGHTNESS_MODE_DEFAULT, - mMockDwbc); - assertNotNull("BrightnessMappingStrategy should not be null", simple); - for (int i = 1; i < LUX_LEVELS.length; i++) { - final float lux = (LUX_LEVELS[i - 1] + LUX_LEVELS[i]) / 2; - final float backlight = simple.getBrightness(lux) * PowerManager.BRIGHTNESS_ON; - assertTrue("Desired brightness should be between adjacent control points.", - backlight > DISPLAY_LEVELS_INT[i - 1] - && backlight < DISPLAY_LEVELS_INT[i]); - } - } - - @Test - public void testSimpleStrategyMappingAtControlPoints_FloatConfig() { - Resources res = createResources(EMPTY_INT_ARRAY); - DisplayDeviceConfig ddc = createDdc(EMPTY_FLOAT_ARRAY, EMPTY_FLOAT_ARRAY, LUX_LEVELS, - EMPTY_FLOAT_ARRAY, DISPLAY_LEVELS); - BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(res, ddc, - AUTO_BRIGHTNESS_MODE_DEFAULT, - mMockDwbc); + AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); assertNotNull("BrightnessMappingStrategy should not be null", simple); for (int i = 0; i < LUX_LEVELS.length; i++) { - assertEquals(DISPLAY_LEVELS[i], simple.getBrightness(LUX_LEVELS[i]), - /* tolerance= */ 0.0001f); + assertEquals(DISPLAY_LEVELS[i], simple.getBrightness(LUX_LEVELS[i]), TOLERANCE); } } @Test - public void testSimpleStrategyMappingBetweenControlPoints_FloatConfig() { - Resources res = createResources(EMPTY_INT_ARRAY); - DisplayDeviceConfig ddc = createDdc(EMPTY_FLOAT_ARRAY, EMPTY_FLOAT_ARRAY, LUX_LEVELS, - EMPTY_FLOAT_ARRAY, DISPLAY_LEVELS); + public void testSimpleStrategyMappingBetweenControlPoints() { + Resources res = createResources(); + DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevels(DISPLAY_LEVELS).build(); BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(res, ddc, - AUTO_BRIGHTNESS_MODE_DEFAULT, - mMockDwbc); + AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); assertNotNull("BrightnessMappingStrategy should not be null", simple); for (int i = 1; i < LUX_LEVELS.length; i++) { final float lux = (LUX_LEVELS[i - 1] + LUX_LEVELS[i]) / 2; @@ -239,8 +186,8 @@ public class BrightnessMappingStrategyTest { @Test public void testSimpleStrategyIgnoresNewConfiguration() { - Resources res = createResources(DISPLAY_LEVELS_INT); - DisplayDeviceConfig ddc = createDdc(); + Resources res = createResources(); + DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevels(DISPLAY_LEVELS).build(); BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); @@ -255,25 +202,23 @@ public class BrightnessMappingStrategyTest { @Test public void testSimpleStrategyIgnoresNullConfiguration() { - Resources res = createResources(DISPLAY_LEVELS_INT); - DisplayDeviceConfig ddc = createDdc(); + Resources res = createResources(); + DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevels(DISPLAY_LEVELS).build(); BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); strategy.setBrightnessConfiguration(null); - final int n = DISPLAY_LEVELS_INT.length; - final float expectedBrightness = - (float) DISPLAY_LEVELS_INT[n - 1] / PowerManager.BRIGHTNESS_ON; + final int n = DISPLAY_LEVELS.length; + final float expectedBrightness = DISPLAY_LEVELS[n - 1]; assertEquals(expectedBrightness, strategy.getBrightness(LUX_LEVELS[n - 1]), 0.0001f /*tolerance*/); } @Test public void testPhysicalStrategyMappingAtControlPoints() { - Resources res = createResources(EMPTY_INT_ARRAY); - DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, - DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, - LUX_LEVELS, DISPLAY_LEVELS_NITS); + Resources res = createResources(); + DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS) + .build(); BrightnessMappingStrategy physical = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); assertNotNull("BrightnessMappingStrategy should not be null", physical); @@ -290,9 +235,9 @@ public class BrightnessMappingStrategyTest { @Test public void testPhysicalStrategyMappingBetweenControlPoints() { - Resources res = createResources(EMPTY_INT_ARRAY); - DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE, - LUX_LEVELS, DISPLAY_LEVELS_NITS); + Resources res = createResources(); + DisplayDeviceConfig ddc = new DdcBuilder().setBrightnessRange(BACKLIGHT_RANGE_ZERO_TO_ONE) + .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS).build(); BrightnessMappingStrategy physical = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); assertNotNull("BrightnessMappingStrategy should not be null", physical); @@ -309,9 +254,9 @@ public class BrightnessMappingStrategyTest { @Test public void testPhysicalStrategyUsesNewConfigurations() { - Resources res = createResources(EMPTY_INT_ARRAY); - DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, - DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, DISPLAY_LEVELS_NITS); + Resources res = createResources(); + DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS) + .build(); BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); @@ -336,9 +281,9 @@ public class BrightnessMappingStrategyTest { @Test public void testPhysicalStrategyRecalculateSplines() { - Resources res = createResources(EMPTY_INT_ARRAY); - DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, - DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, DISPLAY_LEVELS_NITS); + Resources res = createResources(); + DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS) + .build(); BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); float[] adjustedNits50p = new float[DISPLAY_RANGE_NITS.length]; @@ -381,9 +326,10 @@ public class BrightnessMappingStrategyTest { @Test public void testDefaultStrategyIsPhysical() { - Resources res = createResources(DISPLAY_LEVELS_INT); - DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, - DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, DISPLAY_LEVELS_NITS); + Resources res = createResources(); + DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevels(DISPLAY_LEVELS) + .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS) + .build(); BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); assertTrue(strategy instanceof BrightnessMappingStrategy.PhysicalMappingStrategy); @@ -396,17 +342,17 @@ public class BrightnessMappingStrategyTest { float tmp = lux[idx]; lux[idx] = lux[idx + 1]; lux[idx + 1] = tmp; - Resources res = createResources(EMPTY_INT_ARRAY); - DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, - DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, lux, DISPLAY_LEVELS_NITS); + Resources res = createResources(); + DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsLux(lux) + .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS).build(); BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); assertNull(strategy); // And make sure we get the same result even if it's monotone but not increasing. lux[idx] = lux[idx + 1]; - ddc = createDdc(DISPLAY_RANGE_NITS, DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, lux, - DISPLAY_LEVELS_NITS); + ddc = new DdcBuilder().setAutoBrightnessLevelsLux(lux) + .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS).build(); strategy = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); assertNull(strategy); @@ -419,25 +365,25 @@ public class BrightnessMappingStrategyTest { // Make sure it's strictly increasing so that the only failure is the differing array // lengths lux[lux.length - 1] = lux[lux.length - 2] + 1; - Resources res = createResources(EMPTY_INT_ARRAY); - DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, - DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, lux, DISPLAY_LEVELS_NITS); + Resources res = createResources(); + DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsLux(lux) + .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS).build(); BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); assertNull(strategy); - res = createResources(DISPLAY_LEVELS_INT); + ddc = new DdcBuilder().setAutoBrightnessLevelsLux(lux) + .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS) + .setAutoBrightnessLevels(DISPLAY_LEVELS).build(); strategy = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); assertNull(strategy); // Extra backlight level - final int[] backlight = Arrays.copyOf( - DISPLAY_LEVELS_INT, DISPLAY_LEVELS_INT.length + 1); + final float[] backlight = Arrays.copyOf(DISPLAY_LEVELS, DISPLAY_LEVELS.length + 1); backlight[backlight.length - 1] = backlight[backlight.length - 2] + 1; - res = createResources(backlight); - ddc = createDdc(DISPLAY_RANGE_NITS, - DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, EMPTY_FLOAT_ARRAY); + res = createResources(); + ddc = new DdcBuilder().setAutoBrightnessLevels(backlight).build(); strategy = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); assertNull(strategy); @@ -445,9 +391,9 @@ public class BrightnessMappingStrategyTest { // Extra nits level final float[] nits = Arrays.copyOf(DISPLAY_RANGE_NITS, DISPLAY_LEVELS_NITS.length + 1); nits[nits.length - 1] = nits[nits.length - 2] + 1; - res = createResources(EMPTY_INT_ARRAY); - ddc = createDdc(DISPLAY_RANGE_NITS, - DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, nits); + res = createResources(); + ddc = new DdcBuilder().setAutoBrightnessLevelsNits(nits) + .setAutoBrightnessLevels(EMPTY_FLOAT_ARRAY).build(); strategy = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); assertNull(strategy); @@ -455,40 +401,32 @@ public class BrightnessMappingStrategyTest { @Test public void testPhysicalStrategyRequiresNitsMapping() { - Resources res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/); - DisplayDeviceConfig ddc = createDdc(EMPTY_FLOAT_ARRAY /*nitsRange*/); + Resources res = createResources(); + DisplayDeviceConfig ddc = new DdcBuilder().setNitsRange(EMPTY_FLOAT_ARRAY).build(); BrightnessMappingStrategy physical = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); assertNull(physical); - - res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/); - physical = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, - mMockDwbc); - assertNull(physical); - - res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/); - physical = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, - mMockDwbc); - assertNull(physical); } @Test public void testStrategiesAdaptToUserDataPoint() { - Resources res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/); - DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE, - LUX_LEVELS, DISPLAY_LEVELS_NITS); + Resources res = createResources(); + DisplayDeviceConfig ddc = new DdcBuilder().setBrightnessRange(BACKLIGHT_RANGE_ZERO_TO_ONE) + .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS).build(); assertStrategyAdaptsToUserDataPoints(BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc)); - ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE); - res = createResources(DISPLAY_LEVELS_INT); + ddc = new DdcBuilder().setBrightnessRange(BACKLIGHT_RANGE_ZERO_TO_ONE) + .setAutoBrightnessLevels(DISPLAY_LEVELS).build(); + res = createResources(); assertStrategyAdaptsToUserDataPoints(BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc)); } @Test public void testIdleModeConfigLoadsCorrectly() { - Resources res = createResourcesIdle(LUX_LEVELS_IDLE, DISPLAY_LEVELS_NITS_IDLE); - DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE); + Resources res = createResources(LUX_LEVELS_IDLE, DISPLAY_LEVELS_NITS_IDLE); + DisplayDeviceConfig ddc = new DdcBuilder().setBrightnessRange(BACKLIGHT_RANGE_ZERO_TO_ONE) + .build(); // Create an idle mode bms // This will fail if it tries to fetch the wrong configuration. @@ -562,17 +500,11 @@ public class BrightnessMappingStrategyTest { assertEquals(minBrightness, strategy.getBrightness(LUX_LEVELS[0]), 0.0001f /*tolerance*/); } - private Resources createResources(int[] brightnessLevelsBacklight) { - return createResources(brightnessLevelsBacklight, EMPTY_INT_ARRAY, EMPTY_FLOAT_ARRAY); - } - - private Resources createResourcesIdle(int[] luxLevelsIdle, float[] brightnessLevelsNitsIdle) { - return createResources(EMPTY_INT_ARRAY, - luxLevelsIdle, brightnessLevelsNitsIdle); + private Resources createResources() { + return createResources(EMPTY_INT_ARRAY, EMPTY_FLOAT_ARRAY); } - private Resources createResources(int[] brightnessLevelsBacklight, int[] luxLevelsIdle, - float[] brightnessLevelsNitsIdle) { + private Resources createResources(int[] luxLevelsIdle, float[] brightnessLevelsNitsIdle) { Resources mockResources = mock(Resources.class); if (luxLevelsIdle.length > 0) { @@ -583,10 +515,6 @@ public class BrightnessMappingStrategyTest { .thenReturn(luxLevelsIdleResource); } - when(mockResources.getIntArray( - com.android.internal.R.array.config_autoBrightnessLcdBacklightValues)) - .thenReturn(brightnessLevelsBacklight); - TypedArray mockBrightnessLevelNitsIdle = createFloatTypedArray(brightnessLevelsNitsIdle); when(mockResources.obtainTypedArray( com.android.internal.R.array.config_autoBrightnessDisplayValuesNitsIdle)) @@ -604,41 +532,6 @@ public class BrightnessMappingStrategyTest { return mockResources; } - private DisplayDeviceConfig createDdc() { - return createDdc(DISPLAY_RANGE_NITS); - } - - private DisplayDeviceConfig createDdc(float[] nitsArray) { - return createDdc(nitsArray, DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT); - } - - private DisplayDeviceConfig createDdc(float[] nitsArray, float[] backlightArray) { - DisplayDeviceConfig mockDdc = mock(DisplayDeviceConfig.class); - when(mockDdc.getNits()).thenReturn(nitsArray); - when(mockDdc.getBrightness()).thenReturn(backlightArray); - when(mockDdc.getAutoBrightnessBrighteningLevelsLux()).thenReturn(LUX_LEVELS); - when(mockDdc.getAutoBrightnessBrighteningLevelsNits()).thenReturn(EMPTY_FLOAT_ARRAY); - when(mockDdc.getAutoBrightnessBrighteningLevels()).thenReturn(EMPTY_FLOAT_ARRAY); - return mockDdc; - } - - private DisplayDeviceConfig createDdc(float[] nitsArray, float[] backlightArray, - float[] luxLevelsFloat, float[] brightnessLevelsNits) { - return createDdc(nitsArray, backlightArray, luxLevelsFloat, brightnessLevelsNits, - EMPTY_FLOAT_ARRAY); - } - - private DisplayDeviceConfig createDdc(float[] nitsArray, float[] backlightArray, - float[] luxLevelsFloat, float[] brightnessLevelsNits, float[] brightnessLevels) { - DisplayDeviceConfig mockDdc = mock(DisplayDeviceConfig.class); - when(mockDdc.getNits()).thenReturn(nitsArray); - when(mockDdc.getBrightness()).thenReturn(backlightArray); - when(mockDdc.getAutoBrightnessBrighteningLevelsLux()).thenReturn(luxLevelsFloat); - when(mockDdc.getAutoBrightnessBrighteningLevelsNits()).thenReturn(brightnessLevelsNits); - when(mockDdc.getAutoBrightnessBrighteningLevels()).thenReturn(brightnessLevels); - return mockDdc; - } - private TypedArray createFloatTypedArray(float[] vals) { TypedArray mockArray = mock(TypedArray.class); when(mockArray.length()).thenAnswer(invocation -> { @@ -677,10 +570,9 @@ public class BrightnessMappingStrategyTest { final float y2 = GAMMA_CORRECTION_SPLINE.interpolate(x2); final float y3 = GAMMA_CORRECTION_SPLINE.interpolate(x3); - Resources resources = createResources(EMPTY_INT_ARRAY); - DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, - DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX, - GAMMA_CORRECTION_NITS); + Resources resources = createResources(); + DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsLux(GAMMA_CORRECTION_LUX) + .setAutoBrightnessLevelsNits(GAMMA_CORRECTION_NITS).build(); BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); // Let's start with a validity check: @@ -708,10 +600,9 @@ public class BrightnessMappingStrategyTest { final float y1 = GAMMA_CORRECTION_SPLINE.interpolate(x1); final float y2 = GAMMA_CORRECTION_SPLINE.interpolate(x2); final float y3 = GAMMA_CORRECTION_SPLINE.interpolate(x3); - Resources resources = createResources(EMPTY_INT_ARRAY); - DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, - DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX, - GAMMA_CORRECTION_NITS); + Resources resources = createResources(); + DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsLux(GAMMA_CORRECTION_LUX) + .setAutoBrightnessLevelsNits(GAMMA_CORRECTION_NITS).build(); BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); // Validity check: @@ -736,10 +627,9 @@ public class BrightnessMappingStrategyTest { public void testGammaCorrectionExtremeChangeAtCenter() { // Extreme changes (e.g. setting brightness to 0.0 or 1.0) can't be gamma corrected, so we // just make sure the adjustment reflects the change. - Resources resources = createResources(EMPTY_INT_ARRAY); - DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, - DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX, - GAMMA_CORRECTION_NITS); + Resources resources = createResources(); + DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsLux(GAMMA_CORRECTION_LUX) + .setAutoBrightnessLevelsNits(GAMMA_CORRECTION_NITS).build(); BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); assertEquals(0.0f, strategy.getAutoBrightnessAdjustment(), /* delta= */ 0.0001f); @@ -760,10 +650,9 @@ public class BrightnessMappingStrategyTest { final float y0 = GAMMA_CORRECTION_SPLINE.interpolate(x0); final float y2 = GAMMA_CORRECTION_SPLINE.interpolate(x2); final float y4 = GAMMA_CORRECTION_SPLINE.interpolate(x4); - Resources resources = createResources(EMPTY_INT_ARRAY); - DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, - DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX, - GAMMA_CORRECTION_NITS); + Resources resources = createResources(); + DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsLux(GAMMA_CORRECTION_LUX) + .setAutoBrightnessLevelsNits(GAMMA_CORRECTION_NITS).build(); BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc); // Validity, as per tradition: @@ -790,11 +679,54 @@ public class BrightnessMappingStrategyTest { @Test public void testGetMode() { - Resources res = createResourcesIdle(LUX_LEVELS_IDLE, DISPLAY_LEVELS_NITS_IDLE); - DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE); + Resources res = createResources(LUX_LEVELS_IDLE, DISPLAY_LEVELS_NITS_IDLE); + DisplayDeviceConfig ddc = new DdcBuilder().setBrightnessRange(BACKLIGHT_RANGE_ZERO_TO_ONE) + .build(); BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_IDLE, mMockDwbc); assertEquals(AUTO_BRIGHTNESS_MODE_IDLE, strategy.getMode()); } + + private static class DdcBuilder { + private DisplayDeviceConfig mDdc; + + DdcBuilder() { + mDdc = mock(DisplayDeviceConfig.class); + when(mDdc.getNits()).thenReturn(DISPLAY_RANGE_NITS); + when(mDdc.getBrightness()).thenReturn(DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT); + when(mDdc.getAutoBrightnessBrighteningLevelsLux()).thenReturn(LUX_LEVELS); + when(mDdc.getAutoBrightnessBrighteningLevelsNits()).thenReturn(EMPTY_FLOAT_ARRAY); + when(mDdc.getAutoBrightnessBrighteningLevels()).thenReturn(EMPTY_FLOAT_ARRAY); + } + + DdcBuilder setNitsRange(float[] nitsArray) { + when(mDdc.getNits()).thenReturn(nitsArray); + return this; + } + + DdcBuilder setBrightnessRange(float[] brightnessArray) { + when(mDdc.getBrightness()).thenReturn(brightnessArray); + return this; + } + + DdcBuilder setAutoBrightnessLevelsLux(float[] luxLevels) { + when(mDdc.getAutoBrightnessBrighteningLevelsLux()).thenReturn(luxLevels); + return this; + } + + DdcBuilder setAutoBrightnessLevelsNits(float[] brightnessLevelsNits) { + when(mDdc.getAutoBrightnessBrighteningLevelsNits()).thenReturn(brightnessLevelsNits); + return this; + } + + DdcBuilder setAutoBrightnessLevels(float[] brightnessLevels) { + when(mDdc.getAutoBrightnessBrighteningLevels()).thenReturn(brightnessLevels); + return this; + } + + DisplayDeviceConfig build() { + return mDdc; + } + } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java index 31d7e88e671b..a4c15b5c5725 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -17,6 +17,7 @@ package com.android.server.display; +import static com.android.internal.display.BrightnessSynchronizer.brightnessIntToFloat; import static com.android.server.display.config.SensorData.SupportedMode; import static com.android.server.display.utils.DeviceConfigParsingUtils.ambientBrightnessThresholdsIntToFloat; import static com.android.server.display.utils.DeviceConfigParsingUtils.displayBrightnessThresholdsIntToFloat; @@ -47,7 +48,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.internal.R; -import com.android.internal.display.BrightnessSynchronizer; import com.android.server.display.config.HdrBrightnessData; import com.android.server.display.config.ThermalStatus; import com.android.server.display.feature.DisplayManagerFlags; @@ -609,6 +609,9 @@ public final class DisplayDeviceConfigTest { float[]{2.0f, 200.0f, 600.0f}, ZERO_DELTA); assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new float[]{0.0f, 110.0f, 500.0f}, ZERO_DELTA); + assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(), new + float[]{brightnessIntToFloat(50), brightnessIntToFloat(100), + brightnessIntToFloat(150)}, SMALL_DELTA); // Test thresholds assertEquals(0, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(), ZERO_DELTA); @@ -674,7 +677,7 @@ public final class DisplayDeviceConfigTest { assertEquals("test_light_sensor", mDisplayDeviceConfig.getAmbientLightSensor().type); assertEquals("", mDisplayDeviceConfig.getAmbientLightSensor().name); - assertEquals(BrightnessSynchronizer.brightnessIntToFloat(35), + assertEquals(brightnessIntToFloat(35), mDisplayDeviceConfig.getBrightnessCapForWearBedtimeMode(), ZERO_DELTA); } @@ -737,6 +740,27 @@ public final class DisplayDeviceConfigTest { mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), ZERO_DELTA); assertArrayEquals(new float[]{0.2f, 0.3f}, mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(), SMALL_DELTA); + + assertArrayEquals(new float[]{0.0f, 90}, + mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux("default", "dim"), + ZERO_DELTA); + assertArrayEquals(new float[]{0.3f, 0.4f}, + mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels("default", "dim"), + SMALL_DELTA); + + assertArrayEquals(new float[]{0.0f, 95}, + mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux("doze", "normal"), + ZERO_DELTA); + assertArrayEquals(new float[]{0.35f, 0.45f}, + mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels("doze", "normal"), + SMALL_DELTA); + + assertArrayEquals(new float[]{0.0f, 100}, + mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux("doze", "bright"), + ZERO_DELTA); + assertArrayEquals(new float[]{0.4f, 0.5f}, + mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels("doze", "bright"), + SMALL_DELTA); } @Test @@ -746,7 +770,9 @@ public final class DisplayDeviceConfigTest { setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(), getValidProxSensor(), /* includeIdleMode= */ false)); - assertNull(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels()); + assertArrayEquals(new float[]{brightnessIntToFloat(50), brightnessIntToFloat(100), + brightnessIntToFloat(150)}, + mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(), SMALL_DELTA); assertArrayEquals(new float[]{0, 110, 500}, mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), ZERO_DELTA); assertArrayEquals(new float[]{2, 200, 600}, @@ -1138,6 +1164,46 @@ public final class DisplayDeviceConfigTest { + "</point>\n" + "</map>\n" + "</luxToBrightnessMapping>\n" + + "<luxToBrightnessMapping>\n" + + "<setting>dim</setting>\n" + + "<map>\n" + + "<point>\n" + + "<first>0</first>\n" + + "<second>0.3</second>\n" + + "</point>\n" + + "<point>\n" + + "<first>90</first>\n" + + "<second>0.4</second>\n" + + "</point>\n" + + "</map>\n" + + "</luxToBrightnessMapping>\n" + + "<luxToBrightnessMapping>\n" + + "<mode>doze</mode>\n" + + "<map>\n" + + "<point>\n" + + "<first>0</first>\n" + + "<second>0.35</second>\n" + + "</point>\n" + + "<point>\n" + + "<first>95</first>\n" + + "<second>0.45</second>\n" + + "</point>\n" + + "</map>\n" + + "</luxToBrightnessMapping>\n" + + "<luxToBrightnessMapping>\n" + + "<mode>doze</mode>\n" + + "<setting>bright</setting>\n" + + "<map>\n" + + "<point>\n" + + "<first>0</first>\n" + + "<second>0.4</second>\n" + + "</point>\n" + + "<point>\n" + + "<first>100</first>\n" + + "<second>0.5</second>\n" + + "</point>\n" + + "</map>\n" + + "</luxToBrightnessMapping>\n" + "</autoBrightness>\n" + getPowerThrottlingConfig() + "<highBrightnessMode enabled=\"true\">\n" @@ -1435,6 +1501,10 @@ public final class DisplayDeviceConfigTest { when(mResources.getIntArray( com.android.internal.R.array.config_autoBrightnessLevels)) .thenReturn(screenBrightnessLevelLux); + int[] screenBrightnessLevels = new int[]{50, 100, 150}; + when(mResources.getIntArray( + com.android.internal.R.array.config_autoBrightnessLcdBacklightValues)) + .thenReturn(screenBrightnessLevels); // Thresholds // Config.xml requires the levels arrays to be of length N and the thresholds arrays to be diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java index dea838d3763d..fbb14c3db9f9 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java @@ -19,6 +19,7 @@ package com.android.server.display; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -88,4 +89,12 @@ public class DisplayOffloadSessionImplTest { verify(mDisplayPowerController).setBrightnessFromOffload(brightness); } + + @Test + public void testBlockScreenOn() { + Runnable unblocker = () -> {}; + mSession.blockScreenOn(unblocker); + + verify(mDisplayOffloader).onBlockingScreenOn(eq(unblocker)); + } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java index f36854b1ea78..00f98924eff3 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -206,6 +206,9 @@ public class LocalDisplayAdapterTest { when(mMockedResources.getIntArray( com.android.internal.R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate)) .thenReturn(new int[]{}); + when(mMockedResources.getIntArray( + com.android.internal.R.array.config_autoBrightnessLcdBacklightValues)) + .thenReturn(new int[]{}); doReturn(true).when(mFlags).isDisplayOffloadEnabled(); initDisplayOffloadSession(); } @@ -1235,6 +1238,9 @@ public class LocalDisplayAdapterTest { @Override public void stopOffload() {} + + @Override + public void onBlockingScreenOn(Runnable unblocker) {} }); mDisplayOffloadSession = new DisplayOffloadSessionImpl(mDisplayOffloader, diff --git a/services/tests/media/OWNERS b/services/tests/media/OWNERS new file mode 100644 index 000000000000..160767a6427c --- /dev/null +++ b/services/tests/media/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 137631 +include platform/frameworks/av:/media/janitors/media_solutions_OWNERS diff --git a/services/tests/media/mediarouterservicetest/Android.bp b/services/tests/media/mediarouterservicetest/Android.bp new file mode 100644 index 000000000000..aed3af6b69f6 --- /dev/null +++ b/services/tests/media/mediarouterservicetest/Android.bp @@ -0,0 +1,39 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "MediaRouterServiceTests", + srcs: [ + "src/**/*.java", + ], + + static_libs: [ + "androidx.test.core", + "androidx.test.rules", + "androidx.test.runner", + "compatibility-device-util-axt", + "junit", + "platform-test-annotations", + "services.core", + "truth", + ], + + platform_apis: true, + + test_suites: [ + // "device-tests", + "general-tests", + ], + + certificate: "platform", + dxflags: ["--multi-dex"], + optimize: { + enabled: false, + }, +} diff --git a/services/tests/media/mediarouterservicetest/AndroidManifest.xml b/services/tests/media/mediarouterservicetest/AndroidManifest.xml new file mode 100644 index 000000000000..fe65f868bcb3 --- /dev/null +++ b/services/tests/media/mediarouterservicetest/AndroidManifest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.server.media.tests"> + + <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/> + <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" /> + + <application android:testOnly="true" android:debuggable="true"> + <uses-library android:name="android.test.runner"/> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.server.media.tests" + android:label="Frameworks Services Tests"/> +</manifest> diff --git a/services/tests/media/mediarouterservicetest/AndroidTest.xml b/services/tests/media/mediarouterservicetest/AndroidTest.xml new file mode 100644 index 000000000000..b0656816b701 --- /dev/null +++ b/services/tests/media/mediarouterservicetest/AndroidTest.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 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. +--> +<configuration description="Runs MediaRouter Service tests."> + <option name="test-tag" value="MediaRouterServiceTests" /> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-instrumentation" /> + + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> + + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true"/> + <option name="test-file-name" value="MediaRouterServiceTests.apk"/> + <option name="install-arg" value="-t" /> + </target_preparer> + + <test class="com.android.tradefed.testtype.InstrumentationTest" > + <option name="package" value="com.android.server.media.tests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java b/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java new file mode 100644 index 000000000000..6f9b6faa0bb0 --- /dev/null +++ b/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 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. + */ + +package com.android.server.media; + +import static com.android.server.media.AudioRoutingUtils.ATTRIBUTES_MEDIA; +import static com.android.server.media.AudioRoutingUtils.getMediaAudioProductStrategy; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothManager; +import android.content.Context; +import android.content.res.Resources; +import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceCallback; +import android.media.AudioDeviceInfo; +import android.media.AudioDevicePort; +import android.media.AudioManager; +import android.media.AudioSystem; +import android.media.MediaRoute2Info; +import android.media.audiopolicy.AudioProductStrategy; +import android.os.Looper; +import android.os.UserHandle; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@RunWith(JUnit4.class) +public class AudioPoliciesDeviceRouteControllerTest { + + private static final String FAKE_ROUTE_NAME = "fake name"; + private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER = + createAudioDeviceInfo( + AudioSystem.DEVICE_OUT_SPEAKER, "name_builtin", /* address= */ null); + private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET = + createAudioDeviceInfo( + AudioSystem.DEVICE_OUT_WIRED_HEADSET, "name_wired_hs", /* address= */ null); + private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP = + createAudioDeviceInfo( + AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "name_a2dp", /* address= */ "12:34:45"); + + private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_BUILTIN_EARPIECE = + createAudioDeviceInfo( + AudioSystem.DEVICE_OUT_EARPIECE, /* name= */ null, /* address= */ null); + + private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_NO_NAME = + createAudioDeviceInfo( + AudioSystem.DEVICE_OUT_DGTL_DOCK_HEADSET, + /* name= */ null, + /* address= */ null); + + private AudioDeviceInfo mSelectedAudioDeviceInfo; + private Set<AudioDeviceInfo> mAvailableAudioDeviceInfos; + @Mock private AudioManager mMockAudioManager; + @Mock private DeviceRouteController.OnDeviceRouteChangedListener mOnDeviceRouteChangedListener; + private AudioPoliciesDeviceRouteController mControllerUnderTest; + private AudioDeviceCallback mAudioDeviceCallback; + private AudioProductStrategy mMediaAudioProductStrategy; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + Resources mockResources = Mockito.mock(Resources.class); + when(mockResources.getText(anyInt())).thenReturn(FAKE_ROUTE_NAME); + Context realContext = InstrumentationRegistry.getInstrumentation().getContext(); + Context mockContext = Mockito.mock(Context.class); + when(mockContext.getResources()).thenReturn(mockResources); + // The bluetooth stack needs the application info, but we cannot use a spy because the + // concrete class is package private, so we just return the application info through the + // mock. + when(mockContext.getApplicationInfo()).thenReturn(realContext.getApplicationInfo()); + + // Setup the initial state so that the route controller is created in a sensible state. + mSelectedAudioDeviceInfo = FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER; + mAvailableAudioDeviceInfos = Set.of(FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER); + updateMockAudioManagerState(); + mMediaAudioProductStrategy = getMediaAudioProductStrategy(); + + BluetoothAdapter btAdapter = + realContext.getSystemService(BluetoothManager.class).getAdapter(); + mControllerUnderTest = + new AudioPoliciesDeviceRouteController( + mockContext, + mMockAudioManager, + Looper.getMainLooper(), + mMediaAudioProductStrategy, + btAdapter, + mOnDeviceRouteChangedListener); + mControllerUnderTest.start(UserHandle.CURRENT_OR_SELF); + + ArgumentCaptor<AudioDeviceCallback> deviceCallbackCaptor = + ArgumentCaptor.forClass(AudioDeviceCallback.class); + verify(mMockAudioManager) + .registerAudioDeviceCallback(deviceCallbackCaptor.capture(), any()); + mAudioDeviceCallback = deviceCallbackCaptor.getValue(); + + // We clear any invocations during setup. + clearInvocations(mOnDeviceRouteChangedListener); + } + + @Test + public void getSelectedRoute_afterDevicesConnect_returnsRightSelectedRoute() { + assertThat(mControllerUnderTest.getSelectedRoute().getType()) + .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER); + + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP, + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP); + verify(mOnDeviceRouteChangedListener).onDeviceRouteChanged(); + assertThat(mControllerUnderTest.getSelectedRoute().getType()) + .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP); + + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ null, // Selected device doesn't change. + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET); + assertThat(mControllerUnderTest.getSelectedRoute().getType()) + .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP); + } + + @Test + public void getSelectedRoute_afterDeviceRemovals_returnsExpectedRoutes() { + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET, + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP, + FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET); + verify(mOnDeviceRouteChangedListener).onDeviceRouteChanged(); + + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP, + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP); + verify(mOnDeviceRouteChangedListener, times(2)).onDeviceRouteChanged(); + assertThat(mControllerUnderTest.getSelectedRoute().getType()) + .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP); + + removeAvailableAudioDeviceInfos( + /* newSelectedDevice= */ null, + /* devicesToRemove...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET); + assertThat(mControllerUnderTest.getSelectedRoute().getType()) + .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP); + + removeAvailableAudioDeviceInfos( + /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER, + /* devicesToRemove...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET); + assertThat(mControllerUnderTest.getSelectedRoute().getType()) + .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER); + } + + @Test + public void onAudioDevicesAdded_clearsAudioRoutingPoliciesCorrectly() { + clearInvocations(mMockAudioManager); + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ null, // Selected device doesn't change. + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_BUILTIN_EARPIECE); + verifyNoMoreInteractions(mMockAudioManager); + + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET, + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP); + verify(mMockAudioManager).removePreferredDeviceForStrategy(mMediaAudioProductStrategy); + } + + @Test + public void getAvailableDevices_ignoresInvalidMediaOutputs() { + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ null, // Selected device doesn't change. + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_BUILTIN_EARPIECE); + verifyNoMoreInteractions(mOnDeviceRouteChangedListener); + assertThat( + mControllerUnderTest.getAvailableRoutes().stream() + .map(MediaRoute2Info::getType) + .toList()) + .containsExactly(MediaRoute2Info.TYPE_BUILTIN_SPEAKER); + assertThat(mControllerUnderTest.getSelectedRoute().getType()) + .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER); + } + + @Test + public void transferTo_setsTheExpectedRoutingPolicy() { + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET, + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP, + FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET); + MediaRoute2Info builtInSpeakerRoute = + getAvailableRouteWithType(MediaRoute2Info.TYPE_BUILTIN_SPEAKER); + mControllerUnderTest.transferTo(builtInSpeakerRoute.getId()); + verify(mMockAudioManager) + .setPreferredDeviceForStrategy( + mMediaAudioProductStrategy, + createAudioDeviceAttribute(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)); + + MediaRoute2Info wiredHeadsetRoute = + getAvailableRouteWithType(MediaRoute2Info.TYPE_WIRED_HEADSET); + mControllerUnderTest.transferTo(wiredHeadsetRoute.getId()); + verify(mMockAudioManager) + .setPreferredDeviceForStrategy( + mMediaAudioProductStrategy, + createAudioDeviceAttribute(AudioDeviceInfo.TYPE_WIRED_HEADSET)); + } + + @Test + public void updateVolume_propagatesCorrectlyToRouteInfo() { + when(mMockAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC)).thenReturn(2); + when(mMockAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)).thenReturn(3); + when(mMockAudioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC)).thenReturn(1); + when(mMockAudioManager.isVolumeFixed()).thenReturn(false); + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET, + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET); + + MediaRoute2Info selectedRoute = mControllerUnderTest.getSelectedRoute(); + assertThat(selectedRoute.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADSET); + assertThat(selectedRoute.getVolume()).isEqualTo(2); + assertThat(selectedRoute.getVolumeMax()).isEqualTo(3); + assertThat(selectedRoute.getVolumeHandling()) + .isEqualTo(MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE); + + MediaRoute2Info onlyTransferrableRoute = + mControllerUnderTest.getAvailableRoutes().stream() + .filter(it -> !it.equals(selectedRoute)) + .findAny() + .orElseThrow(); + assertThat(onlyTransferrableRoute.getType()) + .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER); + assertThat(onlyTransferrableRoute.getVolume()).isEqualTo(0); + assertThat(onlyTransferrableRoute.getVolumeMax()).isEqualTo(0); + assertThat(onlyTransferrableRoute.getVolume()).isEqualTo(0); + assertThat(onlyTransferrableRoute.getVolumeHandling()) + .isEqualTo(MediaRoute2Info.PLAYBACK_VOLUME_FIXED); + + when(mMockAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC)).thenReturn(0); + when(mMockAudioManager.isVolumeFixed()).thenReturn(true); + mControllerUnderTest.updateVolume(0); + MediaRoute2Info newSelectedRoute = mControllerUnderTest.getSelectedRoute(); + assertThat(newSelectedRoute.getVolume()).isEqualTo(0); + assertThat(newSelectedRoute.getVolumeHandling()) + .isEqualTo(MediaRoute2Info.PLAYBACK_VOLUME_FIXED); + } + + @Test + public void getAvailableRoutes_whenNoProductNameIsProvided_usesTypeToPopulateName() { + assertThat(mControllerUnderTest.getSelectedRoute().getName().toString()) + .isEqualTo(FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER.getProductName().toString()); + + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_NO_NAME, + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_NO_NAME); + + MediaRoute2Info selectedRoute = mControllerUnderTest.getSelectedRoute(); + assertThat(selectedRoute.getName().toString()).isEqualTo(FAKE_ROUTE_NAME); + } + + // Internal methods. + + @NonNull + private MediaRoute2Info getAvailableRouteWithType(int type) { + return mControllerUnderTest.getAvailableRoutes().stream() + .filter(it -> it.getType() == type) + .findFirst() + .orElseThrow(); + } + + private void addAvailableAudioDeviceInfo( + @Nullable AudioDeviceInfo newSelectedDevice, AudioDeviceInfo... newAvailableDevices) { + Set<AudioDeviceInfo> newAvailableDeviceInfos = new HashSet<>(mAvailableAudioDeviceInfos); + newAvailableDeviceInfos.addAll(List.of(newAvailableDevices)); + mAvailableAudioDeviceInfos = newAvailableDeviceInfos; + if (newSelectedDevice != null) { + mSelectedAudioDeviceInfo = newSelectedDevice; + } + updateMockAudioManagerState(); + mAudioDeviceCallback.onAudioDevicesAdded(newAvailableDevices); + } + + private void removeAvailableAudioDeviceInfos( + @Nullable AudioDeviceInfo newSelectedDevice, AudioDeviceInfo... devicesToRemove) { + Set<AudioDeviceInfo> newAvailableDeviceInfos = new HashSet<>(mAvailableAudioDeviceInfos); + List.of(devicesToRemove).forEach(newAvailableDeviceInfos::remove); + mAvailableAudioDeviceInfos = newAvailableDeviceInfos; + if (newSelectedDevice != null) { + mSelectedAudioDeviceInfo = newSelectedDevice; + } + updateMockAudioManagerState(); + mAudioDeviceCallback.onAudioDevicesRemoved(devicesToRemove); + } + + private void updateMockAudioManagerState() { + when(mMockAudioManager.getDevicesForAttributes(ATTRIBUTES_MEDIA)) + .thenReturn( + List.of(createAudioDeviceAttribute(mSelectedAudioDeviceInfo.getType()))); + when(mMockAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) + .thenReturn(mAvailableAudioDeviceInfos.toArray(new AudioDeviceInfo[0])); + } + + private static AudioDeviceAttributes createAudioDeviceAttribute(int type) { + // Address is unused. + return new AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, type, /* address= */ ""); + } + + private static AudioDeviceInfo createAudioDeviceInfo( + int type, @NonNull String name, @NonNull String address) { + return new AudioDeviceInfo(AudioDevicePort.createForTesting(type, name, address)); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java index 3b83e3cc0817..fc2e5b0981e4 100644 --- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java @@ -40,6 +40,7 @@ import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; @@ -109,6 +110,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.function.Supplier; @RunWith(AndroidJUnit4.class) @@ -126,6 +128,8 @@ public class GameManagerServiceTests { private MockitoSession mMockingSession; private String mPackageName; + private Map<String, Integer> mPackageCategories; + private Map<String, Integer> mPackageUids; private TestLooper mTestLooper; @Mock private PackageManager mMockPackageManager; @@ -229,34 +233,50 @@ public class GameManagerServiceTests { .strictness(Strictness.WARN) .startMocking(); mMockContext = new MockContext(InstrumentationRegistry.getContext()); + mPackageCategories = new HashMap<>(); + mPackageUids = new HashMap<>(); mPackageName = mMockContext.getPackageName(); - mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_GAME); - final Resources resources = - InstrumentationRegistry.getInstrumentation().getContext().getResources(); - when(mMockPackageManager.getResourcesForApplication(anyString())) - .thenReturn(resources); - when(mMockPackageManager.getPackageUidAsUser(mPackageName, USER_ID_1)).thenReturn( - DEFAULT_PACKAGE_UID); + mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_GAME); LocalServices.addService(PowerManagerInternal.class, mMockPowerManager); mSetFlagsRule.enableFlags(Flags.FLAG_GAME_DEFAULT_FRAME_RATE); + mSetFlagsRule.disableFlags(Flags.FLAG_DISABLE_GAME_MODE_WHEN_APP_TOP); } - private void mockAppCategory(String packageName, @ApplicationInfo.Category int category) + private void mockAppCategory(String packageName, int packageUid, + @ApplicationInfo.Category int category) throws Exception { reset(mMockPackageManager); - final ApplicationInfo gameApplicationInfo = new ApplicationInfo(); - gameApplicationInfo.category = category; - gameApplicationInfo.packageName = packageName; - final PackageInfo pi = new PackageInfo(); - pi.packageName = packageName; - pi.applicationInfo = gameApplicationInfo; - final List<PackageInfo> packages = new ArrayList<>(); - packages.add(pi); + mPackageCategories.put(packageName, category); + mPackageUids.put(packageName, packageUid); + final List<PackageInfo> packageInfos = new ArrayList<>(); + for (Map.Entry<String, Integer> entry : mPackageCategories.entrySet()) { + + packageName = entry.getKey(); + packageUid = mPackageUids.get(packageName); + category = entry.getValue(); + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.packageName = packageName; + applicationInfo.category = category; + when(mMockPackageManager.getApplicationInfoAsUser(eq(packageName), anyInt(), anyInt())) + .thenReturn(applicationInfo); + + final PackageInfo pi = new PackageInfo(); + pi.packageName = packageName; + pi.applicationInfo = applicationInfo; + packageInfos.add(pi); + + when(mMockPackageManager.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn( + packageUid); + when(mMockPackageManager.getPackagesForUid(packageUid)).thenReturn( + new String[]{packageName}); + } when(mMockPackageManager.getInstalledPackagesAsUser(anyInt(), anyInt())) - .thenReturn(packages); - when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())) - .thenReturn(gameApplicationInfo); + .thenReturn(packageInfos); + final Resources resources = + InstrumentationRegistry.getInstrumentation().getContext().getResources(); + when(mMockPackageManager.getResourcesForApplication(anyString())) + .thenReturn(resources); } @After @@ -508,7 +528,7 @@ public class GameManagerServiceTests { @Test public void testGetGameMode_nonGame() throws Exception { - mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_AUDIO); + mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_AUDIO); GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1); mockModifyGameModeGranted(); assertEquals(GameManager.GAME_MODE_UNSUPPORTED, @@ -780,7 +800,7 @@ public class GameManagerServiceTests { @Test public void testDeviceConfig_nonGame() throws Exception { - mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_AUDIO); + mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_AUDIO); mockDeviceConfigAll(); mockModifyGameModeGranted(); checkReportedAvailableGameModes(createServiceAndStartUser(USER_ID_1)); @@ -1588,7 +1608,7 @@ public class GameManagerServiceTests { @Test public void testSetGameState_nonGame() throws Exception { - mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_AUDIO); + mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_AUDIO); mockDeviceConfigNone(); mockModifyGameModeGranted(); GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1); @@ -1611,14 +1631,14 @@ public class GameManagerServiceTests { gameManagerService.addGameStateListener(mockListener); verify(binder).linkToDeath(mDeathRecipientCaptor.capture(), anyInt()); - mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_AUDIO); + mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_AUDIO); GameState gameState = new GameState(true, GameState.MODE_NONE); gameManagerService.setGameState(mPackageName, gameState, USER_ID_1); assertFalse(gameManagerService.mHandler.hasMessages(SET_GAME_STATE)); mTestLooper.dispatchAll(); verify(mockListener, never()).onGameStateChanged(anyString(), any(), anyInt()); - mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_GAME); + mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_GAME); gameState = new GameState(true, GameState.MODE_NONE); gameManagerService.setGameState(mPackageName, gameState, USER_ID_1); assertTrue(gameManagerService.mHandler.hasMessages(SET_GAME_STATE)); @@ -1654,7 +1674,7 @@ public class GameManagerServiceTests { gameManagerService.addGameStateListener(mockListener); gameManagerService.removeGameStateListener(mockListener); - mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_GAME); + mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_GAME); GameState gameState = new GameState(false, GameState.MODE_CONTENT); gameManagerService.setGameState(mPackageName, gameState, USER_ID_1); assertTrue(gameManagerService.mHandler.hasMessages(SET_GAME_STATE)); @@ -1670,13 +1690,17 @@ public class GameManagerServiceTests { return output; } - private void mockInterventionListForMultipleUsers() { + private void mockInterventionListForMultipleUsers() throws Exception { final String[] packageNames = new String[]{"com.android.app0", "com.android.app1", "com.android.app2"}; + int i = 1; + for (String p : packageNames) { + mockAppCategory(p, DEFAULT_PACKAGE_UID + i++, ApplicationInfo.CATEGORY_GAME); + } final ApplicationInfo[] applicationInfos = new ApplicationInfo[3]; final PackageInfo[] pis = new PackageInfo[3]; - for (int i = 0; i < 3; ++i) { + for (i = 0; i < 3; ++i) { applicationInfos[i] = new ApplicationInfo(); applicationInfos[i].category = ApplicationInfo.CATEGORY_GAME; applicationInfos[i].packageName = packageNames[i]; @@ -1717,7 +1741,6 @@ public class GameManagerServiceTests { new Injector()); startUser(gameManagerService, USER_ID_1); startUser(gameManagerService, USER_ID_2); - gameManagerService.setGameModeConfigOverride("com.android.app0", USER_ID_2, GameManager.GAME_MODE_PERFORMANCE, "120", "0.6"); gameManagerService.setGameModeConfigOverride("com.android.app2", USER_ID_2, @@ -1953,7 +1976,7 @@ public class GameManagerServiceTests { @Test public void testUpdateCustomGameModeConfiguration_nonGame() throws Exception { - mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_IMAGE); + mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_IMAGE); mockModifyGameModeGranted(); GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1); gameManagerService.updateCustomGameModeConfiguration(mPackageName, @@ -2013,13 +2036,14 @@ public class GameManagerServiceTests { } @Test - public void testWritingSettingFile_onShutdown() throws InterruptedException { + public void testWritingSettingFile_onShutdown() throws Exception { mockModifyGameModeGranted(); mockDeviceConfigAll(); GameManagerService gameManagerService = new GameManagerService(mMockContext); gameManagerService.onBootCompleted(); startUser(gameManagerService, USER_ID_1); Thread.sleep(500); + mockAppCategory("com.android.app1", DEFAULT_PACKAGE_UID + 1, ApplicationInfo.CATEGORY_GAME); gameManagerService.setGameModeConfigOverride("com.android.app1", USER_ID_1, GameManager.GAME_MODE_BATTERY, "60", "0.5"); gameManagerService.setGameMode("com.android.app1", USER_ID_1, @@ -2259,7 +2283,7 @@ public class GameManagerServiceTests { when(DeviceConfig.getProperty(anyString(), anyString())) .thenReturn(configString); mockModifyGameModeGranted(); - mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_IMAGE); + mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_IMAGE); GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1); gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_PERFORMANCE, USER_ID_1); assertEquals(GameManager.GAME_MODE_UNSUPPORTED, @@ -2277,9 +2301,7 @@ public class GameManagerServiceTests { mockModifyGameModeGranted(); GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1); String someGamePkg = "some.game"; - mockAppCategory(someGamePkg, ApplicationInfo.CATEGORY_GAME); - when(mMockPackageManager.getPackageUidAsUser(someGamePkg, USER_ID_1)).thenReturn( - DEFAULT_PACKAGE_UID + 1); + mockAppCategory(someGamePkg, DEFAULT_PACKAGE_UID + 1, ApplicationInfo.CATEGORY_GAME); gameManagerService.setGameMode(someGamePkg, GameManager.GAME_MODE_PERFORMANCE, USER_ID_1); assertEquals(GameManager.GAME_MODE_PERFORMANCE, gameManagerService.getGameMode(someGamePkg, USER_ID_1)); @@ -2307,24 +2329,11 @@ public class GameManagerServiceTests { } @Test - public void testGamePowerMode_gamePackage() throws Exception { - GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1); - String[] packages = {mPackageName}; - when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages); - gameManagerService.mUidObserver.onUidStateChanged( - DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0); - verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true); - } - - @Test public void testGamePowerMode_twoGames() throws Exception { GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1); - String[] packages1 = {mPackageName}; - when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages1); String someGamePkg = "some.game"; - String[] packages2 = {someGamePkg}; int somePackageId = DEFAULT_PACKAGE_UID + 1; - when(mMockPackageManager.getPackagesForUid(somePackageId)).thenReturn(packages2); + mockAppCategory(someGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME); HashMap<Integer, Boolean> powerState = new HashMap<>(); doAnswer(inv -> powerState.put(inv.getArgument(0), inv.getArgument(1))) .when(mMockPowerManager).setPowerMode(anyInt(), anyBoolean()); @@ -2333,6 +2342,7 @@ public class GameManagerServiceTests { assertTrue(powerState.get(Mode.GAME)); gameManagerService.mUidObserver.onUidStateChanged( DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); + assertFalse(powerState.get(Mode.GAME)); gameManagerService.mUidObserver.onUidStateChanged( somePackageId, ActivityManager.PROCESS_STATE_TOP, 0, 0); assertTrue(powerState.get(Mode.GAME)); @@ -2344,12 +2354,9 @@ public class GameManagerServiceTests { @Test public void testGamePowerMode_twoGamesOverlap() throws Exception { GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1); - String[] packages1 = {mPackageName}; - when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages1); String someGamePkg = "some.game"; - String[] packages2 = {someGamePkg}; int somePackageId = DEFAULT_PACKAGE_UID + 1; - when(mMockPackageManager.getPackagesForUid(somePackageId)).thenReturn(packages2); + mockAppCategory(someGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME); gameManagerService.mUidObserver.onUidStateChanged( DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0); gameManagerService.mUidObserver.onUidStateChanged( @@ -2363,49 +2370,162 @@ public class GameManagerServiceTests { } @Test - public void testGamePowerMode_released() throws Exception { + public void testGamePowerMode_noPackage() throws Exception { GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1); - String[] packages = {mPackageName}; + String[] packages = {}; when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages); gameManagerService.mUidObserver.onUidStateChanged( DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0); - gameManagerService.mUidObserver.onUidStateChanged( - DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0); - verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, false); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true); } @Test - public void testGamePowerMode_noPackage() throws Exception { + public void testGamePowerMode_gameAndNotGameApps_flagOn() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_DISABLE_GAME_MODE_WHEN_APP_TOP); GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1); - String[] packages = {}; - when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages); + + String nonGamePkg1 = "not.game1"; + int nonGameUid1 = DEFAULT_PACKAGE_UID + 1; + mockAppCategory(nonGamePkg1, nonGameUid1, ApplicationInfo.CATEGORY_IMAGE); + + String nonGamePkg2 = "not.game2"; + int nonGameUid2 = DEFAULT_PACKAGE_UID + 2; + mockAppCategory(nonGamePkg2, nonGameUid2, ApplicationInfo.CATEGORY_IMAGE); + + String gamePkg1 = "game1"; + int gameUid1 = DEFAULT_PACKAGE_UID + 3; + mockAppCategory(gamePkg1, gameUid1, ApplicationInfo.CATEGORY_GAME); + + String gamePkg2 = "game2"; + int gameUid2 = DEFAULT_PACKAGE_UID + 4; + mockAppCategory(gamePkg2, gameUid2, ApplicationInfo.CATEGORY_GAME); + + // non-game1 top and background with no-op gameManagerService.mUidObserver.onUidStateChanged( - DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0); - verify(mMockPowerManager, times(0)).setPowerMode(Mode.GAME, true); - } + nonGameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0); + gameManagerService.mUidObserver.onUidStateChanged( + nonGameUid1, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false); + clearInvocations(mMockPowerManager); - @Test - public void testGamePowerMode_notAGamePackage() throws Exception { - mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_IMAGE); - GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1); - String[] packages = {"someapp"}; - when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages); + // game1 top to enable game mode gameManagerService.mUidObserver.onUidStateChanged( - DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0); - verify(mMockPowerManager, times(0)).setPowerMode(Mode.GAME, true); + gameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0); + verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false); + clearInvocations(mMockPowerManager); + + // non-game1 in foreground to disable game mode + gameManagerService.mUidObserver.onUidStateChanged( + nonGameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true); + verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, false); + clearInvocations(mMockPowerManager); + + // non-game2 in foreground with no-op + gameManagerService.mUidObserver.onUidStateChanged( + nonGameUid2, ActivityManager.PROCESS_STATE_TOP, 0, 0); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false); + clearInvocations(mMockPowerManager); + + // game 2 in foreground with no-op + gameManagerService.mUidObserver.onUidStateChanged( + gameUid2, ActivityManager.PROCESS_STATE_TOP, 0, 0); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false); + clearInvocations(mMockPowerManager); + + // non-game 1 in background with no-op + gameManagerService.mUidObserver.onUidStateChanged( + nonGameUid1, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false); + clearInvocations(mMockPowerManager); + + // non-game2 in background to resume game mode + gameManagerService.mUidObserver.onUidStateChanged( + nonGameUid2, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); + verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false); + clearInvocations(mMockPowerManager); + + // game 1 in background with no-op + gameManagerService.mUidObserver.onUidStateChanged( + gameUid1, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false); + clearInvocations(mMockPowerManager); + + // game 2 in background to stop game mode + gameManagerService.mUidObserver.onUidStateChanged( + gameUid2, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true); + verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, false); + clearInvocations(mMockPowerManager); } @Test - public void testGamePowerMode_notAGamePackageNotReleased() throws Exception { - mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_IMAGE); + public void testGamePowerMode_gameAndNotGameApps_flagOff() throws Exception { + mSetFlagsRule.disableFlags(Flags.FLAG_DISABLE_GAME_MODE_WHEN_APP_TOP); GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1); - String[] packages = {"someapp"}; - when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages); + + String nonGamePkg1 = "not.game1"; + int nonGameUid1 = DEFAULT_PACKAGE_UID + 1; + mockAppCategory(nonGamePkg1, nonGameUid1, ApplicationInfo.CATEGORY_IMAGE); + + String gamePkg1 = "game1"; + int gameUid1 = DEFAULT_PACKAGE_UID + 3; + mockAppCategory(gamePkg1, gameUid1, ApplicationInfo.CATEGORY_GAME); + + // non-game1 top and background with no-op gameManagerService.mUidObserver.onUidStateChanged( - DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0); + nonGameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0); gameManagerService.mUidObserver.onUidStateChanged( - DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); - verify(mMockPowerManager, times(0)).setPowerMode(Mode.GAME, false); + nonGameUid1, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false); + clearInvocations(mMockPowerManager); + + // game1 top to enable game mode + gameManagerService.mUidObserver.onUidStateChanged( + gameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0); + verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false); + clearInvocations(mMockPowerManager); + + // non-game1 in foreground to not interfere + gameManagerService.mUidObserver.onUidStateChanged( + nonGameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false); + clearInvocations(mMockPowerManager); + + // non-game 1 in background to not interfere + gameManagerService.mUidObserver.onUidStateChanged( + nonGameUid1, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false); + clearInvocations(mMockPowerManager); + + // move non-game1 to foreground again + gameManagerService.mUidObserver.onUidStateChanged( + nonGameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0); + + // with non-game1 on top, game 1 in background to still disable game mode + gameManagerService.mUidObserver.onUidStateChanged( + gameUid1, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true); + verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, false); + clearInvocations(mMockPowerManager); + + // with non-game1 on top, game 1 in foreground to still enable game mode + gameManagerService.mUidObserver.onUidStateChanged( + gameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0); + verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true); + verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false); + clearInvocations(mMockPowerManager); } @Test @@ -2432,8 +2552,6 @@ public class GameManagerServiceTests { gameManagerService.onBootCompleted(); // Set up a game in the foreground. - String[] packages = {mPackageName}; - when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages); gameManagerService.mUidObserver.onUidStateChanged( DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0); @@ -2448,10 +2566,8 @@ public class GameManagerServiceTests { // Adding another game to the foreground. String anotherGamePkg = "another.game"; - String[] packages2 = {anotherGamePkg}; - mockAppCategory(anotherGamePkg, ApplicationInfo.CATEGORY_GAME); int somePackageId = DEFAULT_PACKAGE_UID + 1; - when(mMockPackageManager.getPackagesForUid(somePackageId)).thenReturn(packages2); + mockAppCategory(anotherGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME); gameManagerService.mUidObserver.onUidStateChanged( somePackageId, ActivityManager.PROCESS_STATE_TOP, 0, 0); @@ -2484,8 +2600,6 @@ public class GameManagerServiceTests { })); // Set up a game in the foreground. - String[] packages = {mPackageName}; - when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages); gameManagerService.mUidObserver.onUidStateChanged( DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0); @@ -2503,10 +2617,8 @@ public class GameManagerServiceTests { // Toggle game default frame rate off. String anotherGamePkg = "another.game"; - String[] packages2 = {anotherGamePkg}; - mockAppCategory(anotherGamePkg, ApplicationInfo.CATEGORY_GAME); int somePackageId = DEFAULT_PACKAGE_UID + 1; - when(mMockPackageManager.getPackagesForUid(somePackageId)).thenReturn(packages2); + mockAppCategory(anotherGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME); gameManagerService.mUidObserver.onUidStateChanged( somePackageId, ActivityManager.PROCESS_STATE_TOP, 0, 0); gameManagerService.mUidObserver.onUidStateChanged( diff --git a/services/tests/servicestests/src/com/android/server/audio/FadeConfigurationsTest.java b/services/tests/servicestests/src/com/android/server/audio/FadeConfigurationsTest.java index 6fca56134393..69817ad5035a 100644 --- a/services/tests/servicestests/src/com/android/server/audio/FadeConfigurationsTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/FadeConfigurationsTest.java @@ -16,17 +16,21 @@ package com.android.server.audio; -import static android.media.AudioAttributes.USAGE_MEDIA; -import static android.media.AudioAttributes.USAGE_GAME; -import static android.media.AudioAttributes.USAGE_ASSISTANT; import static android.media.AudioAttributes.CONTENT_TYPE_SPEECH; +import static android.media.AudioAttributes.USAGE_ASSISTANT; +import static android.media.AudioAttributes.USAGE_EMERGENCY; +import static android.media.AudioAttributes.USAGE_GAME; +import static android.media.AudioAttributes.USAGE_MEDIA; import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_AAUDIO; import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL; import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_JAM_AUDIOTRACK; import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_UNKNOWN; +import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION; import android.media.AudioAttributes; +import android.media.FadeManagerConfiguration; import android.media.VolumeShaper; +import android.platform.test.flag.junit.SetFlagsRule; import com.google.common.truth.Expect; @@ -42,6 +46,7 @@ import java.util.List; public final class FadeConfigurationsTest { private FadeConfigurations mFadeConfigurations; private static final long DEFAULT_FADE_OUT_DURATION_MS = 2_000; + private static final long DEFAULT_FADE_IN_DURATION_MS = 1_000; private static final long DEFAULT_DELAY_FADE_IN_OFFENDERS_MS = 2000; private static final long DURATION_FOR_UNFADEABLE_MS = 0; private static final int TEST_UID_SYSTEM = 1000; @@ -60,11 +65,19 @@ public final class FadeConfigurationsTest { private static final VolumeShaper.Configuration DEFAULT_FADEOUT_VSHAPE = new VolumeShaper.Configuration.Builder() .setId(PlaybackActivityMonitor.VOLUME_SHAPER_SYSTEM_FADEOUT_ID) - .setCurve(/* times= */new float[]{0.f, 0.25f, 1.0f} , - /* volumes= */new float[]{1.f, 0.65f, 0.0f}) + .setCurve(/* times= */ new float[]{0.f, 0.25f, 1.0f}, + /* volumes= */ new float[]{1.f, 0.65f, 0.0f}) .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME) .setDuration(DEFAULT_FADE_OUT_DURATION_MS) .build(); + private static final VolumeShaper.Configuration DEFAULT_FADEIN_VSHAPE = + new VolumeShaper.Configuration.Builder() + .setId(PlaybackActivityMonitor.VOLUME_SHAPER_SYSTEM_FADEOUT_ID) + .setCurve(/* times= */ new float[]{0.f, 0.50f, 1.0f}, + /* volumes= */ new float[]{0.f, 0.30f, 1.0f}) + .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME) + .setDuration(DEFAULT_FADE_IN_DURATION_MS) + .build(); private static final AudioAttributes TEST_MEDIA_AUDIO_ATTRIBUTE = new AudioAttributes.Builder().setUsage(USAGE_MEDIA).build(); @@ -72,12 +85,18 @@ public final class FadeConfigurationsTest { new AudioAttributes.Builder().setUsage(USAGE_GAME).build(); private static final AudioAttributes TEST_ASSISTANT_AUDIO_ATTRIBUTE = new AudioAttributes.Builder().setUsage(USAGE_ASSISTANT).build(); + private static final AudioAttributes TEST_EMERGENCY_AUDIO_ATTRIBUTE = + new AudioAttributes.Builder().setSystemUsage(USAGE_EMERGENCY).build(); + private static final AudioAttributes TEST_SPEECH_AUDIO_ATTRIBUTE = new AudioAttributes.Builder().setContentType(CONTENT_TYPE_SPEECH).build(); @Rule public final Expect expect = Expect.create(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Before public void setUp() { mFadeConfigurations = new FadeConfigurations(); @@ -156,4 +175,110 @@ public final class FadeConfigurationsTest { .that(mFadeConfigurations.isFadeable(TEST_GAME_AUDIO_ATTRIBUTE, TEST_UID_USER, PLAYER_TYPE_AAUDIO)).isFalse(); } + + @Test + public void testGetFadeableUsages_withFadeManagerConfigurations_equals() { + mSetFlagsRule.enableFlags(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION); + List<Integer> usageList = List.of(AudioAttributes.USAGE_ALARM, + AudioAttributes.USAGE_EMERGENCY); + FadeManagerConfiguration fmc = createFadeMgrConfig(/* fadeableUsages= */ usageList, + /* unfadeableContentTypes= */ null, /* unfadeableUids= */ null, + /* unfadeableAudioAttrs= */ null); + FadeConfigurations fadeConfigs = new FadeConfigurations(); + + fadeConfigs.setFadeManagerConfiguration(fmc); + + expect.withMessage("Fadeable usages with fade manager configuration") + .that(fadeConfigs.getFadeableUsages()).isEqualTo(fmc.getFadeableUsages()); + } + + @Test + public void testGetUnfadeableContentTypes_withFadeManagerConfigurations_equals() { + mSetFlagsRule.enableFlags(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION); + List<Integer> contentTypesList = List.of(AudioAttributes.CONTENT_TYPE_MUSIC, + AudioAttributes.CONTENT_TYPE_MOVIE); + FadeManagerConfiguration fmc = createFadeMgrConfig(/* fadeableUsages= */ null, + /* unfadeableContentTypes= */ contentTypesList, /* unfadeableUids= */ null, + /* unfadeableAudioAttrs= */ null); + FadeConfigurations fadeConfigs = new FadeConfigurations(); + + fadeConfigs.setFadeManagerConfiguration(fmc); + + expect.withMessage("Unfadeable content types with fade manager configuration") + .that(fadeConfigs.getUnfadeableContentTypes()) + .isEqualTo(fmc.getUnfadeableContentTypes()); + } + + @Test + public void testGetUnfadeableAudioAttributes_withFadeManagerConfigurations_equals() { + mSetFlagsRule.enableFlags(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION); + List<AudioAttributes> attrsList = List.of(TEST_ASSISTANT_AUDIO_ATTRIBUTE, + TEST_EMERGENCY_AUDIO_ATTRIBUTE); + FadeManagerConfiguration fmc = createFadeMgrConfig(/* fadeableUsages= */ null, + /* unfadeableContentTypes= */ null, /* unfadeableUids= */ null, + /* unfadeableAudioAttrs= */ attrsList); + FadeConfigurations fadeConfigs = new FadeConfigurations(); + + fadeConfigs.setFadeManagerConfiguration(fmc); + + expect.withMessage("Unfadeable audio attributes with fade manager configuration") + .that(fadeConfigs.getUnfadeableAudioAttributes()) + .isEqualTo(fmc.getUnfadeableAudioAttributes()); + } + + @Test + public void testGetUnfadeableUids_withFadeManagerConfigurations_equals() { + mSetFlagsRule.enableFlags(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION); + List<Integer> uidsList = List.of(TEST_UID_SYSTEM, TEST_UID_USER); + FadeManagerConfiguration fmc = createFadeMgrConfig(/* fadeableUsages= */ null, + /* unfadeableContentTypes= */ null, /* unfadeableUids= */ uidsList, + /* unfadeableAudioAttrs= */ null); + FadeConfigurations fadeConfigs = new FadeConfigurations(); + + fadeConfigs.setFadeManagerConfiguration(fmc); + + expect.withMessage("Unfadeable uids with fade manager configuration") + .that(fadeConfigs.getUnfadeableUids()).isEqualTo(fmc.getUnfadeableUids()); + } + + private static FadeManagerConfiguration createFadeMgrConfig(List<Integer> fadeableUsages, + List<Integer> unfadeableContentTypes, List<Integer> unfadeableUids, + List<AudioAttributes> unfadeableAudioAttrs) { + FadeManagerConfiguration.Builder builder = new FadeManagerConfiguration.Builder(); + if (fadeableUsages != null) { + builder.setFadeableUsages(fadeableUsages); + } + if (unfadeableContentTypes != null) { + builder.setUnfadeableContentTypes(unfadeableContentTypes); + } + if (unfadeableUids != null) { + builder.setUnfadeableUids(unfadeableUids); + } + if (unfadeableAudioAttrs != null) { + builder.setUnfadeableAudioAttributes(unfadeableAudioAttrs); + } + if (fadeableUsages != null) { + for (int index = 0; index < fadeableUsages.size(); index++) { + builder.setFadeOutVolumeShaperConfigForAudioAttributes( + createGenericAudioAttributesForUsage(fadeableUsages.get(index)), + DEFAULT_FADEOUT_VSHAPE); + } + } + if (fadeableUsages != null) { + for (int index = 0; index < fadeableUsages.size(); index++) { + builder.setFadeInVolumeShaperConfigForAudioAttributes( + createGenericAudioAttributesForUsage(fadeableUsages.get(index)), + DEFAULT_FADEIN_VSHAPE); + } + } + + return builder.build(); + } + + private static AudioAttributes createGenericAudioAttributesForUsage(int usage) { + if (AudioAttributes.isSystemUsage(usage)) { + return new AudioAttributes.Builder().setSystemUsage(usage).build(); + } + return new AudioAttributes.Builder().setUsage(usage).build(); + } } diff --git a/services/tests/servicestests/src/com/android/server/audio/FadeOutManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/FadeOutManagerTest.java index 65059d5ca8fd..fa94821d4ff2 100644 --- a/services/tests/servicestests/src/com/android/server/audio/FadeOutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/FadeOutManagerTest.java @@ -23,8 +23,6 @@ import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_AAUDIO; import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_JAM_AUDIOTRACK; import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_UNKNOWN; -import static org.junit.Assert.assertThrows; - import android.content.Context; import android.media.AudioAttributes; import android.media.AudioManager; @@ -75,20 +73,11 @@ public final class FadeOutManagerTest { @Before public void setUp() { - mFadeOutManager = new FadeOutManager(new FadeConfigurations()); + mFadeOutManager = new FadeOutManager(); mContext = ApplicationProvider.getApplicationContext(); } @Test - public void constructor_nullFadeConfigurations_fails() { - Throwable thrown = assertThrows(NullPointerException.class, () -> new FadeOutManager( - /* FadeConfigurations= */ null)); - - expect.withMessage("Constructor exception") - .that(thrown).hasMessageThat().contains("Fade configurations can not be null"); - } - - @Test public void testCanCauseFadeOut_forFaders_returnsTrue() { FocusRequester winner = createFocusRequester(TEST_MEDIA_AUDIO_ATTRIBUTE, "winning-client", "unit-test", TEST_UID_USER, diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java index 3b5cae328b3c..88b2ed4f79c9 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java @@ -50,14 +50,22 @@ import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; import android.hardware.biometrics.IBiometricService; import android.hardware.biometrics.IBiometricServiceReceiver; import android.hardware.biometrics.PromptInfo; +import android.hardware.biometrics.fingerprint.SensorProps; +import android.hardware.face.FaceSensorConfigurations; import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.face.IFaceService; +import android.hardware.fingerprint.FingerprintSensorConfigurations; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IFingerprintService; import android.hardware.iris.IIrisService; import android.os.Binder; +import android.os.RemoteException; import android.os.UserHandle; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.InstrumentationRegistry; @@ -89,6 +97,9 @@ public class AuthServiceTest { @Rule public MockitoRule mockitorule = MockitoJUnit.rule(); + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -118,6 +129,10 @@ public class AuthServiceTest { @Captor private ArgumentCaptor<List<FingerprintSensorPropertiesInternal>> mFingerprintPropsCaptor; @Captor + private ArgumentCaptor<FingerprintSensorConfigurations> mFingerprintSensorConfigurationsCaptor; + @Captor + private ArgumentCaptor<FaceSensorConfigurations> mFaceSensorConfigurationsCaptor; + @Captor private ArgumentCaptor<List<FaceSensorPropertiesInternal>> mFacePropsCaptor; @Before @@ -143,6 +158,9 @@ public class AuthServiceTest { when(mContext.getResources()).thenReturn(mResources); when(mInjector.getBiometricService()).thenReturn(mBiometricService); when(mInjector.getConfiguration(any())).thenReturn(config); + when(mInjector.getFaceConfiguration(any())).thenReturn(config); + when(mInjector.getFingerprintConfiguration(any())).thenReturn(config); + when(mInjector.getIrisConfiguration(any())).thenReturn(config); when(mInjector.getFingerprintService()).thenReturn(mFingerprintService); when(mInjector.getFaceService()).thenReturn(mFaceService); when(mInjector.getIrisService()).thenReturn(mIrisService); @@ -173,12 +191,13 @@ public class AuthServiceTest { } @Test + @RequiresFlagsDisabled(com.android.server.biometrics.Flags.FLAG_DE_HIDL) public void testRegisterAuthenticator_registerAuthenticators() throws Exception { final int fingerprintId = 0; final int fingerprintStrength = 15; final int faceId = 1; - final int faceStrength = 4095; + final int faceStrength = 15; final String[] config = { // ID0:Fingerprint:Strong @@ -206,6 +225,51 @@ public class AuthServiceTest { Utils.authenticatorStrengthToPropertyStrength(faceStrength)); } + @Test + @RequiresFlagsEnabled(com.android.server.biometrics.Flags.FLAG_DE_HIDL) + public void testRegisterAuthenticator_registerAuthenticatorsLegacy() throws RemoteException { + final int fingerprintId = 0; + final int fingerprintStrength = 15; + + final int faceId = 1; + final int faceStrength = 4095; + + final String[] config = { + // ID0:Fingerprint:Strong + String.format("%d:2:%d", fingerprintId, fingerprintStrength), + // ID2:Face:Convenience + String.format("%d:8:%d", faceId, faceStrength) + }; + + when(mInjector.getFingerprintConfiguration(any())).thenReturn(config); + when(mInjector.getFaceConfiguration(any())).thenReturn(config); + when(mInjector.getFingerprintAidlInstances()).thenReturn(new String[]{}); + when(mInjector.getFaceAidlInstances()).thenReturn(new String[]{}); + + mAuthService = new AuthService(mContext, mInjector); + mAuthService.onStart(); + + verify(mFingerprintService).registerAuthenticatorsLegacy( + mFingerprintSensorConfigurationsCaptor.capture()); + + final SensorProps[] fingerprintProp = mFingerprintSensorConfigurationsCaptor.getValue() + .getSensorPairForInstance("defaultHIDL").second; + + assertEquals(fingerprintProp[0].commonProps.sensorId, fingerprintId); + assertEquals(fingerprintProp[0].commonProps.sensorStrength, + Utils.authenticatorStrengthToPropertyStrength(fingerprintStrength)); + + verify(mFaceService).registerAuthenticatorsLegacy( + mFaceSensorConfigurationsCaptor.capture()); + + final android.hardware.biometrics.face.SensorProps[] faceProp = + mFaceSensorConfigurationsCaptor.getValue() + .getSensorPairForInstance("defaultHIDL").second; + + assertEquals(faceProp[0].commonProps.sensorId, faceId); + assertEquals(faceProp[0].commonProps.sensorStrength, + Utils.authenticatorStrengthToPropertyStrength(faceStrength)); + } // TODO(b/141025588): Check that an exception is thrown when the userId != callingUserId @Test diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java new file mode 100644 index 000000000000..c9e1c4a8bfc5 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 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. + */ + +package com.android.server.biometrics.sensors.face; + +import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; +import static android.hardware.face.FaceSensorProperties.TYPE_UNKNOWN; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.biometrics.BiometricAuthenticator; +import android.hardware.biometrics.IBiometricService; +import android.hardware.biometrics.face.IFace; +import android.hardware.biometrics.face.SensorProps; +import android.hardware.face.FaceSensorConfigurations; +import android.hardware.face.FaceSensorPropertiesInternal; +import android.hardware.face.IFaceAuthenticatorsRegisteredCallback; +import android.os.RemoteException; +import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.provider.Settings; +import android.testing.TestableContext; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.internal.util.test.FakeSettingsProvider; +import com.android.internal.util.test.FakeSettingsProviderRule; +import com.android.server.biometrics.Flags; +import com.android.server.biometrics.Utils; +import com.android.server.biometrics.sensors.face.aidl.FaceProvider; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@Presubmit +@SmallTest +public class FaceServiceTest { + private static final int ID_DEFAULT = 2; + private static final int ID_VIRTUAL = 6; + private static final String NAME_DEFAULT = "default"; + private static final String NAME_VIRTUAL = "virtual"; + + @Rule + public final MockitoRule mMockito = MockitoJUnit.rule(); + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Rule + public final TestableContext mContext = new TestableContext( + InstrumentationRegistry.getInstrumentation().getTargetContext(), null); + @Rule + public final FakeSettingsProviderRule mSettingsRule = FakeSettingsProvider.rule(); + @Mock + FaceProvider mFaceProviderDefault; + @Mock + FaceProvider mFaceProviderVirtual; + @Mock + IFace mDefaultFaceDaemon; + @Mock + IFace mVirtualFaceDaemon; + @Mock + IBiometricService mIBiometricService; + + private final SensorProps mDefaultSensorProps = new SensorProps(); + private final SensorProps mVirtualSensorProps = new SensorProps(); + private FaceService mFaceService; + private final FaceSensorPropertiesInternal mSensorPropsDefault = + new FaceSensorPropertiesInternal(ID_DEFAULT, STRENGTH_STRONG, + 2 /* maxEnrollmentsPerUser */, + List.of(), + TYPE_UNKNOWN, + true /* supportsFaceDetection */, + true /* supportsSelfIllumination */, + false /* resetLockoutRequiresChallenge */); + private final FaceSensorPropertiesInternal mSensorPropsVirtual = + new FaceSensorPropertiesInternal(ID_VIRTUAL, STRENGTH_STRONG, + 2 /* maxEnrollmentsPerUser */, + List.of(), + TYPE_UNKNOWN, + true /* supportsFaceDetection */, + true /* supportsSelfIllumination */, + false /* resetLockoutRequiresChallenge */); + private FaceSensorConfigurations mFaceSensorConfigurations; + + @Before + public void setUp() throws RemoteException { + when(mDefaultFaceDaemon.getSensorProps()).thenReturn( + new SensorProps[]{mDefaultSensorProps}); + when(mVirtualFaceDaemon.getSensorProps()).thenReturn( + new SensorProps[]{mVirtualSensorProps}); + when(mFaceProviderDefault.getSensorProperties()).thenReturn(List.of(mSensorPropsDefault)); + when(mFaceProviderVirtual.getSensorProperties()).thenReturn(List.of(mSensorPropsVirtual)); + + mFaceSensorConfigurations = new FaceSensorConfigurations(false); + mFaceSensorConfigurations.addAidlConfigs(new String[]{NAME_DEFAULT, NAME_VIRTUAL}, + (name) -> { + if (name.equals(IFace.DESCRIPTOR + "/" + NAME_DEFAULT)) { + return mDefaultFaceDaemon; + } else if (name.equals(IFace.DESCRIPTOR + "/" + NAME_VIRTUAL)) { + return mVirtualFaceDaemon; + } + return null; + }); + } + + private void initService() { + mFaceService = new FaceService(mContext, + (filteredSensorProps, resetLockoutRequiresChallenge) -> { + if (NAME_DEFAULT.equals(filteredSensorProps.first)) return mFaceProviderDefault; + if (NAME_VIRTUAL.equals(filteredSensorProps.first)) return mFaceProviderVirtual; + return null; + }, () -> mIBiometricService); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL) + public void registerAuthenticatorsLegacy_defaultOnly() throws Exception { + initService(); + + mFaceService.mServiceWrapper.registerAuthenticatorsLegacy(mFaceSensorConfigurations); + waitForRegistration(); + + verify(mIBiometricService).registerAuthenticator(eq(ID_DEFAULT), + eq(BiometricAuthenticator.TYPE_FACE), + eq(Utils.propertyStrengthToAuthenticatorStrength(STRENGTH_STRONG)), + any()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL) + public void registerAuthenticatorsLegacy_virtualOnly() throws Exception { + initService(); + Settings.Secure.putInt(mSettingsRule.mockContentResolver(mContext), + Settings.Secure.BIOMETRIC_VIRTUAL_ENABLED, 1); + + mFaceService.mServiceWrapper.registerAuthenticatorsLegacy(mFaceSensorConfigurations); + waitForRegistration(); + + verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), + eq(BiometricAuthenticator.TYPE_FACE), + eq(Utils.propertyStrengthToAuthenticatorStrength(STRENGTH_STRONG)), any()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL) + public void registerAuthenticatorsLegacy_virtualAlwaysWhenNoOther() throws Exception { + mFaceSensorConfigurations = new FaceSensorConfigurations(false); + mFaceSensorConfigurations.addAidlConfigs(new String[]{NAME_VIRTUAL}, + (name) -> { + if (name.equals(IFace.DESCRIPTOR + "/" + NAME_DEFAULT)) { + return mDefaultFaceDaemon; + } else if (name.equals(IFace.DESCRIPTOR + "/" + NAME_VIRTUAL)) { + return mVirtualFaceDaemon; + } + return null; + }); + initService(); + + mFaceService.mServiceWrapper.registerAuthenticatorsLegacy(mFaceSensorConfigurations); + waitForRegistration(); + + verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), + eq(BiometricAuthenticator.TYPE_FACE), + eq(Utils.propertyStrengthToAuthenticatorStrength(STRENGTH_STRONG)), any()); + } + + private void waitForRegistration() throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + mFaceService.mServiceWrapper.addAuthenticatorsRegisteredCallback( + new IFaceAuthenticatorsRegisteredCallback.Stub() { + public void onAllAuthenticatorsRegistered( + List<FaceSensorPropertiesInternal> sensors) { + latch.countDown(); + } + }); + latch.await(10, TimeUnit.SECONDS); + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java index f43120d1a755..8b1a2915820a 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java @@ -16,6 +16,9 @@ package com.android.server.biometrics.sensors.face.aidl; +import static android.os.UserHandle.USER_NULL; +import static android.os.UserHandle.USER_SYSTEM; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; @@ -32,15 +35,19 @@ import android.hardware.biometrics.common.CommonProps; import android.hardware.biometrics.face.IFace; import android.hardware.biometrics.face.ISession; import android.hardware.biometrics.face.SensorProps; +import android.hardware.face.HidlFaceSensorConfig; import android.os.RemoteException; -import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import com.android.internal.R; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.BiometricScheduler; @@ -49,6 +56,7 @@ import com.android.server.biometrics.sensors.HalClientMonitor; import com.android.server.biometrics.sensors.LockoutResetDispatcher; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -59,6 +67,10 @@ import java.util.ArrayList; @SmallTest public class FaceProviderTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final String TAG = "FaceProviderTest"; private static final float FRR_THRESHOLD = 0.2f; @@ -109,7 +121,7 @@ public class FaceProviderTest { mFaceProvider = new FaceProvider(mContext, mBiometricStateCallback, mSensorProps, TAG, mLockoutResetDispatcher, mBiometricContext, - mDaemon); + mDaemon, false /* resetLockoutRequiresChallenge */, false /* testHalEnabled */); } @Test @@ -124,10 +136,38 @@ public class FaceProviderTest { assertThat(currentClient).isInstanceOf(FaceInternalCleanupClient.class); assertThat(currentClient.getSensorId()).isEqualTo(prop.commonProps.sensorId); - assertThat(currentClient.getTargetUserId()).isEqualTo(UserHandle.USER_SYSTEM); + assertThat(currentClient.getTargetUserId()).isEqualTo(USER_SYSTEM); } } + @Test + @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL) + public void testAddingHidlSensors() { + when(mResources.getIntArray(anyInt())).thenReturn(new int[]{}); + when(mResources.getBoolean(anyInt())).thenReturn(false); + + final int faceId = 0; + final int faceStrength = 15; + final String config = String.format("%d:8:%d", faceId, faceStrength); + final HidlFaceSensorConfig faceSensorConfig = new HidlFaceSensorConfig(); + faceSensorConfig.parse(config, mContext); + final HidlFaceSensorConfig[] hidlFaceSensorConfig = + new HidlFaceSensorConfig[]{faceSensorConfig}; + mFaceProvider = new FaceProvider(mContext, + mBiometricStateCallback, hidlFaceSensorConfig, TAG, + mLockoutResetDispatcher, mBiometricContext, mDaemon, + true /* resetLockoutRequiresChallenge */, + true /* testHalEnabled */); + + assertThat(mFaceProvider.mFaceSensors.get(faceId) + .getLazySession().get().getUserId()).isEqualTo(USER_NULL); + + waitForIdle(); + + assertThat(mFaceProvider.mFaceSensors.get(faceId) + .getLazySession().get().getUserId()).isEqualTo(USER_SYSTEM); + } + @SuppressWarnings("rawtypes") @Test public void halServiceDied_resetsAllSchedulers() { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java index 7a293e80c183..e7f7195588ff 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java @@ -157,7 +157,7 @@ public class SensorTest { sensorProps.halControlsPreview, false /* resetLockoutRequiresChallenge */); final Sensor sensor = new Sensor("SensorTest", mFaceProvider, mContext, null /* handler */, internalProp, mLockoutResetDispatcher, mBiometricContext); - + sensor.init(mLockoutResetDispatcher, mFaceProvider); mScheduler.reset(); assertNull(mScheduler.getCurrentClient()); @@ -185,6 +185,7 @@ public class SensorTest { sensorProps.halControlsPreview, false /* resetLockoutRequiresChallenge */); final Sensor sensor = new Sensor("SensorTest", mFaceProvider, mContext, null, internalProp, mLockoutResetDispatcher, mBiometricContext, mCurrentSession); + sensor.init(mLockoutResetDispatcher, mFaceProvider); mScheduler = (UserAwareBiometricScheduler) sensor.getScheduler(); sensor.mCurrentSession = new AidlSession(0, mock(ISession.class), USER_ID, mHalCallback); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapterTest.java new file mode 100644 index 000000000000..4e43332ab52c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapterTest.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 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. + */ + +package com.android.server.biometrics.sensors.face.hidl; + +import static com.android.server.biometrics.sensors.face.hidl.HidlToAidlSessionAdapter.ENROLL_TIMEOUT_SEC; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.biometrics.IBiometricService; +import android.hardware.biometrics.face.V1_0.IBiometricsFace; +import android.hardware.biometrics.face.V1_0.OptionalUint64; +import android.hardware.biometrics.face.V1_0.Status; +import android.hardware.face.Face; +import android.hardware.face.HidlFaceSensorConfig; +import android.os.Handler; +import android.os.RemoteException; +import android.os.test.TestLooper; +import android.testing.TestableContext; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.AuthSessionCoordinator; +import com.android.server.biometrics.sensors.BiometricScheduler; +import com.android.server.biometrics.sensors.BiometricUtils; +import com.android.server.biometrics.sensors.LockoutResetDispatcher; +import com.android.server.biometrics.sensors.LockoutTracker; +import com.android.server.biometrics.sensors.face.aidl.AidlResponseHandler; +import com.android.server.biometrics.sensors.face.aidl.FaceEnrollClient; +import com.android.server.biometrics.sensors.face.aidl.FaceProvider; +import com.android.server.biometrics.sensors.face.aidl.FaceResetLockoutClient; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class HidlToAidlSensorAdapterTest { + private static final String TAG = "HidlToAidlSensorAdapterTest"; + private static final int USER_ID = 2; + private static final int SENSOR_ID = 4; + private static final byte[] HAT = new byte[69]; + @Rule + public final MockitoRule mMockito = MockitoJUnit.rule(); + + @Mock + private IBiometricService mBiometricService; + @Mock + private LockoutResetDispatcher mLockoutResetDispatcherForSensor; + @Mock + private LockoutResetDispatcher mLockoutResetDispatcherForClient; + @Mock + private BiometricLogger mLogger; + @Mock + private BiometricContext mBiometricContext; + @Mock + private AuthSessionCoordinator mAuthSessionCoordinator; + @Mock + private FaceProvider mFaceProvider; + @Mock + private Runnable mInternalCleanupAndGetFeatureRunnable; + @Mock + private IBiometricsFace mDaemon; + @Mock + AidlResponseHandler.AidlResponseHandlerCallback mAidlResponseHandlerCallback; + @Mock + BiometricUtils<Face> mBiometricUtils; + + private final TestLooper mLooper = new TestLooper(); + private HidlToAidlSensorAdapter mHidlToAidlSensorAdapter; + private final TestableContext mContext = new TestableContext( + ApplicationProvider.getApplicationContext()); + + @Before + public void setUp() throws RemoteException { + final OptionalUint64 result = new OptionalUint64(); + result.status = Status.OK; + + when(mBiometricContext.getAuthSessionCoordinator()).thenReturn(mAuthSessionCoordinator); + when(mDaemon.setCallback(any())).thenReturn(result); + doAnswer((answer) -> { + mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback() + .onLockoutChanged(0); + return null; + }).when(mDaemon).resetLockout(any()); + doAnswer((answer) -> { + mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback() + .onEnrollmentProgress(1, 0); + return null; + }).when(mDaemon).enroll(any(), anyInt(), any()); + + mContext.getOrCreateTestableResources(); + + final String config = String.format("%d:8:15", SENSOR_ID); + final BiometricScheduler scheduler = new BiometricScheduler(TAG, + new Handler(mLooper.getLooper()), + BiometricScheduler.SENSOR_TYPE_FACE, + null /* gestureAvailabilityTracker */, + mBiometricService, 10 /* recentOperationsLimit */); + final HidlFaceSensorConfig faceSensorConfig = new HidlFaceSensorConfig(); + faceSensorConfig.parse(config, mContext); + mHidlToAidlSensorAdapter = new HidlToAidlSensorAdapter(TAG, mFaceProvider, + mContext, new Handler(mLooper.getLooper()), faceSensorConfig, + mLockoutResetDispatcherForSensor, mBiometricContext, + false /* resetLockoutRequiresChallenge */, mInternalCleanupAndGetFeatureRunnable, + mAuthSessionCoordinator, mDaemon, mAidlResponseHandlerCallback); + mHidlToAidlSensorAdapter.init(mLockoutResetDispatcherForSensor, mFaceProvider); + mHidlToAidlSensorAdapter.setScheduler(scheduler); + mHidlToAidlSensorAdapter.handleUserChanged(USER_ID); + } + + @Test + public void lockoutTimedResetViaClient() { + setLockoutTimed(); + + mHidlToAidlSensorAdapter.getScheduler().scheduleClientMonitor( + new FaceResetLockoutClient(mContext, mHidlToAidlSensorAdapter.getLazySession(), + USER_ID, TAG, SENSOR_ID, mLogger, mBiometricContext, HAT, + mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */), + mLockoutResetDispatcherForClient, 0 /* biometricStrength */)); + mLooper.dispatchAll(); + + assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false/* forAuth */) + .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE); + verify(mLockoutResetDispatcherForClient, never()).notifyLockoutResetCallbacks(SENSOR_ID); + verify(mLockoutResetDispatcherForSensor).notifyLockoutResetCallbacks(SENSOR_ID); + verify(mAuthSessionCoordinator, never()).resetLockoutFor(eq(USER_ID), anyInt(), anyLong()); + } + + @Test + public void lockoutTimedResetViaCallback() { + setLockoutTimed(); + + mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback().onLockoutChanged(0); + mLooper.dispatchAll(); + + verify(mLockoutResetDispatcherForSensor).notifyLockoutResetCallbacks(eq( + SENSOR_ID)); + assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */) + .getLockoutModeForUser(USER_ID)) + .isEqualTo(LockoutTracker.LOCKOUT_NONE); + verify(mAuthSessionCoordinator, never()).resetLockoutFor(eq(USER_ID), anyInt(), anyLong()); + } + + @Test + public void lockoutPermanentResetViaCallback() { + setLockoutPermanent(); + + mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback().onLockoutChanged(0); + mLooper.dispatchAll(); + + verify(mLockoutResetDispatcherForSensor).notifyLockoutResetCallbacks(eq( + SENSOR_ID)); + assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */) + .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE); + verify(mAuthSessionCoordinator, never()).resetLockoutFor(eq(USER_ID), anyInt(), anyLong()); + } + + @Test + public void lockoutPermanentResetViaClient() { + setLockoutPermanent(); + + mHidlToAidlSensorAdapter.getScheduler().scheduleClientMonitor( + new FaceResetLockoutClient(mContext, + mHidlToAidlSensorAdapter.getLazySession(), + USER_ID, TAG, SENSOR_ID, mLogger, mBiometricContext, HAT, + mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */), + mLockoutResetDispatcherForClient, 0 /* biometricStrength */)); + mLooper.dispatchAll(); + + verify(mLockoutResetDispatcherForClient, never()).notifyLockoutResetCallbacks(SENSOR_ID); + verify(mLockoutResetDispatcherForSensor).notifyLockoutResetCallbacks(SENSOR_ID); + assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */) + .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE); + verify(mAuthSessionCoordinator, never()).resetLockoutFor(eq(USER_ID), anyInt(), anyLong()); + } + + @Test + public void verifyOnEnrollSuccessCallback() { + mHidlToAidlSensorAdapter.getScheduler().scheduleClientMonitor(new FaceEnrollClient(mContext, + mHidlToAidlSensorAdapter.getLazySession(), null /* token */, null /* listener */, + USER_ID, HAT, TAG, 1 /* requestId */, mBiometricUtils, + new int[]{} /* disabledFeatures */, ENROLL_TIMEOUT_SEC, null /* previewSurface */, + SENSOR_ID, mLogger, mBiometricContext, 1 /* maxTemplatesPerUser */, + false /* debugConsent */)); + mLooper.dispatchAll(); + + verify(mAidlResponseHandlerCallback).onEnrollSuccess(); + } + + private void setLockoutTimed() { + mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback() + .onLockoutChanged(1); + mLooper.dispatchAll(); + + assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */) + .getLockoutModeForUser(USER_ID)) + .isEqualTo(LockoutTracker.LOCKOUT_TIMED); + } + + private void setLockoutPermanent() { + mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback() + .onLockoutChanged(-1); + mLooper.dispatchAll(); + + assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */) + .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_PERMANENT); + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/AidlToHidlAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapterTest.java index 9a40e8a7201a..b9a4fb4e0939 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/AidlToHidlAdapterTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapterTest.java @@ -16,7 +16,7 @@ package com.android.server.biometrics.sensors.face.hidl; -import static com.android.server.biometrics.sensors.face.hidl.AidlToHidlAdapter.ENROLL_TIMEOUT_SEC; +import static com.android.server.biometrics.sensors.face.hidl.HidlToAidlSessionAdapter.ENROLL_TIMEOUT_SEC; import static com.android.server.biometrics.sensors.face.hidl.FaceGenerateChallengeClient.CHALLENGE_TIMEOUT_SEC; import static com.google.common.truth.Truth.assertThat; @@ -68,7 +68,7 @@ import java.util.List; @Presubmit @SmallTest -public class AidlToHidlAdapterTest { +public class HidlToAidlSessionAdapterTest { @Rule public final MockitoRule mockito = MockitoJUnit.rule(); @@ -84,25 +84,32 @@ public class AidlToHidlAdapterTest { private Clock mClock; private final long mChallenge = 100L; - private AidlToHidlAdapter mAidlToHidlAdapter; + private HidlToAidlSessionAdapter mHidlToAidlSessionAdapter; private final Face mFace = new Face("face" /* name */, 1 /* faceId */, 0 /* deviceId */); private final int mFeature = BiometricFaceConstants.FEATURE_REQUIRE_REQUIRE_DIVERSITY; private final byte[] mFeatures = new byte[]{Feature.REQUIRE_ATTENTION}; @Before public void setUp() throws RemoteException { + final OptionalUint64 setCallbackResult = new OptionalUint64(); + setCallbackResult.value = 1; + + when(mSession.setCallback(any())).thenReturn(setCallbackResult); + TestableContext testableContext = new TestableContext( InstrumentationRegistry.getInstrumentation().getContext()); testableContext.addMockSystemService(FaceManager.class, mFaceManager); - mAidlToHidlAdapter = new AidlToHidlAdapter(testableContext, () -> mSession, 0 /* userId */, - mAidlResponseHandler, mClock); + + mHidlToAidlSessionAdapter = new HidlToAidlSessionAdapter(testableContext, () -> mSession, + 0 /* userId */, mAidlResponseHandler, mClock); mHardwareAuthToken.timestamp = new Timestamp(); mHardwareAuthToken.mac = new byte[10]; - final OptionalUint64 result = new OptionalUint64(); - result.status = Status.OK; - result.value = mChallenge; - when(mSession.generateChallenge(anyInt())).thenReturn(result); + final OptionalUint64 generateChallengeResult = new OptionalUint64(); + generateChallengeResult.status = Status.OK; + generateChallengeResult.value = mChallenge; + + when(mSession.generateChallenge(anyInt())).thenReturn(generateChallengeResult); when(mFaceManager.getEnrolledFaces(anyInt())).thenReturn(List.of(mFace)); } @@ -112,16 +119,16 @@ public class AidlToHidlAdapterTest { final ArgumentCaptor<Long> challengeCaptor = ArgumentCaptor.forClass(Long.class); - mAidlToHidlAdapter.generateChallenge(); + mHidlToAidlSessionAdapter.generateChallenge(); verify(mSession).generateChallenge(CHALLENGE_TIMEOUT_SEC); verify(mAidlResponseHandler).onChallengeGenerated(challengeCaptor.capture()); assertThat(challengeCaptor.getValue()).isEqualTo(mChallenge); forwardTime(10 /* seconds */); - mAidlToHidlAdapter.generateChallenge(); + mHidlToAidlSessionAdapter.generateChallenge(); forwardTime(20 /* seconds */); - mAidlToHidlAdapter.generateChallenge(); + mHidlToAidlSessionAdapter.generateChallenge(); //Confirms that the challenge is cached and the hal method is not called again verifyNoMoreInteractions(mSession); @@ -129,7 +136,7 @@ public class AidlToHidlAdapterTest { .onChallengeGenerated(mChallenge); forwardTime(60 /* seconds */); - mAidlToHidlAdapter.generateChallenge(); + mHidlToAidlSessionAdapter.generateChallenge(); //HAL method called after challenge has timed out verify(mSession, times(2)).generateChallenge(CHALLENGE_TIMEOUT_SEC); @@ -138,11 +145,11 @@ public class AidlToHidlAdapterTest { @Test public void testRevokeChallenge_waitsUntilEmpty() throws RemoteException { for (int i = 0; i < 3; i++) { - mAidlToHidlAdapter.generateChallenge(); + mHidlToAidlSessionAdapter.generateChallenge(); forwardTime(10 /* seconds */); } for (int i = 0; i < 3; i++) { - mAidlToHidlAdapter.revokeChallenge(0); + mHidlToAidlSessionAdapter.revokeChallenge(0); forwardTime((i + 1) * 10 /* seconds */); } @@ -151,20 +158,19 @@ public class AidlToHidlAdapterTest { @Test public void testRevokeChallenge_timeout() throws RemoteException { - mAidlToHidlAdapter.generateChallenge(); - mAidlToHidlAdapter.generateChallenge(); + mHidlToAidlSessionAdapter.generateChallenge(); + mHidlToAidlSessionAdapter.generateChallenge(); forwardTime(700); - mAidlToHidlAdapter.generateChallenge(); - mAidlToHidlAdapter.revokeChallenge(0); + mHidlToAidlSessionAdapter.generateChallenge(); + mHidlToAidlSessionAdapter.revokeChallenge(0); verify(mSession).revokeChallenge(); } @Test public void testEnroll() throws RemoteException { - ICancellationSignal cancellationSignal = mAidlToHidlAdapter.enroll(mHardwareAuthToken, - EnrollmentType.DEFAULT, mFeatures, - null /* previewSurface */); + ICancellationSignal cancellationSignal = mHidlToAidlSessionAdapter.enroll( + mHardwareAuthToken, EnrollmentType.DEFAULT, mFeatures, null /* previewSurface */); ArgumentCaptor<ArrayList<Integer>> featureCaptor = ArgumentCaptor.forClass(ArrayList.class); verify(mSession).enroll(any(), eq(ENROLL_TIMEOUT_SEC), featureCaptor.capture()); @@ -182,7 +188,8 @@ public class AidlToHidlAdapterTest { @Test public void testAuthenticate() throws RemoteException { final int operationId = 2; - ICancellationSignal cancellationSignal = mAidlToHidlAdapter.authenticate(operationId); + ICancellationSignal cancellationSignal = mHidlToAidlSessionAdapter.authenticate( + operationId); verify(mSession).authenticate(operationId); @@ -193,7 +200,7 @@ public class AidlToHidlAdapterTest { @Test public void testDetectInteraction() throws RemoteException { - ICancellationSignal cancellationSignal = mAidlToHidlAdapter.detectInteraction(); + ICancellationSignal cancellationSignal = mHidlToAidlSessionAdapter.detectInteraction(); verify(mSession).authenticate(0); @@ -204,7 +211,7 @@ public class AidlToHidlAdapterTest { @Test public void testEnumerateEnrollments() throws RemoteException { - mAidlToHidlAdapter.enumerateEnrollments(); + mHidlToAidlSessionAdapter.enumerateEnrollments(); verify(mSession).enumerate(); } @@ -212,7 +219,7 @@ public class AidlToHidlAdapterTest { @Test public void testRemoveEnrollment() throws RemoteException { final int[] enrollments = new int[]{1}; - mAidlToHidlAdapter.removeEnrollments(enrollments); + mHidlToAidlSessionAdapter.removeEnrollments(enrollments); verify(mSession).remove(enrollments[0]); } @@ -226,8 +233,8 @@ public class AidlToHidlAdapterTest { when(mSession.getFeature(eq(mFeature), anyInt())).thenReturn(result); - mAidlToHidlAdapter.setFeature(mFeature); - mAidlToHidlAdapter.getFeatures(); + mHidlToAidlSessionAdapter.setFeature(mFeature); + mHidlToAidlSessionAdapter.getFeatures(); verify(mSession).getFeature(eq(mFeature), anyInt()); verify(mAidlResponseHandler).onFeaturesRetrieved(featureRetrieved.capture()); @@ -244,8 +251,8 @@ public class AidlToHidlAdapterTest { when(mSession.getFeature(eq(mFeature), anyInt())).thenReturn(result); - mAidlToHidlAdapter.setFeature(mFeature); - mAidlToHidlAdapter.getFeatures(); + mHidlToAidlSessionAdapter.setFeature(mFeature); + mHidlToAidlSessionAdapter.getFeatures(); verify(mSession).getFeature(eq(mFeature), anyInt()); verify(mAidlResponseHandler).onFeaturesRetrieved(featureRetrieved.capture()); @@ -260,8 +267,8 @@ public class AidlToHidlAdapterTest { when(mSession.getFeature(eq(mFeature), anyInt())).thenReturn(result); - mAidlToHidlAdapter.setFeature(mFeature); - mAidlToHidlAdapter.getFeatures(); + mHidlToAidlSessionAdapter.setFeature(mFeature); + mHidlToAidlSessionAdapter.getFeatures(); verify(mSession).getFeature(eq(mFeature), anyInt()); verify(mAidlResponseHandler, never()).onFeaturesRetrieved(any()); @@ -270,7 +277,7 @@ public class AidlToHidlAdapterTest { @Test public void testGetFeatures_featureNotSet() throws RemoteException { - mAidlToHidlAdapter.getFeatures(); + mHidlToAidlSessionAdapter.getFeatures(); verify(mSession, never()).getFeature(eq(mFeature), anyInt()); verify(mAidlResponseHandler, never()).onFeaturesRetrieved(any()); @@ -283,7 +290,7 @@ public class AidlToHidlAdapterTest { when(mSession.setFeature(anyInt(), anyBoolean(), any(), anyInt())).thenReturn(Status.OK); - mAidlToHidlAdapter.setFeature(mHardwareAuthToken, feature, enabled); + mHidlToAidlSessionAdapter.setFeature(mHardwareAuthToken, feature, enabled); verify(mAidlResponseHandler).onFeatureSet(feature); } @@ -296,7 +303,7 @@ public class AidlToHidlAdapterTest { when(mSession.setFeature(anyInt(), anyBoolean(), any(), anyInt())) .thenReturn(Status.INTERNAL_ERROR); - mAidlToHidlAdapter.setFeature(mHardwareAuthToken, feature, enabled); + mHidlToAidlSessionAdapter.setFeature(mHardwareAuthToken, feature, enabled); verify(mAidlResponseHandler).onError(BiometricFaceConstants.FACE_ERROR_UNKNOWN, 0 /* vendorCode */); @@ -311,7 +318,7 @@ public class AidlToHidlAdapterTest { when(mSession.getAuthenticatorId()).thenReturn(result); - mAidlToHidlAdapter.getAuthenticatorId(); + mHidlToAidlSessionAdapter.getAuthenticatorId(); verify(mSession).getAuthenticatorId(); verify(mAidlResponseHandler).onAuthenticatorIdRetrieved(authenticatorId); @@ -319,7 +326,7 @@ public class AidlToHidlAdapterTest { @Test public void testResetLockout() throws RemoteException { - mAidlToHidlAdapter.resetLockout(mHardwareAuthToken); + mHidlToAidlSessionAdapter.resetLockout(mHardwareAuthToken); ArgumentCaptor<ArrayList> hatCaptor = ArgumentCaptor.forClass(ArrayList.class); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java index 2aa62d96168d..f570ba23441d 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java @@ -42,13 +42,19 @@ import static org.mockito.Mockito.when; import android.app.AppOpsManager; import android.content.pm.PackageManager; import android.hardware.biometrics.IBiometricService; +import android.hardware.biometrics.fingerprint.IFingerprint; +import android.hardware.biometrics.fingerprint.SensorProps; import android.hardware.fingerprint.FingerprintAuthenticateOptions; +import android.hardware.fingerprint.FingerprintSensorConfigurations; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback; import android.hardware.fingerprint.IFingerprintServiceReceiver; import android.os.Binder; import android.os.IBinder; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; import android.testing.TestableContext; @@ -58,6 +64,7 @@ import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.util.test.FakeSettingsProvider; import com.android.internal.util.test.FakeSettingsProviderRule; import com.android.server.LocalServices; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvider; import com.android.server.companion.virtual.VirtualDeviceManagerInternal; @@ -89,6 +96,9 @@ public class FingerprintServiceTest { @Rule public final MockitoRule mMockito = MockitoJUnit.rule(); @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Rule public final TestableContext mContext = new TestableContext( InstrumentationRegistry.getInstrumentation().getTargetContext(), null); @Rule @@ -110,6 +120,10 @@ public class FingerprintServiceTest { private IBinder mToken; @Mock private VirtualDeviceManagerInternal mVdmInternal; + @Mock + private IFingerprint mDefaultFingerprintDaemon; + @Mock + private IFingerprint mVirtualFingerprintDaemon; @Captor private ArgumentCaptor<FingerprintAuthenticateOptions> mAuthenticateOptionsCaptor; @@ -126,7 +140,10 @@ public class FingerprintServiceTest { List.of(), TYPE_UDFPS_OPTICAL, false /* resetLockoutRequiresHardwareAuthToken */); + private FingerprintSensorConfigurations mFingerprintSensorConfigurations; private FingerprintService mService; + private final SensorProps mDefaultSensorProps = new SensorProps(); + private final SensorProps mVirtualSensorProps = new SensorProps(); @Before public void setup() throws Exception { @@ -139,6 +156,10 @@ public class FingerprintServiceTest { .thenAnswer(i -> i.getArguments()[0].equals(ID_DEFAULT)); when(mFingerprintVirtual.containsSensor(anyInt())) .thenAnswer(i -> i.getArguments()[0].equals(ID_VIRTUAL)); + when(mDefaultFingerprintDaemon.getSensorProps()).thenReturn( + new SensorProps[]{mDefaultSensorProps}); + when(mVirtualFingerprintDaemon.getSensorProps()).thenReturn( + new SensorProps[]{mVirtualSensorProps}); mContext.addMockSystemService(AppOpsManager.class, mAppOpsManager); for (int permission : List.of(OP_USE_BIOMETRIC, OP_USE_FINGERPRINT)) { @@ -150,6 +171,18 @@ public class FingerprintServiceTest { mContext.getTestablePermissions().setPermission( permission, PackageManager.PERMISSION_GRANTED); } + + mFingerprintSensorConfigurations = new FingerprintSensorConfigurations( + true /* resetLockoutRequiresHardwareAuthToken */); + mFingerprintSensorConfigurations.addAidlSensors(new String[]{NAME_DEFAULT, NAME_VIRTUAL}, + (name) -> { + if (name.equals(IFingerprint.DESCRIPTOR + "/" + NAME_DEFAULT)) { + return mDefaultFingerprintDaemon; + } else if (name.equals(IFingerprint.DESCRIPTOR + "/" + NAME_VIRTUAL)) { + return mVirtualFingerprintDaemon; + } + return null; + }); } private void initServiceWith(String... aidlInstances) { @@ -160,6 +193,10 @@ public class FingerprintServiceTest { if (NAME_DEFAULT.equals(name)) return mFingerprintDefault; if (NAME_VIRTUAL.equals(name)) return mFingerprintVirtual; return null; + }, (sensorPropsPair, resetLockoutRequiresHardwareAuthToken) -> { + if (NAME_DEFAULT.equals(sensorPropsPair.first)) return mFingerprintDefault; + if (NAME_VIRTUAL.equals(sensorPropsPair.first)) return mFingerprintVirtual; + return null; }); } @@ -180,6 +217,17 @@ public class FingerprintServiceTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL) + public void registerAuthenticatorsLegacy_defaultOnly() throws Exception { + initServiceWith(NAME_DEFAULT, NAME_VIRTUAL); + + mService.mServiceWrapper.registerAuthenticatorsLegacy(mFingerprintSensorConfigurations); + waitForRegistration(); + + verify(mIBiometricService).registerAuthenticator(eq(ID_DEFAULT), anyInt(), anyInt(), any()); + } + + @Test public void registerAuthenticators_virtualOnly() throws Exception { initServiceWith(NAME_DEFAULT, NAME_VIRTUAL); Settings.Secure.putInt(mSettingsRule.mockContentResolver(mContext), @@ -192,6 +240,19 @@ public class FingerprintServiceTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL) + public void registerAuthenticatorsLegacy_virtualOnly() throws Exception { + initServiceWith(NAME_DEFAULT, NAME_VIRTUAL); + Settings.Secure.putInt(mSettingsRule.mockContentResolver(mContext), + Settings.Secure.BIOMETRIC_VIRTUAL_ENABLED, 1); + + mService.mServiceWrapper.registerAuthenticatorsLegacy(mFingerprintSensorConfigurations); + waitForRegistration(); + + verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), anyInt(), anyInt(), any()); + } + + @Test public void registerAuthenticators_virtualAlwaysWhenNoOther() throws Exception { initServiceWith(NAME_VIRTUAL); @@ -201,6 +262,28 @@ public class FingerprintServiceTest { verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), anyInt(), anyInt(), any()); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL) + public void registerAuthenticatorsLegacy_virtualAlwaysWhenNoOther() throws Exception { + mFingerprintSensorConfigurations = + new FingerprintSensorConfigurations(true); + mFingerprintSensorConfigurations.addAidlSensors(new String[]{NAME_VIRTUAL}, + (name) -> { + if (name.equals(IFingerprint.DESCRIPTOR + "/" + NAME_DEFAULT)) { + return mDefaultFingerprintDaemon; + } else if (name.equals(IFingerprint.DESCRIPTOR + "/" + NAME_VIRTUAL)) { + return mVirtualFingerprintDaemon; + } + return null; + }); + initServiceWith(NAME_VIRTUAL); + + mService.mServiceWrapper.registerAuthenticatorsLegacy(mFingerprintSensorConfigurations); + waitForRegistration(); + + verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), anyInt(), anyInt(), any()); + } + private void waitForRegistration() throws Exception { final CountDownLatch latch = new CountDownLatch(1); mService.mServiceWrapper.addAuthenticatorsRegisteredCallback( diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java index 4cfb83fa1c69..bf5986c1d0f3 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java @@ -16,6 +16,9 @@ package com.android.server.biometrics.sensors.fingerprint.aidl; +import static android.os.UserHandle.USER_NULL; +import static android.os.UserHandle.USER_SYSTEM; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; @@ -34,17 +37,20 @@ import android.hardware.biometrics.fingerprint.IFingerprint; import android.hardware.biometrics.fingerprint.ISession; import android.hardware.biometrics.fingerprint.SensorLocation; import android.hardware.biometrics.fingerprint.SensorProps; +import android.hardware.fingerprint.HidlFingerprintSensorConfig; import android.os.RemoteException; -import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.sensors.AuthenticationStateListeners; -import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.HalClientMonitor; @@ -52,6 +58,7 @@ import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -64,6 +71,10 @@ public class FingerprintProviderTest { private static final String TAG = "FingerprintProviderTest"; + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Mock private Context mContext; @Mock @@ -115,7 +126,8 @@ public class FingerprintProviderTest { mFingerprintProvider = new FingerprintProvider(mContext, mBiometricStateCallback, mAuthenticationStateListeners, mSensorProps, TAG, mLockoutResetDispatcher, mGestureAvailabilityDispatcher, mBiometricContext, - mDaemon); + mDaemon, false /* resetLockoutRequiresHardwareAuthToken */, + true /* testHalEnabled */); } @Test @@ -123,17 +135,42 @@ public class FingerprintProviderTest { waitForIdle(); for (SensorProps prop : mSensorProps) { - final BiometricScheduler scheduler = - mFingerprintProvider.mFingerprintSensors.get(prop.commonProps.sensorId) - .getScheduler(); - BaseClientMonitor currentClient = scheduler.getCurrentClient(); - - assertThat(currentClient).isInstanceOf(FingerprintInternalCleanupClient.class); - assertThat(currentClient.getSensorId()).isEqualTo(prop.commonProps.sensorId); - assertThat(currentClient.getTargetUserId()).isEqualTo(UserHandle.USER_SYSTEM); + final Sensor sensor = + mFingerprintProvider.mFingerprintSensors.get(prop.commonProps.sensorId); + assertThat(sensor.getLazySession().get().getUserId()).isEqualTo(USER_SYSTEM); } } + @Test + @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL) + public void testAddingHidlSensors() { + when(mResources.getIntArray(anyInt())).thenReturn(new int[]{}); + when(mResources.getBoolean(anyInt())).thenReturn(false); + + final int fingerprintId = 0; + final int fingerprintStrength = 15; + final String config = String.format("%d:2:%d", fingerprintId, fingerprintStrength); + final HidlFingerprintSensorConfig fingerprintSensorConfig = + new HidlFingerprintSensorConfig(); + fingerprintSensorConfig.parse(config, mContext); + HidlFingerprintSensorConfig[] hidlFingerprintSensorConfigs = + new HidlFingerprintSensorConfig[]{fingerprintSensorConfig}; + mFingerprintProvider = new FingerprintProvider(mContext, + mBiometricStateCallback, mAuthenticationStateListeners, + hidlFingerprintSensorConfigs, TAG, mLockoutResetDispatcher, + mGestureAvailabilityDispatcher, mBiometricContext, mDaemon, + false /* resetLockoutRequiresHardwareAuthToken */, + true /* testHalEnabled */); + + assertThat(mFingerprintProvider.mFingerprintSensors.get(fingerprintId) + .getLazySession().get().getUserId()).isEqualTo(USER_NULL); + + waitForIdle(); + + assertThat(mFingerprintProvider.mFingerprintSensors.get(fingerprintId) + .getLazySession().get().getUserId()).isEqualTo(USER_SYSTEM); + } + @SuppressWarnings("rawtypes") @Test public void halServiceDied_resetsAllSchedulers() { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java index 410260074fbd..126a05e12628 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java @@ -96,7 +96,7 @@ public class SensorTest { private final TestLooper mLooper = new TestLooper(); private final LockoutCache mLockoutCache = new LockoutCache(); - private UserAwareBiometricScheduler mScheduler; + private BiometricScheduler mScheduler; private AidlResponseHandler mHalCallback; @Before @@ -164,7 +164,8 @@ public class SensorTest { final Sensor sensor = new Sensor("SensorTest", mFingerprintProvider, mContext, null /* handler */, internalProp, mLockoutResetDispatcher, mGestureAvailabilityDispatcher, mBiometricContext, mCurrentSession); - mScheduler = (UserAwareBiometricScheduler) sensor.getScheduler(); + sensor.init(mGestureAvailabilityDispatcher, mLockoutResetDispatcher); + mScheduler = sensor.getScheduler(); sensor.mCurrentSession = new AidlSession(0, mock(ISession.class), USER_ID, mHalCallback); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java new file mode 100644 index 000000000000..89a49615dbe1 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java @@ -0,0 +1,301 @@ +/* + * Copyright (C) 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. + */ + +package com.android.server.biometrics.sensors.fingerprint.hidl; + +import static android.hardware.fingerprint.FingerprintManager.ENROLL_ENROLL; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AlarmManager; +import android.hardware.biometrics.IBiometricService; +import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint; +import android.hardware.fingerprint.Fingerprint; +import android.hardware.fingerprint.HidlFingerprintSensorConfig; +import android.os.Handler; +import android.os.RemoteException; +import android.os.test.TestLooper; +import android.platform.test.annotations.Presubmit; +import android.testing.TestableContext; + +import androidx.annotation.NonNull; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.filters.SmallTest; + +import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.AuthSessionCoordinator; +import com.android.server.biometrics.sensors.AuthenticationStateListeners; +import com.android.server.biometrics.sensors.BiometricScheduler; +import com.android.server.biometrics.sensors.BiometricUtils; +import com.android.server.biometrics.sensors.ClientMonitorCallback; +import com.android.server.biometrics.sensors.LockoutResetDispatcher; +import com.android.server.biometrics.sensors.LockoutTracker; +import com.android.server.biometrics.sensors.StartUserClient; +import com.android.server.biometrics.sensors.StopUserClient; +import com.android.server.biometrics.sensors.UserAwareBiometricScheduler; +import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher; +import com.android.server.biometrics.sensors.fingerprint.aidl.AidlResponseHandler; +import com.android.server.biometrics.sensors.fingerprint.aidl.AidlSession; +import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintEnrollClient; +import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvider; +import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintResetLockoutClient; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@Presubmit +@SmallTest +public class HidlToAidlSensorAdapterTest { + + private static final String TAG = "HidlToAidlSensorAdapterTest"; + private static final int USER_ID = 2; + private static final int SENSOR_ID = 4; + private static final byte[] HAT = new byte[69]; + + @Rule + public final MockitoRule mMockito = MockitoJUnit.rule(); + + @Mock + private IBiometricService mBiometricService; + @Mock + private LockoutResetDispatcher mLockoutResetDispatcherForSensor; + @Mock + private LockoutResetDispatcher mLockoutResetDispatcherForClient; + @Mock + private BiometricLogger mLogger; + @Mock + private BiometricContext mBiometricContext; + @Mock + private AuthSessionCoordinator mAuthSessionCoordinator; + @Mock + private FingerprintProvider mFingerprintProvider; + @Mock + private GestureAvailabilityDispatcher mGestureAvailabilityDispatcher; + @Mock + private Runnable mInternalCleanupRunnable; + @Mock + private AlarmManager mAlarmManager; + @Mock + private IBiometricsFingerprint mDaemon; + @Mock + private AidlResponseHandler.AidlResponseHandlerCallback mAidlResponseHandlerCallback; + @Mock + private BiometricUtils<Fingerprint> mBiometricUtils; + @Mock + private AuthenticationStateListeners mAuthenticationStateListeners; + + private final TestLooper mLooper = new TestLooper(); + private HidlToAidlSensorAdapter mHidlToAidlSensorAdapter; + private final TestableContext mContext = new TestableContext( + ApplicationProvider.getApplicationContext()); + + private final UserAwareBiometricScheduler.UserSwitchCallback mUserSwitchCallback = + new UserAwareBiometricScheduler.UserSwitchCallback() { + @NonNull + @Override + public StopUserClient<?> getStopUserClient(int userId) { + return new StopUserClient<IBiometricsFingerprint>(mContext, + mHidlToAidlSensorAdapter::getIBiometricsFingerprint, null, USER_ID, + SENSOR_ID, mLogger, mBiometricContext, () -> {}) { + @Override + protected void startHalOperation() { + getCallback().onClientFinished(this, true /* success */); + } + + @Override + public void unableToStart() {} + }; + } + + @NonNull + @Override + public StartUserClient<?, ?> getStartUserClient(int newUserId) { + return new StartUserClient<IBiometricsFingerprint, AidlSession>(mContext, + mHidlToAidlSensorAdapter::getIBiometricsFingerprint, null, + USER_ID, SENSOR_ID, + mLogger, mBiometricContext, + (newUserId1, newUser, halInterfaceVersion) -> + mHidlToAidlSensorAdapter.handleUserChanged(newUserId1)) { + @Override + public void start(@NonNull ClientMonitorCallback callback) { + super.start(callback); + startHalOperation(); + } + + @Override + protected void startHalOperation() { + mUserStartedCallback.onUserStarted(USER_ID, null, 0); + getCallback().onClientFinished(this, true /* success */); + } + + @Override + public void unableToStart() {} + }; + } + };; + + @Before + public void setUp() throws RemoteException { + when(mBiometricContext.getAuthSessionCoordinator()).thenReturn(mAuthSessionCoordinator); + doAnswer((answer) -> { + mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback() + .onEnrollmentProgress(1 /* enrollmentId */, 0 /* remaining */); + return null; + }).when(mDaemon).enroll(any(), anyInt(), anyInt()); + + mContext.addMockSystemService(AlarmManager.class, mAlarmManager); + mContext.getOrCreateTestableResources(); + + final String config = String.format("%d:2:15", SENSOR_ID); + final UserAwareBiometricScheduler scheduler = new UserAwareBiometricScheduler(TAG, + new Handler(mLooper.getLooper()), + BiometricScheduler.SENSOR_TYPE_FP_OTHER, + null /* gestureAvailabilityDispatcher */, + mBiometricService, + () -> USER_ID, + mUserSwitchCallback); + final HidlFingerprintSensorConfig fingerprintSensorConfig = + new HidlFingerprintSensorConfig(); + fingerprintSensorConfig.parse(config, mContext); + mHidlToAidlSensorAdapter = new HidlToAidlSensorAdapter(TAG, + mFingerprintProvider, mContext, new Handler(mLooper.getLooper()), + fingerprintSensorConfig, mLockoutResetDispatcherForSensor, + mGestureAvailabilityDispatcher, mBiometricContext, + false /* resetLockoutRequiresHardwareAuthToken */, + mInternalCleanupRunnable, mAuthSessionCoordinator, mDaemon, + mAidlResponseHandlerCallback); + mHidlToAidlSensorAdapter.init(mGestureAvailabilityDispatcher, + mLockoutResetDispatcherForSensor); + mHidlToAidlSensorAdapter.setScheduler(scheduler); + mHidlToAidlSensorAdapter.handleUserChanged(USER_ID); + } + + @Test + public void lockoutTimedResetViaClient() { + setLockoutTimed(); + + mHidlToAidlSensorAdapter.getScheduler().scheduleClientMonitor( + new FingerprintResetLockoutClient(mContext, + mHidlToAidlSensorAdapter.getLazySession(), + USER_ID, TAG, SENSOR_ID, mLogger, mBiometricContext, HAT, + mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */), + mLockoutResetDispatcherForClient, 0 /* biometricStrength */)); + mLooper.dispatchAll(); + + verify(mAlarmManager).setExact(anyInt(), anyLong(), any()); + verify(mLockoutResetDispatcherForClient).notifyLockoutResetCallbacks(SENSOR_ID); + verify(mLockoutResetDispatcherForSensor).notifyLockoutResetCallbacks(SENSOR_ID); + assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */) + .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE); + verify(mAuthSessionCoordinator).resetLockoutFor(eq(USER_ID), anyInt(), anyLong()); + } + + @Test + public void lockoutTimedResetViaCallback() { + setLockoutTimed(); + + mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback().onLockoutCleared(); + mLooper.dispatchAll(); + + verify(mLockoutResetDispatcherForSensor, times(2)).notifyLockoutResetCallbacks(eq( + SENSOR_ID)); + assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */) + .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE); + verify(mAuthSessionCoordinator).resetLockoutFor(eq(USER_ID), anyInt(), anyLong()); + } + + @Test + public void lockoutPermanentResetViaCallback() { + setLockoutPermanent(); + + mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback().onLockoutCleared(); + mLooper.dispatchAll(); + + verify(mLockoutResetDispatcherForSensor, times(2)).notifyLockoutResetCallbacks(eq( + SENSOR_ID)); + assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */) + .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE); + verify(mAuthSessionCoordinator).resetLockoutFor(eq(USER_ID), anyInt(), anyLong()); + } + + @Test + public void lockoutPermanentResetViaClient() { + setLockoutPermanent(); + + mHidlToAidlSensorAdapter.getScheduler().scheduleClientMonitor( + new FingerprintResetLockoutClient(mContext, + mHidlToAidlSensorAdapter.getLazySession(), + USER_ID, TAG, SENSOR_ID, mLogger, mBiometricContext, HAT, + mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */), + mLockoutResetDispatcherForClient, 0 /* biometricStrength */)); + mLooper.dispatchAll(); + + verify(mAlarmManager, atLeast(1)).setExact(anyInt(), anyLong(), any()); + verify(mLockoutResetDispatcherForClient).notifyLockoutResetCallbacks(SENSOR_ID); + verify(mLockoutResetDispatcherForSensor).notifyLockoutResetCallbacks(SENSOR_ID); + assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */) + .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE); + verify(mAuthSessionCoordinator).resetLockoutFor(eq(USER_ID), anyInt(), anyLong()); + } + + @Test + public void verifyOnEnrollSuccessCallback() { + mHidlToAidlSensorAdapter.getScheduler().scheduleClientMonitor(new FingerprintEnrollClient( + mContext, mHidlToAidlSensorAdapter.getLazySession(), null /* token */, + 1 /* requestId */, null /* listener */, USER_ID, HAT, TAG, mBiometricUtils, + SENSOR_ID, mLogger, mBiometricContext, + mHidlToAidlSensorAdapter.getSensorProperties(), null, null, + mAuthenticationStateListeners, 5 /* maxTemplatesPerUser */, ENROLL_ENROLL)); + mLooper.dispatchAll(); + + verify(mAidlResponseHandlerCallback).onEnrollSuccess(); + } + + private void setLockoutPermanent() { + for (int i = 0; i < 20; i++) { + mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */) + .addFailedAttemptForUser(USER_ID); + } + + assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */) + .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_PERMANENT); + } + + private void setLockoutTimed() { + for (int i = 0; i < 5; i++) { + mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */) + .addFailedAttemptForUser(USER_ID); + } + + assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */) + .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_TIMED); + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/AidlToHidlAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapterTest.java index b78ba82bd8fe..d723e87a62ad 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/AidlToHidlAdapterTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapterTest.java @@ -41,7 +41,7 @@ import org.mockito.junit.MockitoRule; @Presubmit @SmallTest -public class AidlToHidlAdapterTest { +public class HidlToAidlSessionAdapterTest { @Rule public final MockitoRule mockito = MockitoJUnit.rule(); @@ -54,11 +54,11 @@ public class AidlToHidlAdapterTest { private final long mChallenge = 100L; private final int mUserId = 0; - private AidlToHidlAdapter mAidlToHidlAdapter; + private HidlToAidlSessionAdapter mHidlToAidlSessionAdapter; @Before public void setUp() { - mAidlToHidlAdapter = new AidlToHidlAdapter(() -> mSession, mUserId, + mHidlToAidlSessionAdapter = new HidlToAidlSessionAdapter(() -> mSession, mUserId, mAidlResponseHandler); mHardwareAuthToken.timestamp = new Timestamp(); mHardwareAuthToken.mac = new byte[10]; @@ -67,7 +67,7 @@ public class AidlToHidlAdapterTest { @Test public void testGenerateChallenge() throws RemoteException { when(mSession.preEnroll()).thenReturn(mChallenge); - mAidlToHidlAdapter.generateChallenge(); + mHidlToAidlSessionAdapter.generateChallenge(); verify(mSession).preEnroll(); verify(mAidlResponseHandler).onChallengeGenerated(mChallenge); @@ -75,7 +75,7 @@ public class AidlToHidlAdapterTest { @Test public void testRevokeChallenge() throws RemoteException { - mAidlToHidlAdapter.revokeChallenge(mChallenge); + mHidlToAidlSessionAdapter.revokeChallenge(mChallenge); verify(mSession).postEnroll(); verify(mAidlResponseHandler).onChallengeRevoked(0L); @@ -84,9 +84,9 @@ public class AidlToHidlAdapterTest { @Test public void testEnroll() throws RemoteException { final ICancellationSignal cancellationSignal = - mAidlToHidlAdapter.enroll(mHardwareAuthToken); + mHidlToAidlSessionAdapter.enroll(mHardwareAuthToken); - verify(mSession).enroll(any(), anyInt(), eq(AidlToHidlAdapter.ENROLL_TIMEOUT_SEC)); + verify(mSession).enroll(any(), anyInt(), eq(HidlToAidlSessionAdapter.ENROLL_TIMEOUT_SEC)); cancellationSignal.cancel(); @@ -96,7 +96,8 @@ public class AidlToHidlAdapterTest { @Test public void testAuthenticate() throws RemoteException { final int operationId = 2; - final ICancellationSignal cancellationSignal = mAidlToHidlAdapter.authenticate(operationId); + final ICancellationSignal cancellationSignal = mHidlToAidlSessionAdapter.authenticate( + operationId); verify(mSession).authenticate(operationId, mUserId); @@ -107,7 +108,8 @@ public class AidlToHidlAdapterTest { @Test public void testDetectInteraction() throws RemoteException { - final ICancellationSignal cancellationSignal = mAidlToHidlAdapter.detectInteraction(); + final ICancellationSignal cancellationSignal = mHidlToAidlSessionAdapter + .detectInteraction(); verify(mSession).authenticate(0 /* operationId */, mUserId); @@ -118,7 +120,7 @@ public class AidlToHidlAdapterTest { @Test public void testEnumerateEnrollments() throws RemoteException { - mAidlToHidlAdapter.enumerateEnrollments(); + mHidlToAidlSessionAdapter.enumerateEnrollments(); verify(mSession).enumerate(); } @@ -126,7 +128,7 @@ public class AidlToHidlAdapterTest { @Test public void testRemoveEnrollment() throws RemoteException { final int[] enrollmentIds = new int[]{1}; - mAidlToHidlAdapter.removeEnrollments(enrollmentIds); + mHidlToAidlSessionAdapter.removeEnrollments(enrollmentIds); verify(mSession).remove(mUserId, enrollmentIds[0]); } @@ -134,14 +136,14 @@ public class AidlToHidlAdapterTest { @Test public void testRemoveMultipleEnrollments() throws RemoteException { final int[] enrollmentIds = new int[]{1, 2}; - mAidlToHidlAdapter.removeEnrollments(enrollmentIds); + mHidlToAidlSessionAdapter.removeEnrollments(enrollmentIds); verify(mSession).remove(mUserId, 0); } @Test public void testResetLockout() throws RemoteException { - mAidlToHidlAdapter.resetLockout(mHardwareAuthToken); + mHidlToAidlSessionAdapter.resetLockout(mHardwareAuthToken); verify(mAidlResponseHandler).onLockoutCleared(); } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java index edfe1b416f22..071d571adabe 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * 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. @@ -24,10 +24,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.annotation.NonNull; -import android.annotation.Nullable; import android.companion.virtual.camera.VirtualCameraCallback; import android.companion.virtual.camera.VirtualCameraConfig; -import android.companion.virtual.camera.VirtualCameraMetadata; import android.companion.virtual.camera.VirtualCameraStreamConfig; import android.companion.virtualcamera.IVirtualCameraService; import android.companion.virtualcamera.VirtualCameraConfiguration; @@ -156,10 +154,6 @@ public class VirtualCameraControllerTest { @NonNull VirtualCameraStreamConfig streamConfig) {} @Override - public void onProcessCaptureRequest( - int streamId, long frameId, @Nullable VirtualCameraMetadata metadata) {} - - @Override public void onStreamClosed(int streamId) {} }; } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java new file mode 100644 index 000000000000..d9a38eb121ac --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java @@ -0,0 +1,71 @@ +/* + * 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. + */ + +package com.android.server.companion.virtual.camera; + +import android.companion.virtual.camera.VirtualCameraStreamConfig; +import android.graphics.ImageFormat; +import android.graphics.PixelFormat; +import android.os.Parcel; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.google.common.testing.EqualsTester; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@Presubmit +@RunWith(AndroidJUnit4.class) +public class VirtualCameraStreamConfigTest { + + private static final int VGA_WIDTH = 640; + private static final int VGA_HEIGHT = 480; + + private static final int QVGA_WIDTH = 320; + private static final int QVGA_HEIGHT = 240; + + @Test + public void testEquals() { + VirtualCameraStreamConfig vgaYuvStreamConfig = new VirtualCameraStreamConfig(VGA_WIDTH, + VGA_HEIGHT, + ImageFormat.YUV_420_888); + VirtualCameraStreamConfig qvgaYuvStreamConfig = new VirtualCameraStreamConfig(QVGA_WIDTH, + QVGA_HEIGHT, ImageFormat.YUV_420_888); + VirtualCameraStreamConfig vgaRgbaStreamConfig = new VirtualCameraStreamConfig(VGA_WIDTH, + VGA_HEIGHT, PixelFormat.RGBA_8888); + + new EqualsTester() + .addEqualityGroup(vgaYuvStreamConfig, reparcel(vgaYuvStreamConfig)) + .addEqualityGroup(qvgaYuvStreamConfig, reparcel(qvgaYuvStreamConfig)) + .addEqualityGroup(vgaRgbaStreamConfig, reparcel(vgaRgbaStreamConfig)) + .testEquals(); + } + + private static VirtualCameraStreamConfig reparcel(VirtualCameraStreamConfig config) { + Parcel parcel = Parcel.obtain(); + try { + config.writeToParcel(parcel, /* flags= */ 0); + parcel.setDataPosition(0); + return VirtualCameraStreamConfig.CREATOR.createFromParcel(parcel); + } finally { + parcel.recycle(); + } + } + + +} diff --git a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java index 848790381984..89056cc34795 100644 --- a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java +++ b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java @@ -64,17 +64,18 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.eq; import static org.mockito.Matchers.intThat; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; +import android.annotation.DurationMillisLong; import android.annotation.NonNull; import android.app.ActivityManager; import android.app.usage.AppStandbyInfo; import android.app.usage.UsageEvents; +import android.app.usage.UsageStatsManager; import android.app.usage.UsageStatsManagerInternal; import android.appwidget.AppWidgetManager; import android.content.Context; @@ -99,7 +100,6 @@ import android.util.Pair; import android.view.Display; import androidx.test.InstrumentationRegistry; -import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -110,6 +110,7 @@ import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -125,6 +126,7 @@ import java.util.Random; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.function.BooleanSupplier; import java.util.stream.Collectors; /** @@ -175,6 +177,9 @@ public class AppStandbyControllerTests { private static final int ASSERT_RETRY_ATTEMPTS = 20; private static final int ASSERT_RETRY_DELAY_MILLISECONDS = 500; + @DurationMillisLong + private static final long FLUSH_TIMEOUT_MILLISECONDS = 5_000; + /** Mock variable used in {@link MyInjector#isPackageInstalled(String, int, int)} */ private static boolean isPackageInstalled = true; @@ -563,6 +568,7 @@ public class AppStandbyControllerTests { mInjector = new MyInjector(myContext, Looper.getMainLooper()); mController = setupController(); setupInitialUsageHistory(); + flushHandler(mController); } @After @@ -571,12 +577,9 @@ public class AppStandbyControllerTests { } @Test - @FlakyTest(bugId = 185169504) public void testBoundWidgetPackageExempt() throws Exception { assumeTrue(mInjector.getContext().getSystemService(AppWidgetManager.class) != null); - assertEquals(STANDBY_BUCKET_ACTIVE, - mController.getAppStandbyBucket(PACKAGE_EXEMPTED_1, USER_ID, - mInjector.mElapsedRealtime, false)); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_EXEMPTED_1); } @Test @@ -654,7 +657,6 @@ public class AppStandbyControllerTests { } @Test - @FlakyTest(bugId = 185169504) public void testIsAppIdle_Charging() throws Exception { TestParoleListener paroleListener = new TestParoleListener(); mController.addListener(paroleListener); @@ -662,7 +664,7 @@ public class AppStandbyControllerTests { setChargingState(mController, false); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM); - assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false)); assertFalse(mController.isInParole()); @@ -671,7 +673,7 @@ public class AppStandbyControllerTests { setChargingState(mController, true); paroleListener.awaitOnLatch(2000); assertTrue(paroleListener.getParoleState()); - assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); assertFalse(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); assertFalse(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false)); assertTrue(mController.isInParole()); @@ -680,14 +682,13 @@ public class AppStandbyControllerTests { setChargingState(mController, false); paroleListener.awaitOnLatch(2000); assertFalse(paroleListener.getParoleState()); - assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false)); assertFalse(mController.isInParole()); } @Test - @FlakyTest(bugId = 185169504) public void testIsAppIdle_Enabled() throws Exception { setChargingState(mController, false); TestParoleListener paroleListener = new TestParoleListener(); @@ -696,7 +697,7 @@ public class AppStandbyControllerTests { setAppIdleEnabled(mController, true); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM); - assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false)); @@ -711,7 +712,7 @@ public class AppStandbyControllerTests { setAppIdleEnabled(mController, true); paroleListener.awaitOnLatch(2000); assertFalse(paroleListener.getParoleState()); - assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false)); } @@ -723,6 +724,7 @@ public class AppStandbyControllerTests { private void assertTimeout(AppStandbyController controller, long elapsedTime, int bucket, int userId) { mInjector.mElapsedRealtime = elapsedTime; + flushHandler(controller); controller.checkIdleStates(userId); assertEquals(bucket, controller.getAppStandbyBucket(PACKAGE_1, userId, mInjector.mElapsedRealtime, @@ -744,50 +746,85 @@ public class AppStandbyControllerTests { } private int getStandbyBucket(int userId, AppStandbyController controller, String packageName) { + flushHandler(controller); return controller.getAppStandbyBucket(packageName, userId, mInjector.mElapsedRealtime, true); } + private List<AppStandbyInfo> getStandbyBuckets(int userId) { + flushHandler(mController); + return mController.getAppStandbyBuckets(userId); + } + private int getStandbyBucketReason(String packageName) { + flushHandler(mController); return mController.getAppStandbyBucketReason(packageName, USER_ID, mInjector.mElapsedRealtime); } - private void assertBucket(int bucket) throws InterruptedException { - assertBucket(bucket, PACKAGE_1); + private void waitAndAssertBucket(int bucket, String pkg) { + waitAndAssertBucket(mController, bucket, pkg); } - private void assertBucket(int bucket, String pkg) throws InterruptedException { - int retries = 0; - do { - if (bucket == getStandbyBucket(mController, pkg)) { - // Success - return; - } - Thread.sleep(ASSERT_RETRY_DELAY_MILLISECONDS); - retries++; - } while(retries < ASSERT_RETRY_ATTEMPTS); - // try one last time - assertEquals(bucket, getStandbyBucket(mController, pkg)); + private void waitAndAssertBucket(AppStandbyController controller, int bucket, String pkg) { + StringBuilder sb = new StringBuilder(); + sb.append(pkg); + sb.append(" was not in the "); + sb.append(UsageStatsManager.standbyBucketToString(bucket)); + sb.append(" ("); + sb.append(bucket); + sb.append(") bucket."); + waitAndAssertBucket(sb.toString(), controller, bucket, pkg); + } + + private void waitAndAssertBucket(String msg, int bucket, String pkg) { + waitAndAssertBucket(msg, mController, bucket, pkg); } - private void assertNotBucket(int bucket) throws InterruptedException { - final String pkg = PACKAGE_1; + private void waitAndAssertBucket(String msg, AppStandbyController controller, int bucket, + String pkg) { + waitAndAssertBucket(msg, controller, bucket, USER_ID, pkg); + } + + private void waitAndAssertBucket(String msg, AppStandbyController controller, int bucket, + int userId, + String pkg) { + waitUntil(() -> bucket == getStandbyBucket(userId, controller, pkg)); + assertEquals(msg, bucket, getStandbyBucket(userId, controller, pkg)); + } + + private void waitAndAssertNotBucket(int bucket, String pkg) { + waitAndAssertNotBucket(mController, bucket, pkg); + } + + private void waitAndAssertNotBucket(AppStandbyController controller, int bucket, String pkg) { + waitUntil(() -> bucket != getStandbyBucket(controller, pkg)); + assertNotEquals(bucket, getStandbyBucket(controller, pkg)); + } + + private void waitAndAssertLastNoteEvent(int event) { + waitUntil(() -> { + flushHandler(mController); + return event == mInjector.mLastNoteEvent; + }); + assertEquals(event, mInjector.mLastNoteEvent); + } + + // Waits until condition is true or times out. + private void waitUntil(BooleanSupplier resultSupplier) { int retries = 0; do { - if (bucket != getStandbyBucket(mController, pkg)) { - // Success - return; + if (resultSupplier.getAsBoolean()) return; + try { + Thread.sleep(ASSERT_RETRY_DELAY_MILLISECONDS); + } catch (InterruptedException ie) { + // Do nothing } - Thread.sleep(ASSERT_RETRY_DELAY_MILLISECONDS); retries++; - } while(retries < ASSERT_RETRY_ATTEMPTS); - // try one last time - assertNotEquals(bucket, getStandbyBucket(mController, pkg)); + } while (retries < ASSERT_RETRY_ATTEMPTS); } @Test - @FlakyTest(bugId = 185169504) public void testBuckets() throws Exception { assertTimeout(mController, 0, STANDBY_BUCKET_NEVER); @@ -820,14 +857,13 @@ public class AppStandbyControllerTests { } @Test - @FlakyTest(bugId = 185169504) public void testSetAppStandbyBucket() throws Exception { // For a known package, standby bucket should be set properly reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); mInjector.mElapsedRealtime = HOUR_MS; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE, REASON_MAIN_TIMEOUT); - assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // For an unknown package, standby bucket should not be set, hence NEVER is returned // Ensure the unknown package is not already in history by removing it @@ -836,21 +872,20 @@ public class AppStandbyControllerTests { mController.setAppStandbyBucket(PACKAGE_UNKNOWN, USER_ID, STANDBY_BUCKET_ACTIVE, REASON_MAIN_TIMEOUT); isPackageInstalled = true; // Reset mocked variable for other tests - assertEquals(STANDBY_BUCKET_NEVER, getStandbyBucket(mController, PACKAGE_UNKNOWN)); + waitAndAssertBucket(STANDBY_BUCKET_NEVER, PACKAGE_UNKNOWN); } @Test - @FlakyTest(bugId = 185169504) public void testAppStandbyBucketOnInstallAndUninstall() throws Exception { // On package install, standby bucket should be ACTIVE reportEvent(mController, USER_INTERACTION, 0, PACKAGE_UNKNOWN); - assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_UNKNOWN)); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_UNKNOWN); // On uninstall, package should not exist in history and should return a NEVER bucket mController.clearAppIdleForPackage(PACKAGE_UNKNOWN, USER_ID); - assertEquals(STANDBY_BUCKET_NEVER, getStandbyBucket(mController, PACKAGE_UNKNOWN)); + waitAndAssertBucket(STANDBY_BUCKET_NEVER, PACKAGE_UNKNOWN); // Ensure uninstalled app is not in history - List<AppStandbyInfo> buckets = mController.getAppStandbyBuckets(USER_ID); + List<AppStandbyInfo> buckets = getStandbyBuckets(USER_ID); for(AppStandbyInfo bucket : buckets) { if (bucket.mPackageName.equals(PACKAGE_UNKNOWN)) { fail("packageName found in app idle history after uninstall."); @@ -859,7 +894,6 @@ public class AppStandbyControllerTests { } @Test - @FlakyTest(bugId = 185169504) public void testScreenTimeAndBuckets() throws Exception { mInjector.setDisplayOn(false); @@ -876,22 +910,21 @@ public class AppStandbyControllerTests { // RARE bucket, should fail because the screen wasn't ON. mInjector.mElapsedRealtime = RARE_THRESHOLD + 1; mController.checkIdleStates(USER_ID); - assertNotEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertNotBucket(STANDBY_BUCKET_RARE, PACKAGE_1); mInjector.setDisplayOn(true); assertTimeout(mController, RARE_THRESHOLD + 2 * HOUR_MS + 1, STANDBY_BUCKET_RARE); } @Test - @FlakyTest(bugId = 185169504) public void testForcedIdle() throws Exception { mController.forceIdleState(PACKAGE_1, USER_ID, true); - assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); mController.forceIdleState(PACKAGE_1, USER_ID, false); - assertEquals(STANDBY_BUCKET_ACTIVE, mController.getAppStandbyBucket(PACKAGE_1, USER_ID, 0, - true)); + + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); assertFalse(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); } @@ -901,15 +934,15 @@ public class AppStandbyControllerTests { .onPropertiesChanged(mInjector.getDeviceConfigProperties()); reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); - assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mInjector.mElapsedRealtime = 1; rearmQuotaBumpLatch(PACKAGE_1, USER_ID); reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1); - assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mController.forceIdleState(PACKAGE_1, USER_ID, true); reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1); - assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1); assertFalse(mQuotaBumpLatch.await(1, TimeUnit.SECONDS)); } @@ -917,9 +950,10 @@ public class AppStandbyControllerTests { public void testNotificationEvent_bucketPromotion_changePromotedBucket() throws Exception { mInjector.mPropertiesChangedListener .onPropertiesChanged(mInjector.getDeviceConfigProperties()); + mInjector.mElapsedRealtime += RARE_THRESHOLD + 1; mController.forceIdleState(PACKAGE_1, USER_ID, true); reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1); - assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1); // TODO: Avoid hardcoding these string constants. mInjector.mSettingsBuilder.setInt("notification_seen_promoted_bucket", @@ -928,11 +962,10 @@ public class AppStandbyControllerTests { mInjector.getDeviceConfigProperties()); mController.forceIdleState(PACKAGE_1, USER_ID, true); reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1); - assertEquals(STANDBY_BUCKET_FREQUENT, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1); } @Test - @FlakyTest(bugId = 185169504) public void testNotificationEvent_quotaBump() throws Exception { mInjector.mSettingsBuilder .setBoolean("trigger_quota_bump_on_notification_seen", true); @@ -942,7 +975,7 @@ public class AppStandbyControllerTests { .onPropertiesChanged(mInjector.getDeviceConfigProperties()); reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); - assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mInjector.mElapsedRealtime = RARE_THRESHOLD * 2; setAndAssertBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM); @@ -951,83 +984,80 @@ public class AppStandbyControllerTests { reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1); assertTrue(mQuotaBumpLatch.await(1, TimeUnit.SECONDS)); - assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); } @Test - @FlakyTest(bugId = 185169504) public void testSlicePinnedEvent() throws Exception { reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); - assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mInjector.mElapsedRealtime = 1; reportEvent(mController, SLICE_PINNED, mInjector.mElapsedRealtime, PACKAGE_1); - assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mController.forceIdleState(PACKAGE_1, USER_ID, true); reportEvent(mController, SLICE_PINNED, mInjector.mElapsedRealtime, PACKAGE_1); - assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1); } @Test - @FlakyTest(bugId = 185169504) public void testSlicePinnedPrivEvent() throws Exception { mController.forceIdleState(PACKAGE_1, USER_ID, true); reportEvent(mController, SLICE_PINNED_PRIV, mInjector.mElapsedRealtime, PACKAGE_1); - assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); } @Test - @FlakyTest(bugId = 185169504) public void testPredictionTimedOut() throws Exception { // Set it to timeout or usage, so that prediction can override it mInjector.mElapsedRealtime = HOUR_MS; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_TIMEOUT); - assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE, REASON_MAIN_PREDICTED); - assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // Fast forward 12 hours mInjector.mElapsedRealtime += WORKING_SET_THRESHOLD; mController.checkIdleStates(USER_ID); // Should still be in predicted bucket, since prediction timeout is 1 day since prediction - assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // Fast forward two more hours mInjector.mElapsedRealtime += 2 * HOUR_MS; mController.checkIdleStates(USER_ID); // Should have now applied prediction timeout - assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1); // Fast forward RARE bucket mInjector.mElapsedRealtime += RARE_THRESHOLD; mController.checkIdleStates(USER_ID); // Should continue to apply prediction timeout - assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); } @Test - @FlakyTest(bugId = 185169504) + @Ignore("b/317086276") public void testOverrides() throws Exception { // Can force to NEVER mInjector.mElapsedRealtime = HOUR_MS; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_NEVER, REASON_MAIN_FORCED_BY_USER); - assertEquals(STANDBY_BUCKET_NEVER, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_NEVER, PACKAGE_1); // Prediction can't override FORCED reasons mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET, REASON_MAIN_PREDICTED); - assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT, REASON_MAIN_FORCED_BY_USER); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET, REASON_MAIN_PREDICTED); - assertEquals(STANDBY_BUCKET_FREQUENT, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1); // Prediction can't override NEVER mInjector.mElapsedRealtime = 2 * HOUR_MS; @@ -1035,115 +1065,114 @@ public class AppStandbyControllerTests { REASON_MAIN_DEFAULT); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE, REASON_MAIN_PREDICTED); - assertEquals(STANDBY_BUCKET_NEVER, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_NEVER, PACKAGE_1); // Prediction can't set to NEVER mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE, REASON_MAIN_USAGE); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_NEVER, REASON_MAIN_PREDICTED); - assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // Prediction can't remove from RESTRICTED mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_FORCED_BY_USER); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET, REASON_MAIN_PREDICTED); - assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_FORCED_BY_SYSTEM); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET, REASON_MAIN_PREDICTED); - assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); // Force from user can remove from RESTRICTED mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_FORCED_BY_USER); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET, REASON_MAIN_FORCED_BY_USER); - assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_FORCED_BY_SYSTEM); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET, REASON_MAIN_FORCED_BY_USER); - assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1); // Force from system can remove from RESTRICTED if it was put it in due to system mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_FORCED_BY_SYSTEM); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET, REASON_MAIN_FORCED_BY_SYSTEM); - assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_FORCED_BY_USER); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET, REASON_MAIN_FORCED_BY_SYSTEM); - assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_PREDICTED); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET, REASON_MAIN_FORCED_BY_SYSTEM); - assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); // Non-user usage can't remove from RESTRICTED mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_FORCED_BY_SYSTEM); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET, REASON_MAIN_USAGE); - assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET, REASON_MAIN_USAGE | REASON_SUB_USAGE_SYSTEM_INTERACTION); - assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET, REASON_MAIN_USAGE | REASON_SUB_USAGE_SYNC_ADAPTER); - assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET, REASON_MAIN_USAGE | REASON_SUB_USAGE_EXEMPTED_SYNC_SCHEDULED_NON_DOZE); - assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); // Explicit user usage can remove from RESTRICTED mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_FORCED_BY_USER); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET, REASON_MAIN_USAGE | REASON_SUB_USAGE_USER_INTERACTION); - assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_FORCED_BY_SYSTEM); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE, REASON_MAIN_USAGE | REASON_SUB_USAGE_MOVE_TO_FOREGROUND); - assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); } @Test - @FlakyTest(bugId = 185169504) public void testTimeout() throws Exception { reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mInjector.mElapsedRealtime = 2000; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT, REASON_MAIN_PREDICTED); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // bucketing works after timeout mInjector.mElapsedRealtime = mController.mPredictionTimeoutMillis - 100; mController.checkIdleStates(USER_ID); // Use recent prediction - assertBucket(STANDBY_BUCKET_FREQUENT); + waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1); + waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1); // Way past prediction timeout, use system thresholds mInjector.mElapsedRealtime = RARE_THRESHOLD; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_RARE); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); } /** Test that timeouts still work properly even if invalid configuration values are set. */ @Test - @FlakyTest(bugId = 185169504) public void testTimeout_InvalidThresholds() throws Exception { mInjector.mSettingsBuilder .setLong("screen_threshold_active", -1) @@ -1161,19 +1190,19 @@ public class AppStandbyControllerTests { reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mInjector.mElapsedRealtime = HOUR_MS; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_FREQUENT); + waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1); mInjector.mElapsedRealtime = 2 * HOUR_MS; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_RARE); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); mInjector.mElapsedRealtime = 4 * HOUR_MS; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); } /** @@ -1181,74 +1210,72 @@ public class AppStandbyControllerTests { * timeout has passed. */ @Test - @FlakyTest(bugId = 185169504) + @Ignore("b/317086276") public void testTimeoutBeforeRestricted() throws Exception { reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mInjector.mElapsedRealtime += WORKING_SET_THRESHOLD; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_FORCED_BY_SYSTEM); // Bucket shouldn't change - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // bucketing works after timeout mInjector.mElapsedRealtime += DAY_MS; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_FORCED_BY_SYSTEM); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); // Way past all timeouts. Make sure timeout processing doesn't raise bucket. mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); } /** * Test that an app is put into the RESTRICTED bucket after enough time has passed. */ @Test - @FlakyTest(bugId = 185169504) public void testRestrictedDelay() throws Exception { reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mInjector.mElapsedRealtime += mInjector.getAutoRestrictedBucketDelayMs() - 5000; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_FORCED_BY_SYSTEM); // Bucket shouldn't change - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // bucketing works after timeout mInjector.mElapsedRealtime += 6000; Thread.sleep(6000); // Enough time has passed. The app should automatically be put into the RESTRICTED bucket. - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); } /** * Test that an app is put into the RESTRICTED bucket after enough time has passed. */ @Test - @FlakyTest(bugId = 185169504) public void testRestrictedDelay_DelayChange() throws Exception { reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mInjector.mAutoRestrictedBucketDelayMs = 2 * HOUR_MS; mInjector.mElapsedRealtime += 2 * HOUR_MS - 5000; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_FORCED_BY_SYSTEM); // Bucket shouldn't change - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // bucketing works after timeout mInjector.mElapsedRealtime += 6000; Thread.sleep(6000); // Enough time has passed. The app should automatically be put into the RESTRICTED bucket. - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); } /** @@ -1256,36 +1283,35 @@ public class AppStandbyControllerTests { * a low bucket after the RESTRICTED timeout. */ @Test - @FlakyTest(bugId = 185169504) public void testRestrictedTimeoutOverridesRestoredLowBucketPrediction() throws Exception { reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // Predict to RARE Not long enough to time out into RESTRICTED. mInjector.mElapsedRealtime += RARE_THRESHOLD; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_PREDICTED); - assertBucket(STANDBY_BUCKET_RARE); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); // Add a short timeout event mInjector.mElapsedRealtime += 1000; reportEvent(mController, SYSTEM_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mInjector.mElapsedRealtime += 1000; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // Long enough that it could have timed out into RESTRICTED. Instead of reverting to // predicted RARE, should go into RESTRICTED mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); // Ensure that prediction can still raise it out despite this override. mInjector.mElapsedRealtime += 1; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE, REASON_MAIN_PREDICTED); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); } /** @@ -1293,7 +1319,6 @@ public class AppStandbyControllerTests { * a low bucket after the RESTRICTED timeout. */ @Test - @FlakyTest(bugId = 185169504) public void testRestrictedTimeoutOverridesPredictionLowBucket() throws Exception { reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1); @@ -1301,7 +1326,7 @@ public class AppStandbyControllerTests { mInjector.mElapsedRealtime += RARE_THRESHOLD; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_PREDICTED); - assertBucket(STANDBY_BUCKET_RARE); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); mInjector.mElapsedRealtime += 1; reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1); @@ -1310,10 +1335,10 @@ public class AppStandbyControllerTests { mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE, REASON_MAIN_PREDICTED); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_PREDICTED); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); } /** @@ -1321,261 +1346,250 @@ public class AppStandbyControllerTests { * interaction. */ @Test - @FlakyTest(bugId = 185169504) public void testSystemInteractionOverridesRestrictedTimeout() throws Exception { reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // Long enough that it could have timed out into RESTRICTED. mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); // Report system interaction. mInjector.mElapsedRealtime += 1000; reportEvent(mController, SYSTEM_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1); // Ensure that it's raised out of RESTRICTED for the system interaction elevation duration. - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mInjector.mElapsedRealtime += 1000; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // Elevation duration over. Should fall back down. mInjector.mElapsedRealtime += 10 * MINUTE_MS; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); } @Test - @FlakyTest(bugId = 185169504) public void testPredictionRaiseFromRestrictedTimeout_highBucket() throws Exception { reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1); // Way past all timeouts. App times out into RESTRICTED bucket. mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); // Since the app timed out into RESTRICTED, prediction should be able to remove from the // bucket. mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE, REASON_MAIN_PREDICTED); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); } @Test - @FlakyTest(bugId = 185169504) public void testPredictionRaiseFromRestrictedTimeout_lowBucket() throws Exception { reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1); // Way past all timeouts. App times out into RESTRICTED bucket. mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); // Prediction into a low bucket means no expectation of the app being used, so we shouldn't // elevate the app from RESTRICTED. mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_PREDICTED); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); } @Test - @FlakyTest(bugId = 185169504) public void testCascadingTimeouts() throws Exception { mInjector.mPropertiesChangedListener .onPropertiesChanged(mInjector.getDeviceConfigProperties()); reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); reportEvent(mController, NOTIFICATION_SEEN, 1000, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET, REASON_MAIN_PREDICTED); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mInjector.mElapsedRealtime = 2000 + mController.mStrongUsageTimeoutMillis; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT, REASON_MAIN_PREDICTED); - assertBucket(STANDBY_BUCKET_WORKING_SET); + waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1); mInjector.mElapsedRealtime = 2000 + mController.mNotificationSeenTimeoutMillis; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT, REASON_MAIN_PREDICTED); - assertBucket(STANDBY_BUCKET_FREQUENT); + waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1); } @Test - @FlakyTest(bugId = 185169504) public void testOverlappingTimeouts() throws Exception { mInjector.mPropertiesChangedListener .onPropertiesChanged(mInjector.getDeviceConfigProperties()); reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); reportEvent(mController, NOTIFICATION_SEEN, 1000, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // Overlapping USER_INTERACTION before previous one times out reportEvent(mController, USER_INTERACTION, mController.mStrongUsageTimeoutMillis - 1000, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // Still in ACTIVE after first USER_INTERACTION times out mInjector.mElapsedRealtime = mController.mStrongUsageTimeoutMillis + 1000; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT, REASON_MAIN_PREDICTED); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // Both timed out, so NOTIFICATION_SEEN timeout should be effective mInjector.mElapsedRealtime = mController.mStrongUsageTimeoutMillis * 2 + 2000; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT, REASON_MAIN_PREDICTED); - assertBucket(STANDBY_BUCKET_WORKING_SET); + waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1); mInjector.mElapsedRealtime = mController.mNotificationSeenTimeoutMillis + 2000; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_PREDICTED); - assertBucket(STANDBY_BUCKET_RARE); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); } @Test - @FlakyTest(bugId = 185169504) public void testSystemInteractionTimeout() throws Exception { reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); // Fast forward to RARE mInjector.mElapsedRealtime = RARE_THRESHOLD + 100; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_RARE); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); // Trigger a SYSTEM_INTERACTION and verify bucket reportEvent(mController, SYSTEM_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // Verify it's still in ACTIVE close to end of timeout mInjector.mElapsedRealtime += mController.mSystemInteractionTimeoutMillis - 100; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // Verify bucket moves to RARE after timeout mInjector.mElapsedRealtime += 200; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_RARE); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); } @Test - @FlakyTest(bugId = 185169504) public void testInitialForegroundServiceTimeout() throws Exception { mInjector.mElapsedRealtime = 1 * RARE_THRESHOLD + 100; // Make sure app is in NEVER bucket mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_NEVER, REASON_MAIN_FORCED_BY_USER); mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_NEVER); + waitAndAssertBucket(STANDBY_BUCKET_NEVER, PACKAGE_1); mInjector.mElapsedRealtime += 100; // Trigger a FOREGROUND_SERVICE_START and verify bucket reportEvent(mController, FOREGROUND_SERVICE_START, mInjector.mElapsedRealtime, PACKAGE_1); mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // Verify it's still in ACTIVE close to end of timeout mInjector.mElapsedRealtime += mController.mInitialForegroundServiceStartTimeoutMillis - 100; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // Verify bucket moves to RARE after timeout mInjector.mElapsedRealtime += 200; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_RARE); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); // Trigger a FOREGROUND_SERVICE_START again reportEvent(mController, FOREGROUND_SERVICE_START, mInjector.mElapsedRealtime, PACKAGE_1); mController.checkIdleStates(USER_ID); // Bucket should not be immediately elevated on subsequent service starts - assertBucket(STANDBY_BUCKET_RARE); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); } @Test - @FlakyTest(bugId = 185169504) public void testPredictionNotOverridden() throws Exception { mInjector.mPropertiesChangedListener .onPropertiesChanged(mInjector.getDeviceConfigProperties()); reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mInjector.mElapsedRealtime = WORKING_SET_THRESHOLD - 1000; reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // Falls back to WORKING_SET mInjector.mElapsedRealtime += 5000; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_WORKING_SET); + waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1); // Predict to ACTIVE mInjector.mElapsedRealtime += 1000; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE, REASON_MAIN_PREDICTED); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // CheckIdleStates should not change the prediction mInjector.mElapsedRealtime += 1000; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); } @Test - @FlakyTest(bugId = 185169504) public void testPredictionStrikesBack() throws Exception { reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // Predict to FREQUENT mInjector.mElapsedRealtime = RARE_THRESHOLD; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT, REASON_MAIN_PREDICTED); - assertBucket(STANDBY_BUCKET_FREQUENT); + waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1); // Add a short timeout event mInjector.mElapsedRealtime += 1000; reportEvent(mController, SYSTEM_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mInjector.mElapsedRealtime += 1000; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_ACTIVE); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); // Verify it reverted to predicted mInjector.mElapsedRealtime += WORKING_SET_THRESHOLD / 2; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_FREQUENT); + waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1); } @Test - @FlakyTest(bugId = 185169504) public void testSystemForcedFlags_NotAddedForUserForce() throws Exception { final int expectedReason = REASON_MAIN_FORCED_BY_USER; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_FORCED_BY_USER); - assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); assertEquals(expectedReason, getStandbyBucketReason(PACKAGE_1)); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_ABUSE); - assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); assertEquals(expectedReason, getStandbyBucketReason(PACKAGE_1)); } @Test - @FlakyTest(bugId = 185169504) public void testSystemForcedFlags_AddedForSystemForce() throws Exception { mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE, REASON_MAIN_DEFAULT); @@ -1584,13 +1598,13 @@ public class AppStandbyControllerTests { mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE); - assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); assertEquals(REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE, getStandbyBucketReason(PACKAGE_1)); mController.restrictApp(PACKAGE_1, USER_ID, REASON_SUB_FORCED_SYSTEM_FLAG_ABUSE); - assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); // Flags should be combined assertEquals(REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE @@ -1598,7 +1612,6 @@ public class AppStandbyControllerTests { } @Test - @FlakyTest(bugId = 185169504) public void testSystemForcedFlags_SystemForceChangesBuckets() throws Exception { mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE, REASON_MAIN_DEFAULT); @@ -1607,14 +1620,14 @@ public class AppStandbyControllerTests { mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT, REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE); - assertEquals(STANDBY_BUCKET_FREQUENT, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1); assertEquals(REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE, getStandbyBucketReason(PACKAGE_1)); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT, REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_ABUSE); - assertEquals(STANDBY_BUCKET_FREQUENT, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1); // Flags should be combined assertEquals(REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE @@ -1623,20 +1636,19 @@ public class AppStandbyControllerTests { mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY); - assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); // Flags should be combined assertEquals(REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY, getStandbyBucketReason(PACKAGE_1)); mController.restrictApp(PACKAGE_1, USER_ID, REASON_SUB_FORCED_SYSTEM_FLAG_UNDEFINED); - assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); // Flags should not be combined since the bucket changed. assertEquals(REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_UNDEFINED, getStandbyBucketReason(PACKAGE_1)); } @Test - @FlakyTest(bugId = 185169504) public void testRestrictApp_MainReason() throws Exception { mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE, REASON_MAIN_DEFAULT); @@ -1644,11 +1656,11 @@ public class AppStandbyControllerTests { mController.restrictApp(PACKAGE_1, USER_ID, REASON_MAIN_PREDICTED, 0); // Call should be ignored. - assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mController.restrictApp(PACKAGE_1, USER_ID, REASON_MAIN_FORCED_BY_USER, 0); // Call should go through - assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); } @Test @@ -1724,15 +1736,15 @@ public class AppStandbyControllerTests { } @Test - @FlakyTest(bugId = 185169504) public void testUserInteraction_CrossProfile() throws Exception { mInjector.mRunningUsers = new int[] {USER_ID, USER_ID2, USER_ID3}; mInjector.mCrossProfileTargets = Arrays.asList(USER_HANDLE_USER2); reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); - assertEquals("Cross profile connected package bucket should be elevated on usage", - STANDBY_BUCKET_ACTIVE, getStandbyBucket(USER_ID2, mController, PACKAGE_1)); - assertEquals("Not Cross profile connected package bucket should not be elevated on usage", - STANDBY_BUCKET_NEVER, getStandbyBucket(USER_ID3, mController, PACKAGE_1)); + waitAndAssertBucket("Cross profile connected package bucket should be elevated on usage", + mController, STANDBY_BUCKET_ACTIVE, USER_ID2, PACKAGE_1); + waitAndAssertBucket( + "Not Cross profile connected package bucket should not be elevated on usage", + mController, STANDBY_BUCKET_NEVER, USER_ID3, PACKAGE_1); assertTimeout(mController, WORKING_SET_THRESHOLD - 1, STANDBY_BUCKET_ACTIVE, USER_ID); assertTimeout(mController, WORKING_SET_THRESHOLD - 1, STANDBY_BUCKET_ACTIVE, USER_ID2); @@ -1742,51 +1754,50 @@ public class AppStandbyControllerTests { mInjector.mCrossProfileTargets = Collections.emptyList(); reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); - assertEquals("No longer cross profile connected package bucket should not be " - + "elevated on usage", - STANDBY_BUCKET_WORKING_SET, getStandbyBucket(USER_ID2, mController, PACKAGE_1)); + waitAndAssertBucket("No longer cross profile connected package bucket should not be " + + "elevated on usage", mController, STANDBY_BUCKET_WORKING_SET, USER_ID2, + PACKAGE_1); } @Test - @FlakyTest(bugId = 185169504) public void testUnexemptedSyncScheduled() throws Exception { rearmLatch(PACKAGE_1); mController.addListener(mListener); - assertEquals("Test package did not start in the Never bucket", STANDBY_BUCKET_NEVER, - getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket("Test package did not start in the Never bucket", STANDBY_BUCKET_NEVER, + PACKAGE_1); mController.postReportSyncScheduled(PACKAGE_1, USER_ID, false); mStateChangedLatch.await(1000, TimeUnit.MILLISECONDS); - assertEquals("Unexempted sync scheduled should bring the package out of the Never bucket", - STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket( + "Unexempted sync scheduled should bring the package out of the Never bucket", + STANDBY_BUCKET_WORKING_SET, PACKAGE_1); setAndAssertBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM); rearmLatch(PACKAGE_1); mController.postReportSyncScheduled(PACKAGE_1, USER_ID, false); mStateChangedLatch.await(1000, TimeUnit.MILLISECONDS); - assertEquals("Unexempted sync scheduled should not elevate a non Never bucket", - STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket("Unexempted sync scheduled should not elevate a non Never bucket", + STANDBY_BUCKET_RARE, PACKAGE_1); } @Test - @FlakyTest(bugId = 185169504) public void testExemptedSyncScheduled() throws Exception { setAndAssertBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM); mInjector.mDeviceIdleMode = true; rearmLatch(PACKAGE_1); mController.postReportSyncScheduled(PACKAGE_1, USER_ID, true); mStateChangedLatch.await(1000, TimeUnit.MILLISECONDS); - assertEquals("Exempted sync scheduled in doze should set bucket to working set", - STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket("Exempted sync scheduled in doze should set bucket to working set", + STANDBY_BUCKET_WORKING_SET, PACKAGE_1); setAndAssertBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM); mInjector.mDeviceIdleMode = false; rearmLatch(PACKAGE_1); mController.postReportSyncScheduled(PACKAGE_1, USER_ID, true); mStateChangedLatch.await(1000, TimeUnit.MILLISECONDS); - assertEquals("Exempted sync scheduled while not in doze should set bucket to active", - STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket("Exempted sync scheduled while not in doze should set bucket to active", + STANDBY_BUCKET_ACTIVE, PACKAGE_1); } @Test @@ -1796,14 +1807,14 @@ public class AppStandbyControllerTests { reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1); mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4; mController.checkIdleStates(USER_ID); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID2); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); mController.maybeUnrestrictBuggyApp("com.random.package", USER_ID); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); // Updates shouldn't change bucket if the app was forced by the system for a non-buggy // reason. @@ -1814,11 +1825,11 @@ public class AppStandbyControllerTests { | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE); mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID2); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); mController.maybeUnrestrictBuggyApp("com.random.package", USER_ID); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); // Updates should change bucket if the app was forced by the system for a buggy reason. reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1); @@ -1827,11 +1838,11 @@ public class AppStandbyControllerTests { REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY); mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID2); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); mController.maybeUnrestrictBuggyApp("com.random.package", USER_ID); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID); - assertNotEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertNotBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); // Updates shouldn't change bucket if the app was forced by the system for more than just // a buggy reason. @@ -1842,13 +1853,13 @@ public class AppStandbyControllerTests { | REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY); mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); assertEquals(REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_ABUSE, getStandbyBucketReason(PACKAGE_1)); mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID2); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); mController.maybeUnrestrictBuggyApp("com.random.package", USER_ID); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); // Updates shouldn't change bucket if the app was forced by the user. reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1); @@ -1857,11 +1868,11 @@ public class AppStandbyControllerTests { REASON_MAIN_FORCED_BY_USER); mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID2); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); mController.maybeUnrestrictBuggyApp("com.random.package", USER_ID); - assertBucket(STANDBY_BUCKET_RESTRICTED); + waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1); } @Test @@ -1876,37 +1887,37 @@ public class AppStandbyControllerTests { mController.setAppStandbyBucket(PACKAGE_SYSTEM_HEADFULL, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_TIMEOUT); - assertBucket(STANDBY_BUCKET_RARE, PACKAGE_SYSTEM_HEADFULL); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_SYSTEM_HEADFULL); // Make sure headless system apps don't get lowered. mController.setAppStandbyBucket(PACKAGE_SYSTEM_HEADLESS, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_TIMEOUT); - assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_SYSTEM_HEADLESS); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_SYSTEM_HEADLESS); // Package 1 doesn't have activities and is headless, but is not a system app, so it can // be lowered. mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_TIMEOUT); - assertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); } @Test public void testWellbeingAppElevated() throws Exception { reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_WELLBEING); - assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_WELLBEING); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_WELLBEING); reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD; // Make sure the default wellbeing app does not get lowered below WORKING_SET. mController.setAppStandbyBucket(PACKAGE_WELLBEING, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_TIMEOUT); - assertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_WELLBEING); + waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_WELLBEING); // A non default wellbeing app should be able to fall lower than WORKING_SET. mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_TIMEOUT); - assertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); } @Test @@ -1914,22 +1925,22 @@ public class AppStandbyControllerTests { mInjector.mClockApps.add(Pair.create(PACKAGE_1, UID_1)); reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1); - assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1); reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_2); - assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_2); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_2); mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD; // Make sure a clock app does not get lowered below WORKING_SET. mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_TIMEOUT); - assertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1); + waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1); // A non clock app should be able to fall lower than WORKING_SET. mController.setAppStandbyBucket(PACKAGE_2, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_TIMEOUT); - assertBucket(STANDBY_BUCKET_RARE, PACKAGE_2); + waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_2); } @Test @@ -2067,13 +2078,13 @@ public class AppStandbyControllerTests { public void testBackgroundLocationBucket() throws Exception { reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_BACKGROUND_LOCATION); - assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_BACKGROUND_LOCATION); + waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_BACKGROUND_LOCATION); mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD; // Make sure PACKAGE_BACKGROUND_LOCATION does not get lowered than STANDBY_BUCKET_FREQUENT. mController.setAppStandbyBucket(PACKAGE_BACKGROUND_LOCATION, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_TIMEOUT); - assertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_BACKGROUND_LOCATION); + waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_BACKGROUND_LOCATION); } @Test @@ -2083,41 +2094,41 @@ public class AppStandbyControllerTests { mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_USER); - assertEquals(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE, mInjector.mLastNoteEvent); + waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE, REASON_MAIN_FORCED_BY_USER); - assertEquals(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE, mInjector.mLastNoteEvent); + waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE); // Since we're staying on the PACKAGE_ACTIVE side, noteEvent shouldn't be called. // Reset the last event to confirm the method isn't called. mInjector.mLastNoteEvent = BatteryStats.HistoryItem.EVENT_NONE; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET, REASON_MAIN_FORCED_BY_USER); - assertEquals(BatteryStats.HistoryItem.EVENT_NONE, mInjector.mLastNoteEvent); + waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_NONE); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_USER); - assertEquals(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE, mInjector.mLastNoteEvent); + waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE); // Since we're staying on the PACKAGE_ACTIVE side, noteEvent shouldn't be called. // Reset the last event to confirm the method isn't called. mInjector.mLastNoteEvent = BatteryStats.HistoryItem.EVENT_NONE; mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_FORCED_BY_USER); - assertEquals(BatteryStats.HistoryItem.EVENT_NONE, mInjector.mLastNoteEvent); + waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_NONE); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT, REASON_MAIN_FORCED_BY_USER); - assertEquals(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE, mInjector.mLastNoteEvent); + waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED, REASON_MAIN_FORCED_BY_USER); - assertEquals(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE, mInjector.mLastNoteEvent); + waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_EXEMPTED, REASON_MAIN_FORCED_BY_USER); - assertEquals(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE, mInjector.mLastNoteEvent); + waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE); } private String getAdminAppsStr(int userId) { @@ -2187,8 +2198,7 @@ public class AppStandbyControllerTests { rearmLatch(pkg); mController.setAppStandbyBucket(pkg, user, bucket, reason); mStateChangedLatch.await(1, TimeUnit.SECONDS); - assertEquals("Failed to set package bucket", bucket, - getStandbyBucket(mController, PACKAGE_1)); + waitAndAssertBucket("Failed to set package bucket", bucket, PACKAGE_1); } private void rearmLatch(String pkgName) { @@ -2205,4 +2215,12 @@ public class AppStandbyControllerTests { mLatchUserId = userId; mQuotaBumpLatch = new CountDownLatch(1); } + + private void flushHandler(AppStandbyController controller) { + assertTrue("Failed to flush handler!", controller.flushHandler(FLUSH_TIMEOUT_MILLISECONDS)); + // Some AppStandbyController handler messages queue another handler message. Flush again + // to catch those as well. + assertTrue("Failed to flush handler (the second time)!", + controller.flushHandler(FLUSH_TIMEOUT_MILLISECONDS)); + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java index 260ee39656ea..30843d222742 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java @@ -245,8 +245,8 @@ public class DefaultDeviceEffectsApplierTest { } @Test - @TestParameters({"{origin: ORIGIN_USER}", "{origin: ORIGIN_INIT}", "{origin: ORIGIN_INIT_USER}", - "{origin: ORIGIN_SYSTEM_OR_SYSTEMUI}"}) + @TestParameters({"{origin: ORIGIN_USER}", "{origin: ORIGIN_INIT}", + "{origin: ORIGIN_INIT_USER}"}) public void apply_nightModeWithScreenOn_appliedImmediatelyBasedOnOrigin(ChangeOrigin origin) { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); @@ -263,7 +263,7 @@ public class DefaultDeviceEffectsApplierTest { @Test @TestParameters({"{origin: ORIGIN_APP}", "{origin: ORIGIN_RESTORE_BACKUP}", - "{origin: ORIGIN_UNKNOWN}"}) + "{origin: ORIGIN_SYSTEM_OR_SYSTEMUI}", "{origin: ORIGIN_UNKNOWN}"}) public void apply_nightModeWithScreenOn_willBeAppliedLaterBasedOnOrigin(ChangeOrigin origin) { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 56c75b52cf18..e004ca011a6b 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -80,6 +80,9 @@ import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; import static android.service.notification.Adjustment.KEY_IMPORTANCE; import static android.service.notification.Adjustment.KEY_USER_SENTIMENT; import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS; +import static android.service.notification.Condition.SOURCE_CONTEXT; +import static android.service.notification.Condition.SOURCE_USER_ACTION; +import static android.service.notification.Condition.STATE_TRUE; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING; @@ -224,6 +227,7 @@ import android.provider.DeviceConfig; import android.provider.MediaStore; import android.provider.Settings; import android.service.notification.Adjustment; +import android.service.notification.Condition; import android.service.notification.ConversationChannelWrapper; import android.service.notification.DeviceEffectsApplier; import android.service.notification.INotificationListener; @@ -342,6 +346,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private static final String SCHEME_TIMEOUT = "timeout"; private static final String REDACTED_TEXT = "redacted text"; + private static final AutomaticZenRule SOME_ZEN_RULE = + new AutomaticZenRule.Builder("rule", Uri.parse("uri")) + .setOwner(new ComponentName("pkg", "cls")) + .build(); + private final int mUid = Binder.getCallingUid(); private final @UserIdInt int mUserId = UserHandle.getUserId(mUid); @@ -9048,7 +9057,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { zenPolicy, NotificationManager.INTERRUPTION_FILTER_NONE, isEnabled); try { - mBinderService.addAutomaticZenRule(rule, mContext.getPackageName()); + mBinderService.addAutomaticZenRule(rule, mContext.getPackageName(), false); fail("Zen policy only applies to priority only mode"); } catch (IllegalArgumentException e) { // yay @@ -9056,11 +9065,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class), zenPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, isEnabled); - mBinderService.addAutomaticZenRule(rule, mContext.getPackageName()); + mBinderService.addAutomaticZenRule(rule, mContext.getPackageName(), false); rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class), null, NotificationManager.INTERRUPTION_FILTER_NONE, isEnabled); - mBinderService.addAutomaticZenRule(rule, mContext.getPackageName()); + mBinderService.addAutomaticZenRule(rule, mContext.getPackageName(), false); } @Test @@ -9075,7 +9084,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { boolean isEnabled = true; AutomaticZenRule rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class), zenPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, isEnabled); - mBinderService.addAutomaticZenRule(rule, "com.android.settings"); + mBinderService.addAutomaticZenRule(rule, "com.android.settings", false); // verify that zen mode helper gets passed in a package name of "android" verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule), @@ -9097,7 +9106,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { boolean isEnabled = true; AutomaticZenRule rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class), zenPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, isEnabled); - mBinderService.addAutomaticZenRule(rule, "com.android.settings"); + mBinderService.addAutomaticZenRule(rule, "com.android.settings", false); // verify that zen mode helper gets passed in a package name of "android" verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule), @@ -9117,7 +9126,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { boolean isEnabled = true; AutomaticZenRule rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class), zenPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, isEnabled); - mBinderService.addAutomaticZenRule(rule, "another.package"); + mBinderService.addAutomaticZenRule(rule, "another.package", false); // verify that zen mode helper gets passed in the package name from the arg, not the owner verify(mockZenModeHelper).addAutomaticZenRule( @@ -9128,10 +9137,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test @EnableFlags(android.app.Flags.FLAG_MODES_API) public void testAddAutomaticZenRule_typeManagedCanBeUsedByDeviceOwners() throws Exception { + ZenModeHelper zenModeHelper = setUpMockZenTest(); mService.setCallerIsNormalPackage(); - mService.setZenHelper(mock(ZenModeHelper.class)); - when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt())) - .thenReturn(true); AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", Uri.parse("uri")) .setType(AutomaticZenRule.TYPE_MANAGED) @@ -9139,8 +9146,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .build(); when(mDevicePolicyManager.isActiveDeviceOwner(anyInt())).thenReturn(true); - mBinderService.addAutomaticZenRule(rule, "pkg"); - // No exception! + mBinderService.addAutomaticZenRule(rule, "pkg", /* fromUser= */ false); + + verify(zenModeHelper).addAutomaticZenRule(eq("pkg"), eq(rule), anyInt(), any(), anyInt()); } @Test @@ -9158,7 +9166,144 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mDevicePolicyManager.isActiveDeviceOwner(anyInt())).thenReturn(false); assertThrows(IllegalArgumentException.class, - () -> mBinderService.addAutomaticZenRule(rule, "pkg")); + () -> mBinderService.addAutomaticZenRule(rule, "pkg", /* fromUser= */ false)); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void addAutomaticZenRule_fromUser_mappedToOriginUser() throws Exception { + ZenModeHelper zenModeHelper = setUpMockZenTest(); + mService.isSystemUid = true; + + mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ true); + + verify(zenModeHelper).addAutomaticZenRule(eq("pkg"), eq(SOME_ZEN_RULE), + eq(ZenModeConfig.UPDATE_ORIGIN_USER), anyString(), anyInt()); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void addAutomaticZenRule_fromSystemNotUser_mappedToOriginSystem() throws Exception { + ZenModeHelper zenModeHelper = setUpMockZenTest(); + mService.isSystemUid = true; + + mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ false); + + verify(zenModeHelper).addAutomaticZenRule(eq("pkg"), eq(SOME_ZEN_RULE), + eq(ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI), anyString(), anyInt()); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void addAutomaticZenRule_fromApp_mappedToOriginApp() throws Exception { + ZenModeHelper zenModeHelper = setUpMockZenTest(); + mService.setCallerIsNormalPackage(); + + mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ false); + + verify(zenModeHelper).addAutomaticZenRule(eq("pkg"), eq(SOME_ZEN_RULE), + eq(ZenModeConfig.UPDATE_ORIGIN_APP), anyString(), anyInt()); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void addAutomaticZenRule_fromAppFromUser_blocked() throws Exception { + setUpMockZenTest(); + mService.setCallerIsNormalPackage(); + + assertThrows(SecurityException.class, () -> + mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ true)); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void updateAutomaticZenRule_fromUserFromSystem_allowed() throws Exception { + ZenModeHelper zenModeHelper = setUpMockZenTest(); + mService.isSystemUid = true; + + mBinderService.updateAutomaticZenRule("id", SOME_ZEN_RULE, /* fromUser= */ true); + + verify(zenModeHelper).updateAutomaticZenRule(eq("id"), eq(SOME_ZEN_RULE), + eq(ZenModeConfig.UPDATE_ORIGIN_USER), anyString(), anyInt()); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void updateAutomaticZenRule_fromUserFromApp_blocked() throws Exception { + setUpMockZenTest(); + mService.setCallerIsNormalPackage(); + + assertThrows(SecurityException.class, () -> + mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ true)); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void removeAutomaticZenRule_fromUserFromSystem_allowed() throws Exception { + ZenModeHelper zenModeHelper = setUpMockZenTest(); + mService.isSystemUid = true; + + mBinderService.removeAutomaticZenRule("id", /* fromUser= */ true); + + verify(zenModeHelper).removeAutomaticZenRule(eq("id"), + eq(ZenModeConfig.UPDATE_ORIGIN_USER), anyString(), anyInt()); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void removeAutomaticZenRule_fromUserFromApp_blocked() throws Exception { + setUpMockZenTest(); + mService.setCallerIsNormalPackage(); + + assertThrows(SecurityException.class, () -> + mBinderService.removeAutomaticZenRule("id", /* fromUser= */ true)); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void setAutomaticZenRuleState_fromUserMatchesConditionSource_okay() throws Exception { + ZenModeHelper zenModeHelper = setUpMockZenTest(); + mService.setCallerIsNormalPackage(); + + Condition withSourceContext = new Condition(Uri.parse("uri"), "summary", STATE_TRUE, + SOURCE_CONTEXT); + mBinderService.setAutomaticZenRuleState("id", withSourceContext, /* fromUser= */ false); + verify(zenModeHelper).setAutomaticZenRuleState(eq("id"), eq(withSourceContext), + eq(ZenModeConfig.UPDATE_ORIGIN_APP), anyInt()); + + Condition withSourceUser = new Condition(Uri.parse("uri"), "summary", STATE_TRUE, + SOURCE_USER_ACTION); + mBinderService.setAutomaticZenRuleState("id", withSourceUser, /* fromUser= */ true); + verify(zenModeHelper).setAutomaticZenRuleState(eq("id"), eq(withSourceUser), + eq(ZenModeConfig.UPDATE_ORIGIN_USER), anyInt()); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void setAutomaticZenRuleState_fromUserDoesNotMatchConditionSource_blocked() + throws Exception { + ZenModeHelper zenModeHelper = setUpMockZenTest(); + mService.setCallerIsNormalPackage(); + + Condition withSourceContext = new Condition(Uri.parse("uri"), "summary", STATE_TRUE, + SOURCE_CONTEXT); + assertThrows(IllegalArgumentException.class, + () -> mBinderService.setAutomaticZenRuleState("id", withSourceContext, + /* fromUser= */ true)); + + Condition withSourceUser = new Condition(Uri.parse("uri"), "summary", STATE_TRUE, + SOURCE_USER_ACTION); + assertThrows(IllegalArgumentException.class, + () -> mBinderService.setAutomaticZenRuleState("id", withSourceUser, + /* fromUser= */ false)); + } + + private ZenModeHelper setUpMockZenTest() { + ZenModeHelper zenModeHelper = mock(ZenModeHelper.class); + mService.setZenHelper(zenModeHelper); + when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt())) + .thenReturn(true); + return zenModeHelper; } @Test @@ -9184,7 +9329,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { }); mService.getBinderService().setZenMode(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS, null, - "testing!"); + "testing!", false); waitForIdle(); InOrder inOrder = inOrder(mContext); @@ -12157,7 +12302,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { /* isImageBitmap= */ true, /* isExpired= */ true); addRecordAndRemoveBitmaps(record); - assertThat(record.getNotification().extras.containsKey(EXTRA_PICTURE)).isFalse(); + assertThat(record.getNotification().extras.containsKey(EXTRA_PICTURE)).isTrue(); + final Parcelable picture = record.getNotification().extras.getParcelable(EXTRA_PICTURE); + assertThat(picture).isNull(); } @Test @@ -12191,7 +12338,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { /* isImageBitmap= */ false, /* isExpired= */ true); addRecordAndRemoveBitmaps(record); - assertThat(record.getNotification().extras.containsKey(EXTRA_PICTURE_ICON)).isFalse(); + assertThat(record.getNotification().extras.containsKey(EXTRA_PICTURE_ICON)).isTrue(); + final Parcelable pictureIcon = + record.getNotification().extras.getParcelable(EXTRA_PICTURE_ICON); + assertThat(pictureIcon).isNull(); } @Test @@ -13441,9 +13591,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .thenReturn(true); NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0); - mBinderService.setNotificationPolicy("package", policy); + mBinderService.setNotificationPolicy("package", policy, false); - verify(zenHelper).applyGlobalPolicyAsImplicitZenRule(eq("package"), anyInt(), eq(policy)); + verify(zenHelper).applyGlobalPolicyAsImplicitZenRule(eq("package"), anyInt(), eq(policy), + eq(ZenModeConfig.UPDATE_ORIGIN_APP)); } @Test @@ -13457,7 +13608,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.isSystemUid = true; NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0); - mBinderService.setNotificationPolicy("package", policy); + mBinderService.setNotificationPolicy("package", policy, false); verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyInt()); } @@ -13479,7 +13630,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .build())); NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0); - mBinderService.setNotificationPolicy("package", policy); + mBinderService.setNotificationPolicy("package", policy, false); verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyInt()); } @@ -13495,7 +13646,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .thenReturn(true); NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0); - mBinderService.setNotificationPolicy("package", policy); + mBinderService.setNotificationPolicy("package", policy, false); verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyInt()); } @@ -13525,7 +13676,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt())) .thenReturn(true); - mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY); + mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY, false); verify(zenHelper).applyGlobalZenModeAsImplicitZenRule(eq("package"), anyInt(), eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS)); @@ -13542,7 +13693,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .thenReturn(true); mService.isSystemUid = true; - mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY); + mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY, false); verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null), eq(ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI), anyString(), eq("package"), @@ -13565,7 +13716,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .setDeviceProfile(AssociationRequest.DEVICE_PROFILE_WATCH) .build())); - mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY); + mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY, false); verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null), eq(ZenModeConfig.UPDATE_ORIGIN_APP), anyString(), eq("package"), anyInt()); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java index 4a1435f9ee64..1fcee0658afc 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java @@ -99,7 +99,7 @@ public class ZenModeEventLoggerFake extends ZenModeEventLogger { public boolean getFromSystemOrSystemUi(int i) throws IllegalArgumentException { // While this isn't a logged output value, it's still helpful to check in tests. checkInRange(i); - return mChanges.get(i).mFromSystemOrSystemUi; + return mChanges.get(i).isFromSystemOrSystemUi(); } public boolean getIsUserAction(int i) throws IllegalArgumentException { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 1aea56cd8f9f..44f0894f76d7 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -43,6 +43,8 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.provider.Settings.Global.ZEN_MODE_ALARMS; import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; import static android.provider.Settings.Global.ZEN_MODE_OFF; +import static android.service.notification.Condition.SOURCE_SCHEDULE; +import static android.service.notification.Condition.SOURCE_USER_ACTION; import static android.service.notification.Condition.STATE_FALSE; import static android.service.notification.Condition.STATE_TRUE; import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_APP; @@ -112,6 +114,7 @@ import android.net.Uri; import android.os.Parcel; import android.os.Process; import android.os.UserHandle; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.provider.Settings.Global; @@ -119,12 +122,13 @@ import android.service.notification.Condition; import android.service.notification.DeviceEffectsApplier; import android.service.notification.ZenDeviceEffects; import android.service.notification.ZenModeConfig; +import android.service.notification.ZenModeConfig.ConfigChangeOrigin; import android.service.notification.ZenModeConfig.ScheduleInfo; import android.service.notification.ZenModeConfig.ZenRule; import android.service.notification.ZenModeDiff; import android.service.notification.ZenPolicy; import android.test.suitebuilder.annotation.SmallTest; -import android.testing.AndroidTestingRunner; +import android.testing.TestWithLooperRule; import android.testing.TestableLooper; import android.util.ArrayMap; import android.util.Log; @@ -147,6 +151,8 @@ import com.android.server.notification.ManagedServices.UserProfiles; import com.google.common.collect.ImmutableList; import com.google.common.truth.Correspondence; import com.google.protobuf.InvalidProtocolBufferException; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; import org.junit.Before; import org.junit.Rule; @@ -172,7 +178,7 @@ import java.util.concurrent.TimeUnit; @SmallTest @SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service. -@RunWith(AndroidTestingRunner.class) +@RunWith(TestParameterInjector.class) @TestableLooper.RunWithLooper public class ZenModeHelperTest extends UiServiceTestCase { @@ -211,7 +217,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { private static final ZenDeviceEffects NO_EFFECTS = new ZenDeviceEffects.Builder().build(); @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule( + SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT); + + @Rule(order = Integer.MAX_VALUE) // set the highest order so it's the innermost rule + public TestWithLooperRule mLooperRule = new TestWithLooperRule(); ConditionProviders mConditionProviders; @Mock NotificationManager mNotificationManager; @@ -2339,15 +2349,38 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(ZEN_MODE_OFF, mZenModeHelper.mZenMode); } + private enum ModesApiFlag { + ENABLED(true, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_USER), + DISABLED(false, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI); + + private final boolean mEnabled; + @ConfigChangeOrigin private final int mOriginForUserActionInSystemUi; + + ModesApiFlag(boolean enabled, @ConfigChangeOrigin int originForUserActionInSystemUi) { + this.mEnabled = enabled; + this.mOriginForUserActionInSystemUi = originForUserActionInSystemUi; + } + + void applyFlag(SetFlagsRule setFlagsRule) { + if (mEnabled) { + setFlagsRule.enableFlags(Flags.FLAG_MODES_API); + } else { + setFlagsRule.disableFlags(Flags.FLAG_MODES_API); + } + } + } + @Test - public void testZenModeEventLog_setManualZenMode() throws IllegalArgumentException { + public void testZenModeEventLog_setManualZenMode(@TestParameter ModesApiFlag modesApiFlag) + throws IllegalArgumentException { + modesApiFlag.applyFlag(mSetFlagsRule); mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); setupZenConfig(); // Turn zen mode on (to important_interruptions) // Need to additionally call the looper in order to finish the post-apply-config process mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", null, Process.SYSTEM_UID); + modesApiFlag.mOriginForUserActionInSystemUi, "", null, Process.SYSTEM_UID); // Now turn zen mode off, but via a different package UID -- this should get registered as // "not an action by the user" because some other app is changing zen mode @@ -2374,7 +2407,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0)); assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(0)); assertEquals(1, mZenModeEventLogger.getNumRulesActive(0)); - assertTrue(mZenModeEventLogger.getFromSystemOrSystemUi(0)); + assertThat(mZenModeEventLogger.getFromSystemOrSystemUi(0)).isEqualTo( + modesApiFlag == ModesApiFlag.DISABLED); assertTrue(mZenModeEventLogger.getIsUserAction(0)); assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(0)); checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0)); @@ -2399,7 +2433,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void testZenModeEventLog_automaticRules() throws IllegalArgumentException { + public void testZenModeEventLog_automaticRules(@TestParameter ModesApiFlag modesApiFlag) + throws IllegalArgumentException { + modesApiFlag.applyFlag(mSetFlagsRule); mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); setupZenConfig(); @@ -2421,8 +2457,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Event 2: "User" turns off the automatic rule (sets it to not enabled) zenRule.setEnabled(false); - mZenModeHelper.updateAutomaticZenRule(id, zenRule, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(id, zenRule, + modesApiFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID); // Add a new system rule AutomaticZenRule systemRule = new AutomaticZenRule("systemRule", @@ -2440,8 +2476,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); // Event 4: "User" deletes the rule - mZenModeHelper.removeAutomaticZenRule(systemId, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", - Process.SYSTEM_UID); + mZenModeHelper.removeAutomaticZenRule(systemId, modesApiFlag.mOriginForUserActionInSystemUi, + "", Process.SYSTEM_UID); // In total, this represents 4 events assertEquals(4, mZenModeEventLogger.numLoggedChanges()); @@ -2497,20 +2533,109 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void testZenModeEventLog_policyChanges() throws IllegalArgumentException { + @EnableFlags(Flags.FLAG_MODES_API) + public void testZenModeEventLog_automaticRuleActivatedFromAppByAppAndUser() + throws IllegalArgumentException { + mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); + setupZenConfig(); + + // Ann app adds an automatic zen rule + AutomaticZenRule zenRule = new AutomaticZenRule("name", + null, + new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + null, + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, + UPDATE_ORIGIN_APP, "test", CUSTOM_PKG_UID); + + // Event 1: Mimic the rule coming on manually when the user turns it on in the app + // ("Turn on bedtime now" because user goes to bed earlier). + mZenModeHelper.setAutomaticZenRuleState(id, + new Condition(zenRule.getConditionId(), "", STATE_TRUE, SOURCE_USER_ACTION), + UPDATE_ORIGIN_USER, CUSTOM_PKG_UID); + + // Event 2: App deactivates the rule automatically (it's 8 AM, bedtime schedule ends) + mZenModeHelper.setAutomaticZenRuleState(id, + new Condition(zenRule.getConditionId(), "", STATE_FALSE, SOURCE_SCHEDULE), + UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + + // Event 3: App activates the rule automatically (it's now 11 PM, bedtime schedule starts) + mZenModeHelper.setAutomaticZenRuleState(id, + new Condition(zenRule.getConditionId(), "", STATE_TRUE, SOURCE_SCHEDULE), + UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + + // Event 4: User deactivates the rule manually (they get up before 8 AM on the next day) + mZenModeHelper.setAutomaticZenRuleState(id, + new Condition(zenRule.getConditionId(), "", STATE_FALSE, SOURCE_USER_ACTION), + UPDATE_ORIGIN_USER, CUSTOM_PKG_UID); + + // In total, this represents 4 events + assertEquals(4, mZenModeEventLogger.numLoggedChanges()); + + // Automatic rule turning on manually: + // - event ID: DND_TURNED_ON + // - 1 rule (newly) active + // - is a user action + // - package UID is the calling package + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), + mZenModeEventLogger.getEventId(0)); + assertEquals(1, mZenModeEventLogger.getNumRulesActive(0)); + assertTrue(mZenModeEventLogger.getIsUserAction(0)); + assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0)); + + // Automatic rule turned off automatically by app: + // - event ID: DND_TURNED_OFF + // - 0 rules active + // - is not a user action + // - package UID is the calling package + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(), + mZenModeEventLogger.getEventId(1)); + assertEquals(0, mZenModeEventLogger.getNumRulesActive(1)); + assertFalse(mZenModeEventLogger.getIsUserAction(1)); + assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(1)); + + // Automatic rule turned on automatically by app: + // - event ID: DND_TURNED_ON + // - 1 rule (newly) active + // - is not a user action + // - package UID is the calling package + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), + mZenModeEventLogger.getEventId(2)); + assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(2)); + assertEquals(1, mZenModeEventLogger.getNumRulesActive(2)); + assertFalse(mZenModeEventLogger.getIsUserAction(2)); + assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(2)); + + // Automatic rule turned off automatically by the user: + // - event ID: DND_TURNED_ON + // - 0 rules active + // - is a user action + // - package UID is the calling package + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(), + mZenModeEventLogger.getEventId(3)); + assertEquals(0, mZenModeEventLogger.getNumRulesActive(3)); + assertTrue(mZenModeEventLogger.getIsUserAction(3)); + assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(3)); + } + + @Test + public void testZenModeEventLog_policyChanges(@TestParameter ModesApiFlag modesApiFlag) + throws IllegalArgumentException { + modesApiFlag.applyFlag(mSetFlagsRule); mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); setupZenConfig(); // First just turn zen mode on mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", null, Process.SYSTEM_UID); + modesApiFlag.mOriginForUserActionInSystemUi, "", null, Process.SYSTEM_UID); // Now change the policy slightly; want to confirm that this'll be reflected in the logs ZenModeConfig newConfig = mZenModeHelper.mConfig.copy(); newConfig.allowAlarms = true; newConfig.allowRepeatCallers = false; mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + modesApiFlag.mOriginForUserActionInSystemUi, Process.SYSTEM_UID); // Turn zen mode off; we want to make sure policy changes do not get logged when zen mode // is off. @@ -2521,7 +2646,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { newConfig.allowMessages = false; newConfig.allowRepeatCallers = true; mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + modesApiFlag.mOriginForUserActionInSystemUi, Process.SYSTEM_UID); // Total events: we only expect ones for turning on, changing policy, and turning off assertEquals(3, mZenModeEventLogger.numLoggedChanges()); @@ -2554,7 +2679,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void testZenModeEventLog_ruleCounts() throws IllegalArgumentException { + public void testZenModeEventLog_ruleCounts(@TestParameter ModesApiFlag modesApiFlag) + throws IllegalArgumentException { + modesApiFlag.applyFlag(mSetFlagsRule); mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); setupZenConfig(); @@ -2657,8 +2784,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void testZenModeEventLog_noLogWithNoConfigChange() throws IllegalArgumentException { + public void testZenModeEventLog_noLogWithNoConfigChange( + @TestParameter ModesApiFlag modesApiFlag) throws IllegalArgumentException { // If evaluateZenMode is called independently of a config change, don't log. + modesApiFlag.applyFlag(mSetFlagsRule); mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); setupZenConfig(); @@ -2675,9 +2804,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void testZenModeEventLog_reassignUid() throws IllegalArgumentException { + public void testZenModeEventLog_reassignUid(@TestParameter ModesApiFlag modesApiFlag) + throws IllegalArgumentException { // Test that, only in specific cases, we reassign the calling UID to one associated with // the automatic rule owner. + modesApiFlag.applyFlag(mSetFlagsRule); mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); setupZenConfig(); @@ -2689,7 +2820,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + UPDATE_ORIGIN_APP, "test", Process.SYSTEM_UID); // Rule 2, same as rule 1 but owned by the system AutomaticZenRule zenRule2 = new AutomaticZenRule("name2", @@ -2699,11 +2830,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + modesApiFlag.mOriginForUserActionInSystemUi, "test", Process.SYSTEM_UID); // Turn on rule 1; call looks like it's from the system. Because setting a condition is // typically an automatic (non-user-initiated) action, expect the calling UID to be - // re-evaluated to the one associat.d with CUSTOM_PKG_NAME. + // re-evaluated to the one associated with CUSTOM_PKG_NAME. mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); @@ -2717,8 +2848,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Disable rule 1. Because this looks like a user action, the UID should not be modified // from the system-provided one. zenRule.setEnabled(false); - mZenModeHelper.updateAutomaticZenRule(id, zenRule, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(id, zenRule, + modesApiFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID); // Add a manual rule. Any manual rule changes should not get calling uids reassigned. mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, UPDATE_ORIGIN_APP, @@ -2775,8 +2906,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void testZenModeEventLog_channelsBypassingChanges() { + public void testZenModeEventLog_channelsBypassingChanges( + @TestParameter ModesApiFlag modesApiFlag) { // Verify that the right thing happens when the canBypassDnd value changes. + modesApiFlag.applyFlag(mSetFlagsRule); mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); setupZenConfig(); @@ -2872,14 +3005,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Second message where we change the policy: // - DND_POLICY_CHANGED (indicates only the policy changed and nothing else) // - rule type: unknown (it's a policy change, not a rule change) - // - user action (because it comes from a "system" uid) // - change is in allow channels, and final policy assertThat(mZenModeEventLogger.getEventId(1)) .isEqualTo(ZenModeEventLogger.ZenStateChangedEvent.DND_POLICY_CHANGED.getId()); assertThat(mZenModeEventLogger.getChangedRuleType(1)) .isEqualTo(DNDProtoEnums.UNKNOWN_RULE); - assertThat(mZenModeEventLogger.getIsUserAction(1)).isTrue(); - assertThat(mZenModeEventLogger.getPackageUid(1)).isEqualTo(Process.SYSTEM_UID); DNDPolicyProto dndProto = mZenModeEventLogger.getPolicyProto(1); assertThat(dndProto.getAllowChannels().getNumber()) .isEqualTo(DNDProtoEnums.CHANNEL_TYPE_NONE); @@ -3372,6 +3502,29 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void removeAutomaticZenRule_propagatesOriginToEffectsApplier() { + mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); + reset(mDeviceEffectsApplier); + + String ruleId = addRuleWithEffects(new ZenDeviceEffects.Builder() + .setShouldSuppressAmbientDisplay(true) + .setShouldDimWallpaper(true) + .build()); + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + CUSTOM_PKG_UID); + mTestableLooper.processAllMessages(); + verify(mDeviceEffectsApplier).apply(any(), eq(UPDATE_ORIGIN_APP)); + + // Now delete the (currently active!) rule. For example, assume this is done from settings. + mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_USER, "remove", + Process.SYSTEM_UID); + mTestableLooper.processAllMessages(); + + verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(UPDATE_ORIGIN_USER)); + } + + @Test public void testDeviceEffects_applied() { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); @@ -3511,8 +3664,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID) .setDeviceEffects(effects) .build(); - return mZenModeHelper.addAutomaticZenRule("pkg", rule, UPDATE_ORIGIN_APP, "", - CUSTOM_PKG_UID); + return mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, + UPDATE_ORIGIN_APP, "reasons", CUSTOM_PKG_UID); } @Test @@ -3619,7 +3772,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { Policy policy = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED, Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT); - mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, policy); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, policy, + UPDATE_ORIGIN_APP); ZenPolicy expectedZenPolicy = new ZenPolicy.Builder() .disallowAllSounds() @@ -3643,13 +3797,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED, Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT); mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, - original); + original, UPDATE_ORIGIN_APP); // Change priorityCallSenders: contacts -> starred. Policy updated = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_STARRED, PRIORITY_SENDERS_STARRED, Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT); - mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, updated); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, updated, + UPDATE_ORIGIN_APP); ZenPolicy expectedZenPolicy = new ZenPolicy.Builder() .disallowAllSounds() @@ -3671,7 +3826,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { withoutWtfCrash( () -> mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, - CUSTOM_PKG_UID, new Policy(0, 0, 0))); + CUSTOM_PKG_UID, new Policy(0, 0, 0), UPDATE_ORIGIN_APP)); assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty(); } @@ -3684,7 +3839,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { Policy.getAllSuppressedVisualEffects(), STATE_FALSE, CONVERSATION_SENDERS_IMPORTANT); mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, - writtenPolicy); + writtenPolicy, UPDATE_ORIGIN_APP); Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule( CUSTOM_PKG_NAME); diff --git a/services/tests/wmtests/src/com/android/server/wm/CompatModePackagesTests.java b/services/tests/wmtests/src/com/android/server/wm/CompatModePackagesTests.java index 1f7b65e8b701..c18726350d81 100644 --- a/services/tests/wmtests/src/com/android/server/wm/CompatModePackagesTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/CompatModePackagesTests.java @@ -20,10 +20,13 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import android.app.GameManagerInternal; +import android.content.pm.ApplicationInfo; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; @@ -90,6 +93,14 @@ public class CompatModePackagesTests extends SystemServiceTestsBase { public void testGetCompatScale_noGameManager() { assertEquals(mAtm.mCompatModePackages.getCompatScale(TEST_PACKAGE, TEST_USER_ID), 1f, 0.01f); - } + final ApplicationInfo info = new ApplicationInfo(); + // Any non-zero value without FLAG_SUPPORTS_*_SCREENS. + info.flags = ApplicationInfo.FLAG_HAS_CODE; + info.packageName = info.sourceDir = "legacy.app"; + mAtm.mCompatModePackages.compatibilityInfoForPackageLocked(info); + assertTrue(mAtm.mCompatModePackages.useLegacyScreenCompatMode(info.packageName)); + mAtm.mCompatModePackages.handlePackageUninstalledLocked(info.packageName); + assertFalse(mAtm.mCompatModePackages.useLegacyScreenCompatMode(info.packageName)); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceControlViewHostTests.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceControlViewHostTests.java index ef427bb15039..a8b217860946 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SurfaceControlViewHostTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceControlViewHostTests.java @@ -16,7 +16,7 @@ package com.android.server.wm; -import static android.server.wm.CtsWindowInfoUtils.dumpWindowsOnScreen; +import static android.server.wm.CtsWindowInfoUtils.assertAndDumpWindowState; import static android.server.wm.CtsWindowInfoUtils.waitForWindowFocus; import static android.server.wm.CtsWindowInfoUtils.waitForWindowVisible; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; @@ -129,37 +129,22 @@ public class SurfaceControlViewHostTests { mScvh2.setView(mView2, lp2); }); - boolean wasVisible = waitForWindowVisible(mView1); - if (!wasVisible) { - dumpWindowsOnScreen(TAG, "requestFocusWithMultipleWindows"); - } - assertTrue("Failed to wait for view1", wasVisible); - - wasVisible = waitForWindowVisible(mView2); - if (!wasVisible) { - dumpWindowsOnScreen(TAG, "requestFocusWithMultipleWindows-not visible"); - } - assertTrue("Failed to wait for view2", wasVisible); + assertAndDumpWindowState(TAG, "Failed to wait for view1", waitForWindowVisible(mView1)); + assertAndDumpWindowState(TAG, "Failed to wait for view2", waitForWindowVisible(mView2)); IWindow window = IWindow.Stub.asInterface(mSurfaceView.getWindowToken()); WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(window, mScvh1.getInputTransferToken(), true); - boolean gainedFocus = waitForWindowFocus(mView1, true); - if (!gainedFocus) { - dumpWindowsOnScreen(TAG, "requestFocusWithMultipleWindows-view1 not focus"); - } - assertTrue("Failed to gain focus for view1", gainedFocus); + assertAndDumpWindowState(TAG, "Failed to wait for view1 focus", + waitForWindowFocus(mView1, true)); WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(window, mScvh2.getInputTransferToken(), true); - gainedFocus = waitForWindowFocus(mView2, true); - if (!gainedFocus) { - dumpWindowsOnScreen(TAG, "requestFocusWithMultipleWindows-view2 not focus"); - } - assertTrue("Failed to gain focus for view2", gainedFocus); + assertAndDumpWindowState(TAG, "Failed to wait for view2 focus", + waitForWindowFocus(mView2, true)); } private static class TestWindowlessWindowManager extends WindowlessWindowManager { diff --git a/services/tests/wmtests/src/com/android/server/wm/TrustedOverlayTests.java b/services/tests/wmtests/src/com/android/server/wm/TrustedOverlayTests.java index ac498397eb39..6a15b0594428 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TrustedOverlayTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TrustedOverlayTests.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.server.wm.CtsWindowInfoUtils.assertAndDumpWindowState; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; @@ -138,11 +139,8 @@ public class TrustedOverlayTests { return false; }, TIMEOUT_S, TimeUnit.SECONDS); - if (!foundTrusted[0]) { - CtsWindowInfoUtils.dumpWindowsOnScreen(TAG, mName.getMethodName()); - } - - assertTrue("Failed to find window or was not marked trusted", foundTrusted[0]); + assertAndDumpWindowState(TAG, "Failed to find window or was not marked trusted", + foundTrusted[0]); } private void testTrustedOverlayChildHelper(boolean expectedTrustedChild) diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index df4af112c087..616a23e7ab5b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -294,7 +294,7 @@ class WindowTestsBase extends SystemServiceTestsBase { */ static void suppressInsetsAnimation(InsetsControlTarget target) { spyOn(target); - Mockito.doNothing().when(target).notifyInsetsControlChanged(); + Mockito.doNothing().when(target).notifyInsetsControlChanged(anyInt()); } @After diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java index 4720d279a676..f9fa9b7a9524 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java @@ -106,96 +106,104 @@ final class VisualQueryDetectorSession extends DetectorSession { @Override public void onAttentionGained() { Slog.v(TAG, "BinderCallback#onAttentionGained"); - mEgressingData = true; - if (mAttentionListener == null) { - return; - } - try { - mAttentionListener.onAttentionGained(); - } catch (RemoteException e) { - Slog.e(TAG, "Error delivering attention gained event.", e); + synchronized (mLock) { + mEgressingData = true; + if (mAttentionListener == null) { + return; + } try { - callback.onVisualQueryDetectionServiceFailure( - new VisualQueryDetectionServiceFailure( - ERROR_CODE_ILLEGAL_ATTENTION_STATE, - "Attention listener failed to switch to GAINED state.")); - } catch (RemoteException ex) { - Slog.v(TAG, "Fail to call onVisualQueryDetectionServiceFailure"); + mAttentionListener.onAttentionGained(); + } catch (RemoteException e) { + Slog.e(TAG, "Error delivering attention gained event.", e); + try { + callback.onVisualQueryDetectionServiceFailure( + new VisualQueryDetectionServiceFailure( + ERROR_CODE_ILLEGAL_ATTENTION_STATE, + "Attention listener fails to switch to GAINED state.")); + } catch (RemoteException ex) { + Slog.v(TAG, "Fail to call onVisualQueryDetectionServiceFailure"); + } } - return; } } @Override public void onAttentionLost() { Slog.v(TAG, "BinderCallback#onAttentionLost"); - mEgressingData = false; - if (mAttentionListener == null) { - return; - } - try { - mAttentionListener.onAttentionLost(); - } catch (RemoteException e) { - Slog.e(TAG, "Error delivering attention lost event.", e); + synchronized (mLock) { + mEgressingData = false; + if (mAttentionListener == null) { + return; + } try { - callback.onVisualQueryDetectionServiceFailure( - new VisualQueryDetectionServiceFailure( - ERROR_CODE_ILLEGAL_ATTENTION_STATE, - "Attention listener failed to switch to LOST state.")); - } catch (RemoteException ex) { - Slog.v(TAG, "Fail to call onVisualQueryDetectionServiceFailure"); + mAttentionListener.onAttentionLost(); + } catch (RemoteException e) { + Slog.e(TAG, "Error delivering attention lost event.", e); + try { + callback.onVisualQueryDetectionServiceFailure( + new VisualQueryDetectionServiceFailure( + ERROR_CODE_ILLEGAL_ATTENTION_STATE, + "Attention listener fails to switch to LOST state.")); + } catch (RemoteException ex) { + Slog.v(TAG, "Fail to call onVisualQueryDetectionServiceFailure"); + } } - return; } } @Override public void onQueryDetected(@NonNull String partialQuery) throws RemoteException { - Objects.requireNonNull(partialQuery); Slog.v(TAG, "BinderCallback#onQueryDetected"); - if (!mEgressingData) { - Slog.v(TAG, "Query should not be egressed within the unattention state."); - callback.onVisualQueryDetectionServiceFailure( - new VisualQueryDetectionServiceFailure( - ERROR_CODE_ILLEGAL_STREAMING_STATE, - "Cannot stream queries without attention signals.")); - return; + synchronized (mLock) { + Objects.requireNonNull(partialQuery); + if (!mEgressingData) { + Slog.v(TAG, "Query should not be egressed within the unattention state."); + callback.onVisualQueryDetectionServiceFailure( + new VisualQueryDetectionServiceFailure( + ERROR_CODE_ILLEGAL_STREAMING_STATE, + "Cannot stream queries without attention signals.")); + return; + } + mQueryStreaming = true; + callback.onQueryDetected(partialQuery); + Slog.i(TAG, "Egressed from visual query detection process."); } - mQueryStreaming = true; - callback.onQueryDetected(partialQuery); - Slog.i(TAG, "Egressed from visual query detection process."); } @Override public void onQueryFinished() throws RemoteException { Slog.v(TAG, "BinderCallback#onQueryFinished"); - if (!mQueryStreaming) { - Slog.v(TAG, "Query streaming state signal FINISHED is block since there is" - + " no active query being streamed."); - callback.onVisualQueryDetectionServiceFailure( - new VisualQueryDetectionServiceFailure( - ERROR_CODE_ILLEGAL_STREAMING_STATE, - "Cannot send FINISHED signal with no query streamed.")); - return; + synchronized (mLock) { + if (!mQueryStreaming) { + Slog.v(TAG, "Query streaming state signal FINISHED is block since there is" + + " no active query being streamed."); + callback.onVisualQueryDetectionServiceFailure( + new VisualQueryDetectionServiceFailure( + ERROR_CODE_ILLEGAL_STREAMING_STATE, + "Cannot send FINISHED signal with no query streamed.")); + return; + } + callback.onQueryFinished(); + mQueryStreaming = false; } - callback.onQueryFinished(); - mQueryStreaming = false; } @Override public void onQueryRejected() throws RemoteException { Slog.v(TAG, "BinderCallback#onQueryRejected"); - if (!mQueryStreaming) { - Slog.v(TAG, "Query streaming state signal REJECTED is block since there is" - + " no active query being streamed."); - callback.onVisualQueryDetectionServiceFailure( - new VisualQueryDetectionServiceFailure( - ERROR_CODE_ILLEGAL_STREAMING_STATE, - "Cannot send REJECTED signal with no query streamed.")); - return; + synchronized (mLock) { + if (!mQueryStreaming) { + Slog.v(TAG, "Query streaming state signal REJECTED is block since there is" + + " no active query being streamed."); + callback.onVisualQueryDetectionServiceFailure( + new VisualQueryDetectionServiceFailure( + ERROR_CODE_ILLEGAL_STREAMING_STATE, + "Cannot send REJECTED signal with no query streamed.")); + return; + } + callback.onQueryRejected(); + mQueryStreaming = false; } - callback.onQueryRejected(); - mQueryStreaming = false; } }; return mRemoteDetectionService.run( diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java index def52a517913..874c10c8ea83 100644 --- a/telecomm/java/android/telecom/Call.java +++ b/telecomm/java/android/telecom/Call.java @@ -210,100 +210,6 @@ public final class Call { "android.telecom.extra.SILENT_RINGING_REQUESTED"; /** - * Call event sent from a {@link Call} via {@link #sendCallEvent(String, Bundle)} to inform - * Telecom that the user has requested that the current {@link Call} should be handed over - * to another {@link ConnectionService}. - * <p> - * The caller must specify the {@link #EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE} to indicate to - * Telecom which {@link PhoneAccountHandle} the {@link Call} should be handed over to. - * @hide - * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated - * APIs instead. - */ - public static final String EVENT_REQUEST_HANDOVER = - "android.telecom.event.REQUEST_HANDOVER"; - - /** - * Extra key used with the {@link #EVENT_REQUEST_HANDOVER} call event. Specifies the - * {@link PhoneAccountHandle} to which a call should be handed over to. - * @hide - * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated - * APIs instead. - */ - public static final String EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE = - "android.telecom.extra.HANDOVER_PHONE_ACCOUNT_HANDLE"; - - /** - * Integer extra key used with the {@link #EVENT_REQUEST_HANDOVER} call event. Specifies the - * video state of the call when it is handed over to the new {@link PhoneAccount}. - * <p> - * Valid values: {@link VideoProfile#STATE_AUDIO_ONLY}, - * {@link VideoProfile#STATE_BIDIRECTIONAL}, {@link VideoProfile#STATE_RX_ENABLED}, and - * {@link VideoProfile#STATE_TX_ENABLED}. - * @hide - * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated - * APIs instead. - */ - public static final String EXTRA_HANDOVER_VIDEO_STATE = - "android.telecom.extra.HANDOVER_VIDEO_STATE"; - - /** - * Extra key used with the {@link #EVENT_REQUEST_HANDOVER} call event. Used by the - * {@link InCallService} initiating a handover to provide a {@link Bundle} with extra - * information to the handover {@link ConnectionService} specified by - * {@link #EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE}. - * <p> - * This {@link Bundle} is not interpreted by Telecom, but passed as-is to the - * {@link ConnectionService} via the request extras when - * {@link ConnectionService#onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)} - * is called to initate the handover. - * @hide - * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated - * APIs instead. - */ - public static final String EXTRA_HANDOVER_EXTRAS = "android.telecom.extra.HANDOVER_EXTRAS"; - - /** - * Call event sent from Telecom to the handover {@link ConnectionService} via - * {@link Connection#onCallEvent(String, Bundle)} to inform a {@link Connection} that a handover - * to the {@link ConnectionService} has completed successfully. - * <p> - * A handover is initiated with the {@link #EVENT_REQUEST_HANDOVER} call event. - * @hide - * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated - * APIs instead. - */ - public static final String EVENT_HANDOVER_COMPLETE = - "android.telecom.event.HANDOVER_COMPLETE"; - - /** - * Call event sent from Telecom to the handover destination {@link ConnectionService} via - * {@link Connection#onCallEvent(String, Bundle)} to inform the handover destination that the - * source connection has disconnected. The {@link Bundle} parameter for the call event will be - * {@code null}. - * <p> - * A handover is initiated with the {@link #EVENT_REQUEST_HANDOVER} call event. - * @hide - * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated - * APIs instead. - */ - public static final String EVENT_HANDOVER_SOURCE_DISCONNECTED = - "android.telecom.event.HANDOVER_SOURCE_DISCONNECTED"; - - /** - * Call event sent from Telecom to the handover {@link ConnectionService} via - * {@link Connection#onCallEvent(String, Bundle)} to inform a {@link Connection} that a handover - * to the {@link ConnectionService} has failed. - * <p> - * A handover is initiated with the {@link #EVENT_REQUEST_HANDOVER} call event. - * @hide - * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated - * APIs instead. - */ - public static final String EVENT_HANDOVER_FAILED = - "android.telecom.event.HANDOVER_FAILED"; - - /** * Event reported from the Telecom stack to report an in-call diagnostic message which the * dialer app may opt to display to the user. A diagnostic message is used to communicate * scenarios the device has detected which may impact the quality of the ongoing call. diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java index 4a541da9e5aa..ee9bf8983900 100644 --- a/telecomm/java/android/telecom/Connection.java +++ b/telecomm/java/android/telecom/Connection.java @@ -961,28 +961,6 @@ public abstract class Connection extends Conferenceable { "android.telecom.event.CALL_REMOTELY_UNHELD"; /** - * Connection event used to inform an {@link InCallService} which initiated a call handover via - * {@link Call#EVENT_REQUEST_HANDOVER} that the handover from this {@link Connection} has - * successfully completed. - * @hide - * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated - * APIs instead. - */ - public static final String EVENT_HANDOVER_COMPLETE = - "android.telecom.event.HANDOVER_COMPLETE"; - - /** - * Connection event used to inform an {@link InCallService} which initiated a call handover via - * {@link Call#EVENT_REQUEST_HANDOVER} that the handover from this {@link Connection} has failed - * to complete. - * @hide - * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated - * APIs instead. - */ - public static final String EVENT_HANDOVER_FAILED = - "android.telecom.event.HANDOVER_FAILED"; - - /** * String Connection extra key used to store SIP invite fields for an incoming call for IMS call */ public static final String EXTRA_SIP_INVITE = "android.telecom.extra.SIP_INVITE"; diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index bcd99295b605..101d285bc92c 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -525,6 +525,12 @@ public class CarrierConfigManager { public static final String KEY_PREFER_2G_BOOL = "prefer_2g_bool"; /** + * Used in the Preferred Network Types menu to determine if the 3G option is displayed. + */ + @FlaggedApi(Flags.FLAG_HIDE_PREFER_3G_ITEM) + public static final String KEY_PREFER_3G_VISIBILITY_BOOL = "prefer_3g_visibility_bool"; + + /** * Used in Cellular Network Settings for preferred network type to show 4G only mode. * @hide */ @@ -10144,6 +10150,7 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_MDN_IS_ADDITIONAL_VOICEMAIL_NUMBER_BOOL, false); sDefaults.putBoolean(KEY_OPERATOR_SELECTION_EXPAND_BOOL, true); sDefaults.putBoolean(KEY_PREFER_2G_BOOL, false); + sDefaults.putBoolean(KEY_PREFER_3G_VISIBILITY_BOOL, true); sDefaults.putBoolean(KEY_4G_ONLY_BOOL, false); sDefaults.putBoolean(KEY_SHOW_APN_SETTING_CDMA_BOOL, false); sDefaults.putBoolean(KEY_SHOW_CDMA_CHOICES_BOOL, false); diff --git a/tests/ActivityManagerPerfTests/tests/Android.bp b/tests/ActivityManagerPerfTests/tests/Android.bp index e5813aec9f43..cce40f3a2664 100644 --- a/tests/ActivityManagerPerfTests/tests/Android.bp +++ b/tests/ActivityManagerPerfTests/tests/Android.bp @@ -30,6 +30,12 @@ android_test { "ActivityManagerPerfTestsUtils", "collector-device-lib-platform", ], + data: [ + ":ActivityManagerPerfTestsTestApp", + ":ActivityManagerPerfTestsStubApp1", + ":ActivityManagerPerfTestsStubApp2", + ":ActivityManagerPerfTestsStubApp3", + ], platform_apis: true, min_sdk_version: "25", // For android.permission.FORCE_STOP_PACKAGES permission diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt index c49f8fecfdee..be47ec7098f9 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt @@ -19,12 +19,12 @@ package com.android.server.wm.flicker.ime import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation +import android.tools.common.flicker.subject.layers.LayersTraceSubject.Companion.VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import android.tools.device.traces.parsers.toFlickerComponent -import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ImeAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper @@ -58,9 +58,10 @@ class CloseImeToHomeOnFinishActivityTest(flicker: LegacyFlickerTest) : BaseTest( } transitions { broadcastActionTrigger.doAction(ACTION_FINISH_ACTIVITY) - wmHelper.StateSyncBuilder() - .withActivityRemoved(ActivityOptions.Ime.Default.COMPONENT.toFlickerComponent()) - .waitForAndVerify() + wmHelper + .StateSyncBuilder() + .withActivityRemoved(ActivityOptions.Ime.Default.COMPONENT.toFlickerComponent()) + .waitForAndVerify() } teardown { simpleApp.exit(wmHelper) } } @@ -69,10 +70,16 @@ class CloseImeToHomeOnFinishActivityTest(flicker: LegacyFlickerTest) : BaseTest( @Presubmit @Test fun imeLayerBecomesInvisible() = flicker.imeLayerBecomesInvisible() - @FlakyTest(bugId = 246284124) + @Presubmit @Test override fun visibleLayersShownMoreThanOneConsecutiveEntry() { - super.visibleLayersShownMoreThanOneConsecutiveEntry() + flicker.assertLayers { + this.visibleLayersShownMoreThanOneConsecutiveEntry( + VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS.toMutableList().also { + it.add(simpleApp.componentMatcher) + } + ) + } } @Presubmit diff --git a/tests/testables/src/android/testing/TestWithLooperRule.java b/tests/testables/src/android/testing/TestWithLooperRule.java index 99b303e0c43a..37b39c314e53 100644 --- a/tests/testables/src/android/testing/TestWithLooperRule.java +++ b/tests/testables/src/android/testing/TestWithLooperRule.java @@ -19,7 +19,6 @@ package android.testing; import android.testing.TestableLooper.LooperFrameworkMethod; import android.testing.TestableLooper.RunWithLooper; -import org.junit.internal.runners.statements.InvokeMethod; import org.junit.rules.MethodRule; import org.junit.runner.RunWith; import org.junit.runners.model.FrameworkMethod; @@ -79,13 +78,11 @@ public class TestWithLooperRule implements MethodRule { while (next != null) { switch (next.getClass().getSimpleName()) { case "RunAfters": - this.<List<FrameworkMethod>>wrapFieldMethodFor(next, - next.getClass(), "afters", method, target); + this.wrapFieldMethodFor(next, "afters", method, target); next = getNextStatement(next, "next"); break; case "RunBefores": - this.<List<FrameworkMethod>>wrapFieldMethodFor(next, - next.getClass(), "befores", method, target); + this.wrapFieldMethodFor(next, "befores", method, target); next = getNextStatement(next, "next"); break; case "FailOnTimeout": @@ -95,8 +92,10 @@ public class TestWithLooperRule implements MethodRule { next = getNextStatement(next, "originalStatement"); break; case "InvokeMethod": - this.<FrameworkMethod>wrapFieldMethodFor(next, - InvokeMethod.class, "testMethod", method, target); + this.wrapFieldMethodFor(next, "testMethod", method, target); + return; + case "InvokeParameterizedMethod": + this.wrapFieldMethodFor(next, "frameworkMethod", method, target); return; default: throw new Exception( @@ -112,12 +111,11 @@ public class TestWithLooperRule implements MethodRule { // Wrapping the befores, afters, and InvokeMethods with LooperFrameworkMethod // within the statement. - private <T> void wrapFieldMethodFor(Statement base, Class<?> targetClass, String fieldStr, - FrameworkMethod method, Object target) - throws NoSuchFieldException, IllegalAccessException { - Field field = targetClass.getDeclaredField(fieldStr); + private void wrapFieldMethodFor(Statement base, String fieldStr, FrameworkMethod method, + Object target) throws NoSuchFieldException, IllegalAccessException { + Field field = base.getClass().getDeclaredField(fieldStr); field.setAccessible(true); - T fieldInstance = (T) field.get(base); + Object fieldInstance = field.get(base); if (fieldInstance instanceof FrameworkMethod) { field.set(base, looperWrap(method, target, (FrameworkMethod) fieldInstance)); } else { diff --git a/tools/hoststubgen/README.md b/tools/hoststubgen/README.md index 3455b0af4360..1a895dc7dfce 100644 --- a/tools/hoststubgen/README.md +++ b/tools/hoststubgen/README.md @@ -34,11 +34,6 @@ AndroidHeuristicsFilter has hardcoded heuristics to detect AIDL generated classe - `test-tiny-framework/` See `README.md` in it. - - `test-framework` - This directory was used during the prototype phase, but now that we have real ravenwood tests, - this directory is obsolete and should be deleted. - - - `scripts` - `dump-jar.sh` diff --git a/tools/hoststubgen/hoststubgen/Android.bp b/tools/hoststubgen/hoststubgen/Android.bp index 4eac361d6e53..57bcc04a8aec 100644 --- a/tools/hoststubgen/hoststubgen/Android.bp +++ b/tools/hoststubgen/hoststubgen/Android.bp @@ -9,7 +9,7 @@ package { // Visibility only for ravenwood prototype uses. genrule_defaults { - name: "hoststubgen-for-prototype-only-genrule", + name: "ravenwood-internal-only-visibility-genrule", visibility: [ ":__subpackages__", "//frameworks/base", @@ -19,7 +19,7 @@ genrule_defaults { // Visibility only for ravenwood prototype uses. java_defaults { - name: "hoststubgen-for-prototype-only-java", + name: "ravenwood-internal-only-visibility-java", visibility: [ ":__subpackages__", "//frameworks/base", @@ -29,7 +29,7 @@ java_defaults { // Visibility only for ravenwood prototype uses. filegroup_defaults { - name: "hoststubgen-for-prototype-only-filegroup", + name: "ravenwood-internal-only-visibility-filegroup", visibility: [ ":__subpackages__", "//frameworks/base", @@ -41,7 +41,7 @@ filegroup_defaults { // This is only for the prototype. The productionized version is "ravenwood-annotations". java_library { name: "hoststubgen-annotations", - defaults: ["hoststubgen-for-prototype-only-java"], + defaults: ["ravenwood-internal-only-visibility-java"], srcs: [ "annotations-src/**/*.java", ], @@ -115,7 +115,7 @@ java_test_host { // This is only for the prototype. The productionized version is "ravenwood-standard-options". filegroup { name: "hoststubgen-standard-options", - defaults: ["hoststubgen-for-prototype-only-filegroup"], + defaults: ["ravenwood-internal-only-visibility-filegroup"], srcs: [ "hoststubgen-standard-options.txt", ], @@ -153,149 +153,25 @@ genrule_defaults { ], } -// Generate the stub/impl from framework-all, with hidden APIs. -java_genrule_host { - name: "framework-all-hidden-api-host", - defaults: ["hoststubgen-command-defaults"], - cmd: hoststubgen_common_options + - "--in-jar $(location :framework-all) " + - "--policy-override-file $(location framework-policy-override.txt) ", - srcs: [ - ":framework-all", - "framework-policy-override.txt", - ], - visibility: ["//visibility:private"], -} - -// Extract the stub jar from "framework-all-host" for subsequent build rules. -// This is only for the prototype. Do not use it in "productionized" build rules. -java_genrule_host { - name: "framework-all-hidden-api-host-stub", - defaults: ["hoststubgen-for-prototype-only-genrule"], - cmd: "cp $(in) $(out)", - srcs: [ - ":framework-all-hidden-api-host{host_stub.jar}", - ], - out: [ - "host_stub.jar", - ], -} - -// Extract the impl jar from "framework-all-host" for subsequent build rules. -// This is only for the prototype. Do not use it in "productionized" build rules. -java_genrule_host { - name: "framework-all-hidden-api-host-impl", - defaults: ["hoststubgen-for-prototype-only-genrule"], - cmd: "cp $(in) $(out)", - srcs: [ - ":framework-all-hidden-api-host{host_impl.jar}", - ], - out: [ - "host_impl.jar", - ], -} - -// Generate the stub/impl from framework-all, with only public/system/test APIs, without -// hidden APIs. -// This is only for the prototype. Do not use it in "productionized" build rules. -java_genrule_host { - name: "framework-all-test-api-host", - defaults: ["hoststubgen-command-defaults"], - cmd: hoststubgen_common_options + - "--intersect-stub-jar $(location :android_test_stubs_current{.jar}) " + - "--in-jar $(location :framework-all) " + - "--policy-override-file $(location framework-policy-override.txt) ", - srcs: [ - ":framework-all", - ":android_test_stubs_current{.jar}", - "framework-policy-override.txt", - ], - visibility: ["//visibility:private"], -} - -// Extract the stub jar from "framework-all-test-api-host" for subsequent build rules. -// This is only for the prototype. Do not use it in "productionized" build rules. -java_genrule_host { - name: "framework-all-test-api-host-stub", - defaults: ["hoststubgen-for-prototype-only-genrule"], - cmd: "cp $(in) $(out)", - srcs: [ - ":framework-all-test-api-host{host_stub.jar}", - ], - out: [ - "host_stub.jar", - ], -} - -// Extract the impl jar from "framework-all-test-api-host" for subsequent build rules. -// This is only for the prototype. Do not use it in "productionized" build rules. -java_genrule_host { - name: "framework-all-test-api-host-impl", - defaults: ["hoststubgen-for-prototype-only-genrule"], - cmd: "cp $(in) $(out)", - srcs: [ - ":framework-all-test-api-host{host_impl.jar}", - ], - out: [ - "host_impl.jar", - ], -} - -// This library contains helper classes to build hostside tests/targets. -// This essentially contains dependencies from tests that we can't actually use the real ones. -// For example, the actual AndroidTestCase and AndroidJUnit4 don't run on the host side (yet), -// so we pup "fake" implementations here. -// Ideally this library should be empty. -java_library_host { - name: "hoststubgen-helper-framework-buildtime", - defaults: ["hoststubgen-for-prototype-only-java"], - srcs: [ - "helper-framework-buildtime-src/**/*.java", - ], - libs: [ - // We need it to pull in some of the framework classes used in this library, - // such as Context.java. - "framework-all-hidden-api-host-impl", - "junit", - ], -} - -// This module contains "fake" libcore/dalvik classes, framework native substitution, etc, -// that are needed at runtime. -java_library_host { - name: "hoststubgen-helper-framework-runtime", - defaults: ["hoststubgen-for-prototype-only-java"], - srcs: [ - "helper-framework-runtime-src/**/*.java", - ], - exclude_srcs: [ - "helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/CursorWindow_host.java", - ], - libs: [ - "hoststubgen-helper-runtime", - "framework-all-hidden-api-host-impl", - ], -} - java_library_host { name: "hoststubgen-helper-libcore-runtime", - defaults: ["hoststubgen-for-prototype-only-java"], srcs: [ "helper-framework-runtime-src/libcore-fake/**/*.java", ], + visibility: ["//visibility:private"], } java_host_for_device { name: "hoststubgen-helper-libcore-runtime.ravenwood", - defaults: ["hoststubgen-for-prototype-only-java"], libs: [ "hoststubgen-helper-libcore-runtime", ], + visibility: ["//visibility:private"], } java_library { name: "hoststubgen-helper-framework-runtime.ravenwood", - defaults: ["hoststubgen-for-prototype-only-java"], + defaults: ["ravenwood-internal-only-visibility-java"], srcs: [ "helper-framework-runtime-src/framework/**/*.java", ], @@ -308,88 +184,3 @@ java_library { "hoststubgen-helper-libcore-runtime.ravenwood", ], } - -// Defaults for host side test modules. -// We need two rules for each test. -// 1. A "-test-lib" jar, which compiles the test against the stub jar. -// This one is only used by the second rule, so it should be "private. -// 2. A "-test" jar, which includes 1 + the runtime (impl) jars. - -// This and next ones are for tests using framework-app, with hidden APIs. -java_defaults { - name: "hosttest-with-framework-all-hidden-api-test-lib-defaults", - installable: false, - libs: [ - "framework-all-hidden-api-host-stub", - ], - static_libs: [ - "hoststubgen-helper-framework-buildtime", - "framework-annotations-lib", - ], - visibility: ["//visibility:private"], -} - -// Default rules to include `libandroid_runtime`. For now, it's empty, but we'll use it -// once we start using JNI. -java_defaults { - name: "hosttest-with-libandroid_runtime", - jni_libs: [ - // "libandroid_runtime", - - // TODO: Figure out how to build them automatically. - // Following ones are depended by libandroid_runtime. - // Without listing them here, not only we won't get them under - // $ANDROID_HOST_OUT/testcases/*/lib64, but also not under - // $ANDROID_HOST_OUT/lib64, so we'd fail to load them at runtime. - // ($ANDROID_HOST_OUT/lib/ gets all of them though.) - // "libcutils", - // "libharfbuzz_ng", - // "libminikin", - // "libz", - // "libbinder", - // "libhidlbase", - // "libvintf", - // "libicu", - // "libutils", - // "libtinyxml2", - ], -} - -java_defaults { - name: "hosttest-with-framework-all-hidden-api-test-defaults", - defaults: ["hosttest-with-libandroid_runtime"], - installable: false, - test_config: "AndroidTest-host.xml", - static_libs: [ - "hoststubgen-helper-runtime", - "hoststubgen-helper-framework-runtime", - "framework-all-hidden-api-host-impl", - ], -} - -// This and next ones are for tests using framework-app, with public/system/test APIs, -// without hidden APIs. -java_defaults { - name: "hosttest-with-framework-all-test-api-test-lib-defaults", - installable: false, - libs: [ - "framework-all-test-api-host-stub", - ], - static_libs: [ - "hoststubgen-helper-framework-buildtime", - "framework-annotations-lib", - ], - visibility: ["//visibility:private"], -} - -java_defaults { - name: "hosttest-with-framework-all-test-api-test-defaults", - defaults: ["hosttest-with-libandroid_runtime"], - installable: false, - test_config: "AndroidTest-host.xml", - static_libs: [ - "hoststubgen-helper-runtime", - "hoststubgen-helper-framework-runtime", - "framework-all-test-api-host-impl", - ], -} diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/annotation/NonNull.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/annotation/NonNull.java deleted file mode 100644 index 51c5d9a05e52..000000000000 --- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/annotation/NonNull.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2013 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 androidx.annotation; - -// [ravenwood] TODO: Find the actual androidx jar containing it.s - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -/** - * Denotes that a parameter, field or method return value can never be null. - * <p> - * This is a marker annotation and it has no specific attributes. - * - * @paramDoc This value cannot be {@code null}. - * @returnDoc This value cannot be {@code null}. - * @hide - */ -@Retention(SOURCE) -@Target({METHOD, PARAMETER, FIELD}) -public @interface NonNull { -} diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/annotation/Nullable.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/annotation/Nullable.java deleted file mode 100644 index f1f0e8b43f16..000000000000 --- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/annotation/Nullable.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2013 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 androidx.annotation; - -// [ravenwood] TODO: Find the actual androidx jar containing it.s - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -/** - * Denotes that a parameter, field or method return value can be null. - * <p> - * When decorating a method call parameter, this denotes that the parameter can - * legitimately be null and the method will gracefully deal with it. Typically - * used on optional parameters. - * <p> - * When decorating a method, this denotes the method might legitimately return - * null. - * <p> - * This is a marker annotation and it has no specific attributes. - * - * @paramDoc This value may be {@code null}. - * @returnDoc This value may be {@code null}. - * @hide - */ -@Retention(SOURCE) -@Target({METHOD, PARAMETER, FIELD}) -public @interface Nullable { -} diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/ext/junit/runners/AndroidJUnit4.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/ext/junit/runners/AndroidJUnit4.java deleted file mode 100644 index 0c82e4e268d3..000000000000 --- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/ext/junit/runners/AndroidJUnit4.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 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. - */ - -package androidx.test.ext.junit.runners; - -import org.junit.runners.BlockJUnit4ClassRunner; -import org.junit.runners.model.InitializationError; - -// TODO: We need to simulate the androidx test runner. -// https://source.corp.google.com/piper///depot/google3/third_party/android/androidx_test/ext/junit/java/androidx/test/ext/junit/runners/AndroidJUnit4.java -// https://source.corp.google.com/piper///depot/google3/third_party/android/androidx_test/runner/android_junit_runner/java/androidx/test/internal/runner/junit4/AndroidJUnit4ClassRunner.java - -public class AndroidJUnit4 extends BlockJUnit4ClassRunner { - public AndroidJUnit4(Class<?> testClass) throws InitializationError { - super(testClass); - } -} diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/FlakyTest.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/FlakyTest.java deleted file mode 100644 index 2470d8390f5d..000000000000 --- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/FlakyTest.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2014 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 androidx.test.filters; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Designates a test as being flaky (non-deterministic). - * - * <p>Can then be used to filter tests on execution using -e annotation or -e notAnnotation as - * desired. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) -public @interface FlakyTest { - /** - * An optional bug number associated with the test. -1 Means that no bug number is associated with - * the flaky annotation. - * - * @return int - */ - int bugId() default -1; - - /** - * Details, such as the reason of why the test is flaky. - * - * @return String - */ - String detail() default ""; -} diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/LargeTest.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/LargeTest.java deleted file mode 100644 index 578d7dc73647..000000000000 --- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/LargeTest.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2016 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 androidx.test.filters; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation to assign a large test size qualifier to a test. This annotation can be used at a - * method or class level. - * - * <p>Test size qualifiers are a great way to structure test code and are used to assign a test to a - * test suite of similar run time. - * - * <p>Execution time: >1000ms - * - * <p>Large tests should be focused on testing integration of all application components. These - * tests fully participate in the system and may make use of all resources such as databases, file - * systems and network. As a rule of thumb most functional UI tests are large tests. - * - * <p>Note: This class replaces the deprecated Android platform size qualifier <a - * href="{@docRoot}reference/android/test/suitebuilder/annotation/LargeTest.html"><code> - * android.test.suitebuilder.annotation.LargeTest</code></a> and is the recommended way to annotate - * tests written with the AndroidX Test Library. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) -public @interface LargeTest {} diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/MediumTest.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/MediumTest.java deleted file mode 100644 index dfdaa53ee6ac..000000000000 --- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/MediumTest.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2016 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 androidx.test.filters; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation to assign a medium test size qualifier to a test. This annotation can be used at a - * method or class level. - * - * <p>Test size qualifiers are a great way to structure test code and are used to assign a test to a - * test suite of similar run time. - * - * <p>Execution time: <1000ms - * - * <p>Medium tests should be focused on a very limited subset of components or a single component. - * Resource access to the file system through well defined interfaces like databases, - * ContentProviders, or Context is permitted. Network access should be restricted, (long-running) - * blocking operations should be avoided and use mock objects instead. - * - * <p>Note: This class replaces the deprecated Android platform size qualifier <a - * href="{@docRoot}reference/android/test/suitebuilder/annotation/MediumTest.html"><code> - * android.test.suitebuilder.annotation.MediumTest</code></a> and is the recommended way to annotate - * tests written with the AndroidX Test Library. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) -public @interface MediumTest {} diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/RequiresDevice.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/RequiresDevice.java deleted file mode 100644 index 3d3ee3318bfa..000000000000 --- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/RequiresDevice.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2014 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 androidx.test.filters; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Indicates that a specific test should not be run on emulator. - * - * <p>It will be executed only if the test is running on the physical android device. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE, ElementType.METHOD}) -public @interface RequiresDevice {} diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/SdkSuppress.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/SdkSuppress.java deleted file mode 100644 index dd65ddb382dc..000000000000 --- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/SdkSuppress.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2014 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 androidx.test.filters; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Indicates that a specific test or class requires a minimum or maximum API Level to execute. - * - * <p>Test(s) will be skipped when executed on android platforms less/more than specified level - * (inclusive). - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE, ElementType.METHOD}) -public @interface SdkSuppress { - /** The minimum API level to execute (inclusive) */ - int minSdkVersion() default 1; - /** The maximum API level to execute (inclusive) */ - int maxSdkVersion() default Integer.MAX_VALUE; - /** - * The {@link android.os.Build.VERSION.CODENAME} to execute on. This is intended to be used to run - * on a pre-release SDK, where the {@link android.os.Build.VERSION.SDK_INT} has not yet been - * finalized. This is treated as an OR operation with respect to the minSdkVersion and - * maxSdkVersion attributes. - * - * <p>For example, to filter a test so it runs on only the prerelease R SDK: <code> - * {@literal @}SdkSuppress(minSdkVersion = Build.VERSION_CODES.R, codeName = "R") - * </code> - */ - String codeName() default "unset"; -} diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/SmallTest.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/SmallTest.java deleted file mode 100644 index dd32df44effe..000000000000 --- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/SmallTest.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2016 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 androidx.test.filters; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation to assign a small test size qualifier to a test. This annotation can be used at a - * method or class level. - * - * <p>Test size qualifiers are a great way to structure test code and are used to assign a test to a - * test suite of similar run time. - * - * <p>Execution time: <200ms - * - * <p>Small tests should be run very frequently. Focused on units of code to verify specific logical - * conditions. These tests should runs in an isolated environment and use mock objects for external - * dependencies. Resource access (such as file system, network, or databases) are not permitted. - * Tests that interact with hardware, make binder calls, or that facilitate android instrumentation - * should not use this annotation. - * - * <p>Note: This class replaces the deprecated Android platform size qualifier <a - * href="http://developer.android.com/reference/android/test/suitebuilder/annotation/SmallTest.html"> - * android.test.suitebuilder.annotation.SmallTest</a> and is the recommended way to annotate tests - * written with the AndroidX Test Library. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) -public @interface SmallTest {} diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/Suppress.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/Suppress.java deleted file mode 100644 index 88e636c2dd77..000000000000 --- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/Suppress.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2016 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 androidx.test.filters; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Use this annotation on test classes or test methods that should not be included in a test suite. - * If the annotation appears on the class then no tests in that class will be included. If the - * annotation appears only on a test method then only that method will be excluded. - * - * <p>Note: This class replaces the deprecated Android platform annotation <a - * href="http://developer.android.com/reference/android/test/suitebuilder/annotation/Suppress.html"> - * android.test.suitebuilder.annotation.Suppress</a> and is the recommended way to suppress tests - * written with the AndroidX Test Library. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) -public @interface Suppress {} diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/runner/AndroidJUnit4.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/runner/AndroidJUnit4.java deleted file mode 100644 index e1379390f98b..000000000000 --- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/runner/AndroidJUnit4.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (C) 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. - */ -package androidx.test.runner; - -import org.junit.runners.model.InitializationError; - -public class AndroidJUnit4 extends androidx.test.ext.junit.runners.AndroidJUnit4 { - public AndroidJUnit4(Class<?> testClass) throws InitializationError { - super(testClass); - } -} diff --git a/tools/hoststubgen/hoststubgen/test-framework/Android.bp b/tools/hoststubgen/hoststubgen/test-framework/Android.bp deleted file mode 100644 index 2b91cc161b7f..000000000000 --- a/tools/hoststubgen/hoststubgen/test-framework/Android.bp +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (C) 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. - -package { - default_applicable_licenses: ["Android-Apache-2.0"], -} - -build = ["AndroidHostTest.bp"] diff --git a/tools/hoststubgen/hoststubgen/test-framework/AndroidHostTest.bp b/tools/hoststubgen/hoststubgen/test-framework/AndroidHostTest.bp deleted file mode 100644 index 1f8382ad468d..000000000000 --- a/tools/hoststubgen/hoststubgen/test-framework/AndroidHostTest.bp +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (C) 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. - -// Add `build = ["AndroidHostTest.bp"]` to Android.bp to include this file. - -// Compile the test jar, using 2 rules. -// 1. Build the test against the stub. -java_library_host { - name: "HostStubGenTest-framework-test-host-test-lib", - defaults: ["hosttest-with-framework-all-hidden-api-test-lib-defaults"], - srcs: [ - "src/**/*.java", - ], - static_libs: [ - "junit", - "truth", - "mockito", - - // http://cs/h/googleplex-android/platform/superproject/main/+/main:platform_testing/libraries/annotations/src/android/platform/test/annotations/ - "platform-test-annotations", - "hoststubgen-annotations", - ], -} - -// 2. Link the above module with necessary runtime dependencies, so it can be executed stand-alone. -java_test_host { - name: "HostStubGenTest-framework-all-test-host-test", - defaults: ["hosttest-with-framework-all-hidden-api-test-defaults"], - static_libs: [ - "HostStubGenTest-framework-test-host-test-lib", - ], - test_suites: ["general-tests"], -} - -// "Productionized" build rule. -android_ravenwood_test { - name: "HostStubGenTest-framework-test", - srcs: [ - "src/**/*.java", - ], - static_libs: [ - "junit", - "truth", - "mockito", - ], -} diff --git a/tools/hoststubgen/hoststubgen/test-framework/AndroidTest-host.xml b/tools/hoststubgen/hoststubgen/test-framework/AndroidTest-host.xml deleted file mode 100644 index f35dcf69ecc9..000000000000 --- a/tools/hoststubgen/hoststubgen/test-framework/AndroidTest-host.xml +++ /dev/null @@ -1,29 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 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. ---> - -<!-- [Ravenwood] Copied from $ANDROID_BUILD_TOP/cts/hostsidetests/devicepolicy/AndroidTest.xml --> -<configuration description="CtsContentTestCases host-side test"> - <option name="test-suite-tag" value="ravenwood" /> - <option name="config-descriptor:metadata" key="component" value="framework" /> - <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" /> - <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" /> - <option name="config-descriptor:metadata" key="parameter" value="not_secondary_user" /> - <option name="config-descriptor:metadata" key="parameter" value="no_foldable_states" /> - - <test class="com.android.tradefed.testtype.IsolatedHostTest" > - <option name="jar" value="HostStubGenTest-framework-all-test-host-test.jar" /> - </test> -</configuration> diff --git a/tools/hoststubgen/hoststubgen/test-framework/README.md b/tools/hoststubgen/hoststubgen/test-framework/README.md deleted file mode 100644 index 26a9ad13f746..000000000000 --- a/tools/hoststubgen/hoststubgen/test-framework/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# HostStubGen: (obsolete) real framework test - -This directory contains tests against the actual framework.jar code. The tests were -copied from somewhere else in the android tree. We use this directory to quickly run existing -tests. - -This directory was used during the prototype phase, but now that we have real ravenwood tests, -this directory is obsolete and should be deleted. - -## How to run - -- With `atest`. This is the proper way to run it, but it may fail due to atest's known problems. - -``` -$ atest HostStubGenTest-framework-all-test-host-test -``` - -- Advanced option: `run-test-without-atest.sh` runs the test without using `atest` - -``` -$ ./run-test-without-atest.sh -```
\ No newline at end of file diff --git a/tools/hoststubgen/hoststubgen/test-framework/run-test-without-atest.sh b/tools/hoststubgen/hoststubgen/test-framework/run-test-without-atest.sh deleted file mode 100755 index cfc06a12e881..000000000000 --- a/tools/hoststubgen/hoststubgen/test-framework/run-test-without-atest.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/bin/bash -# Copyright (C) 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. - -# Run HostStubGenTest-framework-test-host-test directly with JUnit. -# (without using atest.) - -source "${0%/*}"/../../common.sh - - -# Options: -# -v enable verbose log -# -d enable debugger - -verbose=0 -debug=0 -while getopts "vd" opt; do - case "$opt" in - v) verbose=1 ;; - d) debug=1 ;; - esac -done -shift $(($OPTIND - 1)) - - -if (( $verbose )) ; then - JAVA_OPTS="$JAVA_OPTS -verbose:class" -fi - -if (( $debug )) ; then - JAVA_OPTS="$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8700" -fi - -#======================================= -module=HostStubGenTest-framework-all-test-host-test -module_jar=$ANDROID_BUILD_TOP/out/host/linux-x86/testcases/$module/$module.jar -run m $module - -out=out - -rm -fr $out -mkdir -p $out - - -# Copy and extract the relevant jar files so we can look into them. -run cp \ - $module_jar \ - $SOONG_INT/frameworks/base/tools/hoststubgen/hoststubgen/framework-all-hidden-api-host/linux_glibc_common/gen/*.jar \ - $out - -run extract $out/*.jar - -# Result is the number of failed tests. -result=0 - - -# This suite runs all tests in the JAR. -tests=(com.android.hoststubgen.hosthelper.HostTestSuite) - -# Uncomment this to run a specific test. -# tests=(com.android.hoststubgen.frameworktest.LogTest) - - -for class in ${tests[@]} ; do - echo "Running $class ..." - - run cd "${module_jar%/*}" - run $JAVA $JAVA_OPTS \ - -cp $module_jar \ - org.junit.runner.JUnitCore \ - $class || result=$(( $result + 1 )) -done - -exit $result diff --git a/tools/hoststubgen/hoststubgen/test-framework/src/com/android/hoststubgen/frameworktest/ArrayMapTest.java b/tools/hoststubgen/hoststubgen/test-framework/src/com/android/hoststubgen/frameworktest/ArrayMapTest.java deleted file mode 100644 index 2c5949c1c630..000000000000 --- a/tools/hoststubgen/hoststubgen/test-framework/src/com/android/hoststubgen/frameworktest/ArrayMapTest.java +++ /dev/null @@ -1,472 +0,0 @@ -/* - * Copyright (C) 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. - */ -package com.android.hoststubgen.frameworktest; - -// [ravewnwood] Copied from cts/, and commented out unsupported stuff. - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import android.util.ArrayMap; -import android.util.Log; - -import org.junit.Test; - -import java.util.AbstractMap; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Map.Entry; -import java.util.NoSuchElementException; -import java.util.Set; -import java.util.function.BiFunction; - -/** - * Some basic tests for {@link android.util.ArrayMap}. - */ -public class ArrayMapTest { - static final boolean DEBUG = false; - - private static boolean compare(Object v1, Object v2) { - if (v1 == null) { - return v2 == null; - } - if (v2 == null) { - return false; - } - return v1.equals(v2); - } - - private static void compareMaps(HashMap map, ArrayMap array) { - if (map.size() != array.size()) { - fail("Bad size: expected " + map.size() + ", got " + array.size()); - } - - Set<Entry> mapSet = map.entrySet(); - for (Map.Entry entry : mapSet) { - Object expValue = entry.getValue(); - Object gotValue = array.get(entry.getKey()); - if (!compare(expValue, gotValue)) { - fail("Bad value: expected " + expValue + ", got " + gotValue - + " at key " + entry.getKey()); - } - } - - for (int i = 0; i < array.size(); i++) { - Object gotValue = array.valueAt(i); - Object key = array.keyAt(i); - Object expValue = map.get(key); - if (!compare(expValue, gotValue)) { - fail("Bad value: expected " + expValue + ", got " + gotValue - + " at key " + key); - } - } - - if (map.entrySet().hashCode() != array.entrySet().hashCode()) { - fail("Entry set hash codes differ: map=0x" - + Integer.toHexString(map.entrySet().hashCode()) + " array=0x" - + Integer.toHexString(array.entrySet().hashCode())); - } - - if (!map.entrySet().equals(array.entrySet())) { - fail("Failed calling equals on map entry set against array set"); - } - - if (!array.entrySet().equals(map.entrySet())) { - fail("Failed calling equals on array entry set against map set"); - } - - if (map.keySet().hashCode() != array.keySet().hashCode()) { - fail("Key set hash codes differ: map=0x" - + Integer.toHexString(map.keySet().hashCode()) + " array=0x" - + Integer.toHexString(array.keySet().hashCode())); - } - - if (!map.keySet().equals(array.keySet())) { - fail("Failed calling equals on map key set against array set"); - } - - if (!array.keySet().equals(map.keySet())) { - fail("Failed calling equals on array key set against map set"); - } - - if (!map.keySet().containsAll(array.keySet())) { - fail("Failed map key set contains all of array key set"); - } - - if (!array.keySet().containsAll(map.keySet())) { - fail("Failed array key set contains all of map key set"); - } - - if (!array.containsAll(map.keySet())) { - fail("Failed array contains all of map key set"); - } - - if (!map.entrySet().containsAll(array.entrySet())) { - fail("Failed map entry set contains all of array entry set"); - } - - if (!array.entrySet().containsAll(map.entrySet())) { - fail("Failed array entry set contains all of map entry set"); - } - } - - private static void validateArrayMap(ArrayMap array) { - Set<Map.Entry> entrySet = array.entrySet(); - int index = 0; - Iterator<Entry> entryIt = entrySet.iterator(); - while (entryIt.hasNext()) { - Map.Entry entry = entryIt.next(); - Object value = entry.getKey(); - Object realValue = array.keyAt(index); - if (!compare(realValue, value)) { - fail("Bad array map entry set: expected key " + realValue - + ", got " + value + " at index " + index); - } - value = entry.getValue(); - realValue = array.valueAt(index); - if (!compare(realValue, value)) { - fail("Bad array map entry set: expected value " + realValue - + ", got " + value + " at index " + index); - } - index++; - } - - index = 0; - Set keySet = array.keySet(); - Iterator keyIt = keySet.iterator(); - while (keyIt.hasNext()) { - Object value = keyIt.next(); - Object realValue = array.keyAt(index); - if (!compare(realValue, value)) { - fail("Bad array map key set: expected key " + realValue - + ", got " + value + " at index " + index); - } - index++; - } - - index = 0; - Collection valueCol = array.values(); - Iterator valueIt = valueCol.iterator(); - while (valueIt.hasNext()) { - Object value = valueIt.next(); - Object realValue = array.valueAt(index); - if (!compare(realValue, value)) { - fail("Bad array map value col: expected value " + realValue - + ", got " + value + " at index " + index); - } - index++; - } - } - - private static void dump(Map map, ArrayMap array) { - Log.e("test", "HashMap of " + map.size() + " entries:"); - Set<Map.Entry> mapSet = map.entrySet(); - for (Map.Entry entry : mapSet) { - Log.e("test", " " + entry.getKey() + " -> " + entry.getValue()); - } - Log.e("test", "ArrayMap of " + array.size() + " entries:"); - for (int i = 0; i < array.size(); i++) { - Log.e("test", " " + array.keyAt(i) + " -> " + array.valueAt(i)); - } - } - - private static void dump(ArrayMap map1, ArrayMap map2) { - Log.e("test", "ArrayMap of " + map1.size() + " entries:"); - for (int i = 0; i < map1.size(); i++) { - Log.e("test", " " + map1.keyAt(i) + " -> " + map1.valueAt(i)); - } - Log.e("test", "ArrayMap of " + map2.size() + " entries:"); - for (int i = 0; i < map2.size(); i++) { - Log.e("test", " " + map2.keyAt(i) + " -> " + map2.valueAt(i)); - } - } - - @Test - public void testCopyArrayMap() { - // map copy constructor test - ArrayMap newMap = new ArrayMap<Integer, String>(); - for (int i = 0; i < 10; ++i) { - newMap.put(i, String.valueOf(i)); - } - ArrayMap mapCopy = new ArrayMap(newMap); - if (!compare(mapCopy, newMap)) { - String msg = "ArrayMap copy constructor failure: expected " + - newMap + ", got " + mapCopy; - Log.e("test", msg); - dump(newMap, mapCopy); - fail(msg); - return; - } - } - - @Test - public void testEqualsArrayMap() { - ArrayMap<Integer, String> map1 = new ArrayMap<>(); - ArrayMap<Integer, String> map2 = new ArrayMap<>(); - HashMap<Integer, String> map3 = new HashMap<>(); - if (!compare(map1, map2) || !compare(map1, map3) || !compare(map3, map2)) { - fail("ArrayMap equals failure for empty maps " + map1 + ", " + - map2 + ", " + map3); - } - - for (int i = 0; i < 10; ++i) { - String value = String.valueOf(i); - map1.put(i, value); - map2.put(i, value); - map3.put(i, value); - } - if (!compare(map1, map2) || !compare(map1, map3) || !compare(map3, map2)) { - fail("ArrayMap equals failure for populated maps " + map1 + ", " + - map2 + ", " + map3); - } - - map1.remove(0); - if (compare(map1, map2) || compare(map1, map3) || compare(map3, map1)) { - fail("ArrayMap equals failure for map size " + map1 + ", " + - map2 + ", " + map3); - } - - map1.put(0, "-1"); - if (compare(map1, map2) || compare(map1, map3) || compare(map3, map1)) { - fail("ArrayMap equals failure for map contents " + map1 + ", " + - map2 + ", " + map3); - } - } - - private static void checkEntrySetToArray(ArrayMap<?, ?> testMap) { - try { - testMap.entrySet().toArray(); - fail(); - } catch (UnsupportedOperationException expected) { - } - - try { - Map.Entry<?, ?>[] entries = new Map.Entry[20]; - testMap.entrySet().toArray(entries); - fail(); - } catch (UnsupportedOperationException expected) { - } - } - - // http://b/32294038, Test ArrayMap.entrySet().toArray() - @Test - public void testEntrySetArray() { - // Create - ArrayMap<Integer, String> testMap = new ArrayMap<>(); - - // Test empty - checkEntrySetToArray(testMap); - - // Test non-empty - for (int i = 0; i < 10; ++i) { - testMap.put(i, String.valueOf(i)); - } - checkEntrySetToArray(testMap); - } - - @Test - public void testCanNotIteratePastEnd_entrySetIterator() { - Map<String, String> map = new ArrayMap<>(); - map.put("key 1", "value 1"); - map.put("key 2", "value 2"); - Set<Map.Entry<String, String>> expectedEntriesToIterate = new HashSet<>(Arrays.asList( - entryOf("key 1", "value 1"), - entryOf("key 2", "value 2") - )); - Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator(); - - // Assert iteration over the expected two entries in any order - assertTrue(iterator.hasNext()); - Map.Entry<String, String> firstEntry = copyOf(iterator.next()); - assertTrue(expectedEntriesToIterate.remove(firstEntry)); - - assertTrue(iterator.hasNext()); - Map.Entry<String, String> secondEntry = copyOf(iterator.next()); - assertTrue(expectedEntriesToIterate.remove(secondEntry)); - - assertFalse(iterator.hasNext()); - - try { - iterator.next(); - fail(); - } catch (NoSuchElementException expected) { - } - } - - private static <K, V> Map.Entry<K, V> entryOf(K key, V value) { - return new AbstractMap.SimpleEntry<>(key, value); - } - - private static <K, V> Map.Entry<K, V> copyOf(Map.Entry<K, V> entry) { - return entryOf(entry.getKey(), entry.getValue()); - } - - @Test - public void testCanNotIteratePastEnd_keySetIterator() { - Map<String, String> map = new ArrayMap<>(); - map.put("key 1", "value 1"); - map.put("key 2", "value 2"); - Set<String> expectedKeysToIterate = new HashSet<>(Arrays.asList("key 1", "key 2")); - Iterator<String> iterator = map.keySet().iterator(); - - // Assert iteration over the expected two keys in any order - assertTrue(iterator.hasNext()); - String firstKey = iterator.next(); - assertTrue(expectedKeysToIterate.remove(firstKey)); - - assertTrue(iterator.hasNext()); - String secondKey = iterator.next(); - assertTrue(expectedKeysToIterate.remove(secondKey)); - - assertFalse(iterator.hasNext()); - - try { - iterator.next(); - fail(); - } catch (NoSuchElementException expected) { - } - } - - @Test - public void testCanNotIteratePastEnd_valuesIterator() { - Map<String, String> map = new ArrayMap<>(); - map.put("key 1", "value 1"); - map.put("key 2", "value 2"); - Set<String> expectedValuesToIterate = new HashSet<>(Arrays.asList("value 1", "value 2")); - Iterator<String> iterator = map.values().iterator(); - - // Assert iteration over the expected two values in any order - assertTrue(iterator.hasNext()); - String firstValue = iterator.next(); - assertTrue(expectedValuesToIterate.remove(firstValue)); - - assertTrue(iterator.hasNext()); - String secondValue = iterator.next(); - assertTrue(expectedValuesToIterate.remove(secondValue)); - - assertFalse(iterator.hasNext()); - - try { - iterator.next(); - fail(); - } catch (NoSuchElementException expected) { - } - } - - @Test - public void testForEach() { - ArrayMap<String, Integer> map = new ArrayMap<>(); - - for (int i = 0; i < 50; ++i) { - map.put(Integer.toString(i), i * 10); - } - - // Make sure forEach goes through all of the elements. - HashMap<String, Integer> seen = new HashMap<>(); - map.forEach(seen::put); - compareMaps(seen, map); - } - - /** - * The entrySet Iterator returns itself from each call to {@code next()}. This is unusual - * behavior for {@link Iterator#next()}; this test ensures that any future change to this - * behavior is deliberate. - */ - @Test - public void testUnusualBehavior_eachEntryIsSameAsIterator_entrySetIterator() { - Map<String, String> map = new ArrayMap<>(); - map.put("key 1", "value 1"); - map.put("key 2", "value 2"); - Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator(); - - assertSame(iterator, iterator.next()); - assertSame(iterator, iterator.next()); - } - - @SuppressWarnings("SelfEquals") - @Test - public void testUnusualBehavior_equalsThrowsAfterRemove_entrySetIterator() { - Map<String, String> map = new ArrayMap<>(); - map.put("key 1", "value 1"); - map.put("key 2", "value 2"); - Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator(); - iterator.next(); - iterator.remove(); - try { - iterator.equals(iterator); - fail(); - } catch (IllegalStateException expected) { - } - } - - private static <T> void assertEqualsBothWays(T a, T b) { - assertEquals(a, b); - assertEquals(b, a); - assertEquals(a.hashCode(), b.hashCode()); - } - - @Test - public void testRemoveAll() { - final ArrayMap<Integer, String> map = new ArrayMap<>(); - for (Integer i : Arrays.asList(0, 1, 2, 3, 4, 5)) { - map.put(i, i.toString()); - } - - final ArrayMap<Integer, String> expectedMap = new ArrayMap<>(); - for (Integer i : Arrays.asList(2, 4)) { - expectedMap.put(i, String.valueOf(i)); - } - map.removeAll(Arrays.asList(0, 1, 3, 5, 6)); - if (!compare(map, expectedMap)) { - fail("ArrayMap removeAll failure, expect " + expectedMap + ", but " + map); - } - - map.removeAll(Collections.emptyList()); - if (!compare(map, expectedMap)) { - fail("ArrayMap removeAll failure for empty maps, expect " + expectedMap + ", but " + - map); - } - - map.removeAll(Arrays.asList(2, 4)); - if (!map.isEmpty()) { - fail("ArrayMap removeAll failure, expect empty, but " + map); - } - } - - @Test - public void testReplaceAll() { - final ArrayMap<Integer, Integer> map = new ArrayMap<>(); - final ArrayMap<Integer, Integer> expectedMap = new ArrayMap<>(); - final BiFunction<Integer, Integer, Integer> function = (k, v) -> 2 * v; - for (Integer i : Arrays.asList(0, 1, 2, 3, 4, 5)) { - map.put(i, i); - expectedMap.put(i, 2 * i); - } - - map.replaceAll(function); - if (!compare(map, expectedMap)) { - fail("ArrayMap replaceAll failure, expect " + expectedMap + ", but " + map); - } - } -} diff --git a/tools/hoststubgen/hoststubgen/test-framework/src/com/android/hoststubgen/frameworktest/LogTest.java b/tools/hoststubgen/hoststubgen/test-framework/src/com/android/hoststubgen/frameworktest/LogTest.java deleted file mode 100644 index 3e33b54bb9f9..000000000000 --- a/tools/hoststubgen/hoststubgen/test-framework/src/com/android/hoststubgen/frameworktest/LogTest.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 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. - */ -package com.android.hoststubgen.frameworktest; - -import static com.google.common.truth.Truth.assertThat; - -import android.util.Log; - -import org.junit.Test; - -/** - * Some basic tests for {@link android.util.Log}. - */ -public class LogTest { - @Test - public void testBasicLogging() { - Log.v("TAG", "Test v log"); - Log.d("TAG", "Test d log"); - Log.i("TAG", "Test i log"); - Log.w("TAG", "Test w log"); - Log.e("TAG", "Test e log"); - } - - @Test - public void testNativeMethods() { - assertThat(Log.isLoggable("mytag", Log.INFO)).isTrue(); - } -} diff --git a/tools/hoststubgen/scripts/build-framework-hostside-jars-and-extract.sh b/tools/hoststubgen/scripts/build-framework-hostside-jars-and-extract.sh deleted file mode 100755 index 72681234dad8..000000000000 --- a/tools/hoststubgen/scripts/build-framework-hostside-jars-and-extract.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash -# Copyright (C) 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. - - -# Script to build `framework-host-stub` and `framework-host-impl`, and copy the -# generated jars to $out, and unzip them. Useful for looking into the generated files. - -source "${0%/*}"/../common.sh - -out=framework-all-stub-out - -rm -fr $out -mkdir -p $out - -# Build the jars with `m`. -run m framework-all-hidden-api-host - -# Copy the jar to out/ and extract them. -run cp \ - $SOONG_INT/frameworks/base/tools/hoststubgen/hoststubgen/framework-all-hidden-api-host/linux_glibc_common/gen/* \ - $out - -extract $out/*.jar diff --git a/tools/hoststubgen/scripts/run-all-tests.sh b/tools/hoststubgen/scripts/run-all-tests.sh index 222c874ac34b..a6847ae97bae 100755 --- a/tools/hoststubgen/scripts/run-all-tests.sh +++ b/tools/hoststubgen/scripts/run-all-tests.sh @@ -22,7 +22,6 @@ ATEST_ARGS="--host" # These tests are known to pass. READY_TEST_MODULES=( - HostStubGenTest-framework-all-test-host-test hoststubgen-test-tiny-test CtsUtilTestCasesRavenwood CtsOsTestCasesRavenwood # This one uses native sustitution, so let's run it too. @@ -30,7 +29,6 @@ READY_TEST_MODULES=( MUST_BUILD_MODULES=( "${NOT_READY_TEST_MODULES[*]}" - HostStubGenTest-framework-test ) # First, build all the test / etc modules. This shouldn't fail. @@ -44,11 +42,8 @@ run atest $ATEST_ARGS hoststubgentest hoststubgen-invoke-test # files, and they may fail when something changes in the build system. run ./hoststubgen/test-tiny-framework/diff-and-update-golden.sh -run ./hoststubgen/test-framework/run-test-without-atest.sh - run ./hoststubgen/test-tiny-framework/run-test-manually.sh run atest $ATEST_ARGS tiny-framework-dump-test -run ./scripts/build-framework-hostside-jars-and-extract.sh # This script is already broken on goog/master # run ./scripts/build-framework-hostside-jars-without-genrules.sh |